<commit_before>/**
 * Terminal Module for the Cloud9
 *
 * @copyright 2013, Ajax.org B.V.
 */

define(function(require, exports, module) {
    "use strict";

    module.exports = function(c9) {

        var prefix = 'Cloud9 Help ';
        var wrongPortIP = "You may be using the wrong PORT & IP for your server application.";

        var messages = {
            generic: {
                wrongPortIP: prefix + wrongPortIP + "Try passing $PORT and $IP to properly launch your application. You can find more information in our docs.",
                appRunning: prefix + "Your code is running at https://" + c9.hostname + "",
                bindToInternalIP: prefix + wrongPortIP + "Only binding to the internal IP configured in $IP is supported.",
                noLiveReload: prefix + "We currently only support listening on one port. Opening up a second port for live-reloading is currently not possible.",
                addressInUse: prefix + "There are issues starting your app. Please make sure you are using the correct $IP and $PORT, run as the correct user or kill any processes which might be conflicting. You can find more information in our docs.",
                debuggerPortInUse: prefix + "A process is listening on the port specified. Please try and kill that process and restart the app. You can find more information in our docs"
            },
            rails: {
                wrongPortIP: prefix + wrongPortIP + "For rails, use: 'rails s -p $PORT -b $IP'. For Sinatra, use: ruby app.rb -p $PORT -o $IP'."
            },
            node: {
                wrongPortIP: prefix + wrongPortIP + " Use 'process.env.PORT' as the port and 'process.env.IP' as the host in your scripts or refer to the documentation for more information."
            },
            django: {
                wrongPortIP: prefix + wrongPortIP + " Use './manage.py runserver $IP:$PORT' to run your Django application."
            }
        };

        var matchers = [
            {
                // Generic
                pattern: /^(?!reload )server(?: is | )(?:listening|running) (?:at|on)((?!0\.0\.0\.0:8080).)*$/i,
                message: messages.generic.wrongPortIP
            },
            {
                // Generic correct port
                pattern: /^(?!reload )server(?: is | )(?:listening|running) (?:at|on).*?(?=0\.0\.0\.0:8080)/i,
                message: messages.generic.appRunning
            },
            {
                // grunt-serve correct port
                pattern: /Server is running on port (?!8080)/i,
                message: messages.generic.wrongPortIP
            },
            {
                // grunt-serve correct port
                pattern: /Server is running on port 8080/i,
                message: messages.generic.appRunning
            },
            {
                // grunt-reload
                pattern: /^Proxying http:\/\/(?!0\.0\.0\.0:8080)/i,
                message: messages.generic.wrongPortIP
            },
            {
                // grunt-reload
                pattern: /^Proxying http:\/\/(?=0\.0\.0\.0:8080)/i,
                message: messages.generic.appRunning
            },
            {
                // grunt no support for live-reload
                pattern: /^reload server running at http:\/\/.*?:\d{4,5}/i,
                message: messages.generic.noLiveReload
            },
            {
                // Meteor wrong port
                pattern: /(App running at:)((?!0\.0\.0\.0:8080).)*$/,
                message: messages.generic.wrongPortIP
            },
            {
                // Meteor correct port
                pattern: /(App running at:).*?(?=0\.0\.0\.0:8080)/,
                message: messages.generic.appRunning
            },
            {
                // Ionic wrong port
                pattern: /(Running dev server:)((?!\d+\.\d+\.\d+\.\d+:8080).)*$/,
                message: messages.generic.wrongPortIP
            },
            {
                // Ionic correct port
                pattern: /(Running dev server:).*?(?=\d+\.\d+\.\d+\.\d+:8080)/,
                message: messages.generic.appRunning
            },
            {
                // WEBrick correct port
                pattern: /INFO\ \ WEBrick::HTTPServer#start: pid=\d+ port=8080/,
                message: messages.generic.appRunning
            },
            {
                // WEBrick wrong port
                pattern: /INFO\ \ WEBrick::HTTPServer#start: pid=\d+ port=(?!8080)/,
                message: messages.rails.wrongPortIP
            },
            {
                // Rails or Sinatra
                pattern: /WARN {1,2}TCPServer Error: (?:Address already in use|Permission denied) - bind\(2\)/,
                message: messages.rails.wrongPortIP
            },
            {
                // Node app
                pattern: /Error: listen (?:EACCES|EADDRNOTAVAIL)/,
                message: messages.generic.addressInUse
            },
            {
                // Node address in use
                pattern: /Error: listen EADDRINUSE/,
                message: messages.generic.addressInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:$PORT -t)",
                    label: "Kill processes"
                }
            },
            {
                // Django correct port
                pattern: /Starting development server at.*?(?=0\.0\.0\.0:8080)/i,
                message: messages.generic.appRunning
            },
            {
                // Django app wrong port
                pattern: /Error: You don't have permission to access that port./,
                message: messages.django.wrongPortIP
            },
            {
                pattern: /Failed to open socket on port 15454/,
                message: messages.generic.debuggerPortInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:15454 -t)",
                    label: "Kill processes"
                }
            },
            {
                pattern: /Failed to listen on 0.0.0.0:8080 \(reason: Address already in use\)/,
                message: messages.generic.debuggerPortInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:8080 -t)",
                    label: "Kill processes"
                }
            },
            {
                // Django app wrong port
                pattern: /Error: That port is already in use/,
                message: messages.generic.addressInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:$PORT -t)",
                    label: "Kill processes"
                }
            },
            {
                // Jekyll
                pattern: /Server address: http:\/\/(?=0\.0\.0\.0:8080)/,
                message: messages.generic.appRunning
            },
            {
                // Jekyll
                pattern: /Server address: http:\/\/(?!0\.0\.0\.0:8080)/,
                message: messages.generic.wrongPortIP
            },
            {
                // Gradle
                pattern: /Tomcat started on port\(s\): 8080/,
                message: messages.generic.appRunning
            }
        ];
        return {
            matchers: matchers,
            messages: messages
        };
    };
});
<commit_msg>Add spring<commit_after>/**
 * Terminal Module for the Cloud9
 *
 * @copyright 2013, Ajax.org B.V.
 */

define(function(require, exports, module) {
    "use strict";

    module.exports = function(c9) {

        var prefix = 'Cloud9 Help ';
        var wrongPortIP = "You may be using the wrong PORT & IP for your server application.";

        var messages = {
            generic: {
                wrongPortIP: prefix + wrongPortIP + "Try passing $PORT and $IP to properly launch your application. You can find more information in our docs.",
                appRunning: prefix + "Your code is running at https://" + c9.hostname + "",
                bindToInternalIP: prefix + wrongPortIP + "Only binding to the internal IP configured in $IP is supported.",
                noLiveReload: prefix + "We currently only support listening on one port. Opening up a second port for live-reloading is currently not possible.",
                addressInUse: prefix + "There are issues starting your app. Please make sure you are using the correct $IP and $PORT, run as the correct user or kill any processes which might be conflicting. You can find more information in our docs.",
                debuggerPortInUse: prefix + "A process is listening on the port specified. Please try and kill that process and restart the app. You can find more information in our docs"
            },
            rails: {
                wrongPortIP: prefix + wrongPortIP + "For rails, use: 'rails s -p $PORT -b $IP'. For Sinatra, use: ruby app.rb -p $PORT -o $IP'."
            },
            node: {
                wrongPortIP: prefix + wrongPortIP + " Use 'process.env.PORT' as the port and 'process.env.IP' as the host in your scripts or refer to the documentation for more information."
            },
            django: {
                wrongPortIP: prefix + wrongPortIP + " Use './manage.py runserver $IP:$PORT' to run your Django application."
            }
        };

        var matchers = [
            {
                // Generic
                pattern: /^(?!reload )server(?: is | )(?:listening|running) (?:at|on)((?!0\.0\.0\.0:8080).)*$/i,
                message: messages.generic.wrongPortIP
            },
            {
                // Generic correct port
                pattern: /^(?!reload )server(?: is | )(?:listening|running) (?:at|on).*?(?=0\.0\.0\.0:8080)/i,
                message: messages.generic.appRunning
            },
            {
                // grunt-serve correct port
                pattern: /Server is running on port (?!8080)/i,
                message: messages.generic.wrongPortIP
            },
            {
                // grunt-serve correct port
                pattern: /Server is running on port 8080/i,
                message: messages.generic.appRunning
            },
            {
                // grunt-reload
                pattern: /^Proxying http:\/\/(?!0\.0\.0\.0:8080)/i,
                message: messages.generic.wrongPortIP
            },
            {
                // grunt-reload
                pattern: /^Proxying http:\/\/(?=0\.0\.0\.0:8080)/i,
                message: messages.generic.appRunning
            },
            {
                // grunt no support for live-reload
                pattern: /^reload server running at http:\/\/.*?:\d{4,5}/i,
                message: messages.generic.noLiveReload
            },
            {
                // Meteor wrong port
                pattern: /(App running at:)((?!0\.0\.0\.0:8080).)*$/,
                message: messages.generic.wrongPortIP
            },
            {
                // Meteor correct port
                pattern: /(App running at:).*?(?=0\.0\.0\.0:8080)/,
                message: messages.generic.appRunning
            },
            {
                // Ionic wrong port
                pattern: /(Running dev server:)((?!\d+\.\d+\.\d+\.\d+:8080).)*$/,
                message: messages.generic.wrongPortIP
            },
            {
                // Ionic correct port
                pattern: /(Running dev server:).*?(?=\d+\.\d+\.\d+\.\d+:8080)/,
                message: messages.generic.appRunning
            },
            {
                // WEBrick correct port
                pattern: /INFO\ \ WEBrick::HTTPServer#start: pid=\d+ port=8080/,
                message: messages.generic.appRunning
            },
            {
                // WEBrick wrong port
                pattern: /INFO\ \ WEBrick::HTTPServer#start: pid=\d+ port=(?!8080)/,
                message: messages.rails.wrongPortIP
            },
            {
                // Rails or Sinatra
                pattern: /WARN {1,2}TCPServer Error: (?:Address already in use|Permission denied) - bind\(2\)/,
                message: messages.rails.wrongPortIP
            },
            {
                // Node app
                pattern: /Error: listen (?:EACCES|EADDRNOTAVAIL)/,
                message: messages.generic.addressInUse
            },
            {
                // Node address in use
                pattern: /Error: listen EADDRINUSE/,
                message: messages.generic.addressInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:$PORT -t)",
                    label: "Kill processes"
                }
            },
            {
                // Django correct port
                pattern: /Starting development server at.*?(?=0\.0\.0\.0:8080)/i,
                message: messages.generic.appRunning
            },
            {
                // Django app wrong port
                pattern: /Error: You don't have permission to access that port./,
                message: messages.django.wrongPortIP
            },
            {
                pattern: /Failed to open socket on port 15454/,
                message: messages.generic.debuggerPortInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:15454 -t)",
                    label: "Kill processes"
                }
            },
            {
                pattern: /Failed to listen on 0.0.0.0:8080 \(reason: Address already in use\)/,
                message: messages.generic.debuggerPortInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:8080 -t)",
                    label: "Kill processes"
                }
            },
            {
                // Django app wrong port
                pattern: /Error: That port is already in use/,
                message: messages.generic.addressInUse,
                action: {
                    cmd: "kill -9 $(lsof -i:$PORT -t)",
                    label: "Kill processes"
                }
            },
            {
                // Jekyll
                pattern: /Server address: http:\/\/(?=0\.0\.0\.0:8080)/,
                message: messages.generic.appRunning
            },
            {
                // Jekyll
                pattern: /Server address: http:\/\/(?!0\.0\.0\.0:8080)/,
                message: messages.generic.wrongPortIP
            },
            {
                // Gradle
                pattern: /Tomcat started on port\(s\): 8080/,
                message: messages.generic.appRunning
            },
            {
                // Spring
                pattern: /::\s+Spring Boot\s+::\s+\(v\d.\d.\d.RELEASE\)/,
                message: messages.generic.appRunning
            }
        ];
        return {
            matchers: matchers,
            messages: messages
        };
    };
});
<|endoftext|>


<commit_before>import React, { PureComponent } from "react";
import Link from "gatsby-link";
import Header from "../components/Header";
import { cssMediaQueries as cm } from "../utils/mediaqueries";
import { jsMediaQueries as jm } from "../../gatsby-browser";
import Burger from "../components/NavMobile";
import { Div } from "glamorous";
import Helmet from "react-helmet";
import TitleCard from "../components/TitleCard";
import g from "glamorous";
import { rhythm as r } from "../utils/typography";

const props = {
  title: `Mother Teresa Welfare Trust`,
  subtitle: `Doing Small Things With Great ❤`,
};

const PageContainerDIV = g.div({}, ({ isHome }) => {
  if (!isHome) {
    return {
      width: "80%",
      margin: "0 auto",
      ".page-container div p": {
        textAlign: "left",
      },
      fontSize: `${r(0.8)}`,
      position: "absolute",
      top: `${r(14)}`,
      left: 0,
      right: 0,
      bottom: 0,
      textAlign: "center",
      [cm.smallnt]: {
        // position: "relative",
        top: `${r(12)}`,
        width: "60%",
        // textAlign: "center",
      },
    };
  }
});
import { rhythm, scale } from "../utils/typography";

class Wrapper extends PureComponent {
  render() {
    let data = this.props.data.allMarkdownRemark.edges;
    let ethsSProps = {
      background: `rgba(208, 205, 215, 0.6)`,
      display: "flex",
      flexDirection: "column",
      justifyContent: "center",
    };
    let conditionalHelmet, helmet;

    helmet = (
      <Helmet>

      </Helmet>
    );

    if (this.props.location.pathname === "/") {
      ethsSProps.height = `100vh`;
      ethsSProps.overflowY = `hidden`;
      conditionalHelmet = (
        <Helmet>

        </Helmet>
      );
    }

    if (jm.landscape && !jm.smallnt) {
      ethsSProps.overflowY = `auto`;
    }

    return (

        {jm.smallnt ?  : <Burger data={data} />}

          {conditionalHelmet}
          {helmet}
          <PageContainerDIV
            className="page-container"
            id="page-container"
            isHome={this.props.location.pathname === "/"}
          >
            {this.props.children({
              ...this.props,
            })}
          </PageContainerDIV>
          <TitleCard {...props} />

    );
  }
}

export default Wrapper;

export const layoutQuery = graphql`
  query LayoutQuery {
    allMarkdownRemark(sort: { fields: [id], order: ASC }) {
      edges {
        node {
          ...Header__Details
        }
      }
    }
  }
`;
<commit_msg>used regex (yay) to extract text between route slug<commit_after>import React, { PureComponent } from "react";
import Link from "gatsby-link";
import Header from "../components/Header";
import { cssMediaQueries as cm } from "../utils/mediaqueries";
import { jsMediaQueries as jm } from "../../gatsby-browser";
import Burger from "../components/NavMobile";
import { Div } from "glamorous";
import Helmet from "react-helmet";
import TitleCard from "../components/TitleCard";
import g from "glamorous";
import { rhythm as r } from "../utils/typography";

const props = {
  title: `Mother Teresa Welfare Trust`,
  subtitle: `Doing Small Things With Great ❤`,
};

const PageContainerDIV = g.div({}, ({ isHome }) => {
  if (!isHome) {
    return {
      width: "80%",
      margin: "0 auto",
      ".page-container div p": {
        textAlign: "left",
      },
      fontSize: `${r(0.8)}`,
      position: "absolute",
      top: `${r(14)}`,
      left: 0,
      right: 0,
      bottom: 0,
      textAlign: "center",
      [cm.smallnt]: {
        // position: "relative",
        top: `${r(12)}`,
        width: "60%",
        // textAlign: "center",
      },
    };
  }
});
import { rhythm, scale } from "../utils/typography";

class Wrapper extends PureComponent {
  render() {
    let data = this.props.data.allMarkdownRemark.edges;
    let ethsSProps = {
      background: `rgba(208, 205, 215, 0.6)`,
      display: "flex",
      flexDirection: "column",
      justifyContent: "center",
    };
    let conditionalHelmet, helmet;

    helmet = (
      <Helmet>

      </Helmet>
    );

    if (this.props.location.pathname === "/") {
      ethsSProps.height = `100vh`;
      ethsSProps.overflowY = `hidden`;
      conditionalHelmet = (
        <Helmet>

        </Helmet>
      );
    }

    if (jm.landscape && !jm.smallnt) {
      ethsSProps.overflowY = `auto`;
    }

    return (

        <Helmet
          defaultTitle="MTWT"
          titleTemplate="MTWT | %s"
          title={`${this.props.location.pathname.split(
            /\/([a-zA-Z]{0,})\//,
          )[1] || "home"}`}
        />
        {jm.smallnt ?  : <Burger data={data} />}

          {conditionalHelmet}
          {helmet}
          <PageContainerDIV
            className="page-container"
            id="page-container"
            isHome={this.props.location.pathname === "/"}
          >
            {this.props.children({
              ...this.props,
            })}
          </PageContainerDIV>
          <TitleCard {...props} />

    );
  }
}

export default Wrapper;

export const layoutQuery = graphql`
  query LayoutQuery {
    allMarkdownRemark(sort: { fields: [id], order: ASC }) {
      edges {
        node {
          ...Header__Details
        }
      }
    }
  }
`;
<|endoftext|>


<commit_before>// Import Vue
import Vue from 'vue'

// Import F7
import Framework7 from 'framework7'

// Import F7 Vue Plugin
import { Framework7Vue, f7Navbar, f7Page } from 'framework7-vue'

// Init F7 Vue Plugin
Vue.use(Framework7Vue, Framework7)

Vue.component('f7-navbar', f7Navbar)
Vue.component('f7-page', f7Page)

// Routes
import routes from '~/f7/routes'

// Disable android contextmenu
window.oncontextmenu = function(event) {
    event.preventDefault();
    event.stopPropagation();
    return false;
};

// Register plugin
export default async function framework7({ app }, inject) {
    app.framework7 = {
        // Router Options
        view: {
            pushState: true,
            pushStateSeparator: '',
            pushStateRoot: document.origin
        },
        // Main routes
        routes,
    }
}

<commit_msg>fix: origin hash fallback and ios support<commit_after>// Import Vue
import Vue from 'vue'

// Import F7
import Framework7 from 'framework7'

// Import F7 Vue Plugin
import { Framework7Vue, f7Navbar, f7Page } from 'framework7-vue'

// Init F7 Vue Plugin
Vue.use(Framework7Vue, Framework7)

Vue.component('f7-navbar', f7Navbar)
Vue.component('f7-page', f7Page)

// Routes
import routes from '~/f7/routes'

// Disable android contextmenu
window.oncontextmenu = function (event) {
    event.preventDefault();
    event.stopPropagation();
    return false;
};

// Register plugin
export default async function framework7({ app }, inject) {
    // Check to fallback hash mode if origin is not available
    const origin = window.location.origin

    app.framework7 = {
        // Router Options
        view: {
            pushState: true,
            pushStateSeparator: origin ? '' : '#',
            pushStateRoot: origin
        },
        // Main routes
        routes,
    }
}

<|endoftext|>


<commit_before>import resemble from 'resemblejs';

let REMOVE_DOM_ELEMENTS = true;

function compareImages(
  image,
  baselines,
  testName,
  tapeContext,
  threshold = 0.5,
  nextCallback = null
) {
  const nbBaselines = baselines.length;
  let minDelta = 100;
  let minDiff = '';
  let isSameDimensions = false;

  function done() {
    tapeContext.ok(minDelta < threshold, `Matching image - delta ${minDelta}%`);
    tapeContext.ok(isSameDimensions, 'Image match resolution');

    if (minDelta >= threshold) {
      tapeContext.fail(
        ` vs  === `
      );
    }

    if (nextCallback) {
      nextCallback();
    } else {
      tapeContext.end();
    }
  }

  resemble.outputSettings({
    transparency: 0.5,
  });
  baselines.forEach((baseline, idx) => {
    resemble(baseline)
      .compareTo(image)
      .onComplete((data) => {
        if (minDelta >= data.misMatchPercentage) {
          minDelta = data.misMatchPercentage;
          minDiff = data.getImageDataUrl();
        }
        isSameDimensions = isSameDimensions || data.isSameDimensions;

        if (idx + 1 === nbBaselines) {
          done();
        }
      });
  });
}

function createGarbageCollector(testContext) {
  const resources = [];
  const domElements = [];

  function registerResource(vtkObj, priority = 0) {
    resources.push({ vtkObj, priority });
    return vtkObj;
  }

  function registerDOMElement(el) {
    domElements.push(el);
    return el;
  }

  function releaseResources() {
    // DOM Element handling
    if (REMOVE_DOM_ELEMENTS) {
      domElements.forEach((el) => {
        if (el.parentNode) {
          el.parentNode.removeChild(el);
        }
      });
    }
    while (domElements.length) {
      domElements.pop();
    }

    // vtkObject handling
    resources.sort((a, b) => b.priority - a.priority);
    resources.forEach(({ vtkObj }) => {
      if (vtkObj) {
        vtkObj.delete();
      }
    });
    while (resources.length) {
      resources.pop();
    }

    // Test end handling
    if (testContext) {
      testContext.end();
    }
  }

  return {
    registerResource,
    registerDOMElement,
    releaseResources,
  };
}

function keepDOM() {
  REMOVE_DOM_ELEMENTS = false;
}

function removeDOM() {
  REMOVE_DOM_ELEMENTS = true;
}

export default {
  compareImages,
  createGarbageCollector,
  keepDOM,
  removeDOM,
};
<commit_msg>test(testUtils): ignore antialiasing for tests Co-Authored-by: Ken Martin <ken.martin@kitware.com><commit_after>import resemble from 'resemblejs';

let REMOVE_DOM_ELEMENTS = true;

function compareImages(
  image,
  baselines,
  testName,
  tapeContext,
  threshold = 0.5,
  nextCallback = null
) {
  const nbBaselines = baselines.length;
  let minDelta = 100;
  let minDiff = '';
  let isSameDimensions = false;

  function done() {
    tapeContext.ok(minDelta < threshold, `Matching image - delta ${minDelta}%`);
    tapeContext.ok(isSameDimensions, 'Image match resolution');

    if (minDelta >= threshold) {
      tapeContext.fail(
        ` vs  === `
      );
    }

    if (nextCallback) {
      nextCallback();
    } else {
      tapeContext.end();
    }
  }

  resemble.outputSettings({
    transparency: 0.5,
  });
  baselines.forEach((baseline, idx) => {
    resemble(baseline)
      .compareTo(image)
      .ignoreAntialiasing()
      .onComplete((data) => {
        if (minDelta >= data.misMatchPercentage) {
          minDelta = data.misMatchPercentage;
          minDiff = data.getImageDataUrl();
        }
        isSameDimensions = isSameDimensions || data.isSameDimensions;

        if (idx + 1 === nbBaselines) {
          done();
        }
      });
  });
}

function createGarbageCollector(testContext) {
  const resources = [];
  const domElements = [];

  function registerResource(vtkObj, priority = 0) {
    resources.push({ vtkObj, priority });
    return vtkObj;
  }

  function registerDOMElement(el) {
    domElements.push(el);
    return el;
  }

  function releaseResources() {
    // DOM Element handling
    if (REMOVE_DOM_ELEMENTS) {
      domElements.forEach((el) => {
        if (el.parentNode) {
          el.parentNode.removeChild(el);
        }
      });
    }
    while (domElements.length) {
      domElements.pop();
    }

    // vtkObject handling
    resources.sort((a, b) => b.priority - a.priority);
    resources.forEach(({ vtkObj }) => {
      if (vtkObj) {
        vtkObj.delete();
      }
    });
    while (resources.length) {
      resources.pop();
    }

    // Test end handling
    if (testContext) {
      testContext.end();
    }
  }

  return {
    registerResource,
    registerDOMElement,
    releaseResources,
  };
}

function keepDOM() {
  REMOVE_DOM_ELEMENTS = false;
}

function removeDOM() {
  REMOVE_DOM_ELEMENTS = true;
}

export default {
  compareImages,
  createGarbageCollector,
  keepDOM,
  removeDOM,
};
<|endoftext|>


<commit_before>import Utils from './utils/utils';
import Scene from './scene';
import Geo from './geo';

// Exports must appear outside a function, but will only be defined in main thread (below)
export var LeafletLayer;
export function leafletLayer(options) {
    return extendLeaflet(options);
}

function extendLeaflet(options) {

    // If LeafletLayer is already defined when this is called just return that immediately
    // e.g. if you call leafletLayer multiple times (which is valid)
    if (typeof LeafletLayer !== 'undefined') {
        return new LeafletLayer(options);
    }

    // Leaflet layer functionality is only defined in main thread
    if (Utils.isMainThread) {

        let L = options.leaflet || window.L;

        // Determine if we are extending the leaflet 0.7.x TileLayer class, or the newer
        // leaflet 1.x GridLayer class.
        let layerBaseClass = L.GridLayer ? L.GridLayer : L.TileLayer;
        let leafletVersion = layerBaseClass === L.GridLayer ? '1.x' : '0.7.x';
        let layerClassConfig = {};

        // If extending leaflet 0.7.x TileLayer, make add/remove tile no ops
        if (layerBaseClass === L.TileLayer) {
            layerClassConfig._addTile = function(){};
            layerClassConfig._removeTile = function(){};
        }

        // Define custom layer methods
        Object.assign(layerClassConfig, {

            initialize: function (options) {
                // Defaults
                options.showDebug = (!options.showDebug ? false : true);
                options.wheelDebounceTime = options.wheelDebounceTime || 40;

                L.setOptions(this, options);
                this.createScene();
                this.hooks = {};
                this._updating_tangram = false;

                // Force leaflet zoom animations off
                this._zoomAnimated = false;

                this.debounceViewReset = Utils.debounce(() => {
                    this._map.fire('zoomend');
                    this._map.fire('moveend');
                }, this.options.wheelDebounceTime);
            },

            createScene: function () {
                this.scene = Scene.create(
                    this.options.scene,
                    {
                        numWorkers: this.options.numWorkers,
                        preUpdate: this.options.preUpdate,
                        postUpdate: this.options.postUpdate,
                        continuousZoom: (LeafletLayer.leafletVersion === '1.x'),
                        highDensityDisplay: this.options.highDensityDisplay,
                        logLevel: this.options.logLevel,
                        // advanced option, app will have to manually called scene.update() per frame
                        disableRenderLoop: this.options.disableRenderLoop,
                        // advanced option, will require library to be served as same host as page
                        allowCrossDomainWorkers: this.options.allowCrossDomainWorkers
                    });
            },

            // Finish initializing scene and setup events when layer is added to map
            onAdd: function (map) {
                if (!this.scene) {
                    this.createScene();
                }

                layerBaseClass.prototype.onAdd.apply(this, arguments);

                this.hooks.resize = () => {
                    this._updating_tangram = true;
                    this.updateSize();
                    this._updating_tangram = false;
                };
                map.on('resize', this.hooks.resize);

                this.hooks.move = () => {
                    if (this._updating_tangram) {
                        return;
                    }

                    this._updating_tangram = true;
                    var view = map.getCenter();
                    view.zoom = Math.min(map.getZoom(), map.getMaxZoom() || Geo.max_zoom);

                    this.scene.setView(view);
                    this.scene.immediateRedraw();
                    this.reverseTransform();
                    this._updating_tangram = false;
                };
                map.on('move', this.hooks.move);

                this.hooks.zoomstart = () => {
                    if (this._updating_tangram) {
                        return;
                    }

                    this._updating_tangram = true;
                    this.scene.startZoom();
                    this._updating_tangram = false;
                };
                map.on('zoomstart', this.hooks.zoomstart);

                this.hooks.dragstart = () => {
                    this.scene.panning = true;
                };
                map.on('dragstart', this.hooks.dragstart);

                this.hooks.dragend = () => {
                    this.scene.panning = false;
                };
                map.on('dragend', this.hooks.dragend);

                // Force leaflet zoom animations off
                map._zoomAnimated = false;

                // Modify default leaflet scroll wheel behavior
                this.modifyScrollWheelBehavior(map);

                // Add GL canvas to layer container
                this.scene.container = this.getContainer();

                // Initial view
                this.updateView();

                // Subscribe to tangram events
                this.scene.subscribe({
                    move: this.onTangramViewUpdate.bind(this)
                });

                // Use leaflet's existing event system as the callback mechanism
                this.scene.load().then(() => {
                    this._updating_tangram = true;

                    this.updateSize();
                    this.updateView();
                    this.reverseTransform();

                    this._updating_tangram = false;

                    this.fire('init');
                }).catch(error => {
                    this.fire('error', error);
                });
            },

            onRemove: function (map) {
                layerBaseClass.prototype.onRemove.apply(this, arguments);

                map.off('resize', this.hooks.resize);
                map.off('move', this.hooks.move);
                map.off('zoomstart', this.hooks.zoomstart);
                map.off('dragstart', this.hooks.dragstart);
                map.off('dragend', this.hooks.dragend);
                this.hooks = {};

                if (this.scene) {
                    this.scene.destroy();
                    this.scene = null;
                }
            },

            createTile: function (coords) {
                var key = coords.x + '/' + coords.y + '/' + coords.z;
                var div = document.createElement('div');
                div.setAttribute('data-tile-key', key);
                div.style.width = '256px';
                div.style.height = '256px';

                if (this.options.showDebug) {
                    var debug_overlay = document.createElement('div');
                    debug_overlay.textContent = key;
                    debug_overlay.style.position = 'absolute';
                    debug_overlay.style.left = 0;
                    debug_overlay.style.top = 0;
                    debug_overlay.style.color = 'white';
                    debug_overlay.style.fontSize = '16px';
                    debug_overlay.style.textOutline = '1px #000000';
                    debug_overlay.style.padding = '8px';

                    div.appendChild(debug_overlay);
                    div.style.borderStyle = 'solid';
                    div.style.borderColor = 'white';
                    div.style.borderWidth = '1px';
                }

                return div;
            },

            // Modify leaflet's default scroll wheel behavior to have a much more sensitve/continuous zoom
            // Note: this should be deprecated once leaflet continuous zoom is more widely used and the
            // default behavior is presumably improved
            modifyScrollWheelBehavior: function (map) {
                if (this.scene.continuous_zoom && map.scrollWheelZoom && this.options.modifyScrollWheel !== false) {
                    let layer = this;
                    let enabled = map.scrollWheelZoom.enabled();
                    if (enabled) {
                        map.scrollWheelZoom.disable(); // disable before modifying
                    }

                    // modify prototype and current instance, so add/remove hooks work on existing references
                    L.Map.ScrollWheelZoom._onWheelScroll = map.scrollWheelZoom._onWheelScroll = function(e) {
                        // modify to skip debounce, as it seems to cause animation-sync issues in Chrome
                        // with Tangram continuous rendering
                        this._delta += L.DomEvent.getWheelDelta(e);
                        this._lastMousePos = this._map.mouseEventToContainerPoint(e);
                        this._performZoom();
                        L.DomEvent.stop(e);
                    };

                    L.Map.ScrollWheelZoom._performZoom = map.scrollWheelZoom._performZoom = function () {
                        var map = this._map,
                            delta = this._delta,
                            zoom = map.getZoom();

                        map.stop(); // stop panning and fly animations if any

                        // NOTE: this is the only real modification to default leaflet behavior
                        delta /= 40;

                        delta = Math.max(Math.min(delta, 4), -4);
                        delta = map._limitZoom(zoom + delta) - zoom;

                        this._delta = 0;
                        this._startTime = null;

                        if (!delta) { return; }

                        if (map.options.scrollWheelZoom === 'center') {
                            map._move(map.getCenter(), zoom + delta);
                        } else {
                            // Re-centering code from Leaflet's map.setZoomAround() function
                            var latlng = this._lastMousePos,
                                newZoom = zoom + delta,
                                scale = map.getZoomScale(newZoom),
                                viewHalf = map.getSize().divideBy(2),
                                containerPoint = latlng instanceof L.Point ? latlng : map.latLngToContainerPoint(latlng),

                                centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
                                newCenter = map.containerPointToLatLng(viewHalf.add(centerOffset));

                            map._move(newCenter, newZoom);
                        }

                        layer.debounceViewReset();
                    };

                    if (enabled) {
                        map.scrollWheelZoom.enable(); // re-enable after modifying
                    }
                }
            },

            updateView: function () {
                var view = this._map.getCenter();
                view.zoom = Math.min(this._map.getZoom(), this._map.getMaxZoom() || Geo.max_zoom);
                this.scene.setView(view);
            },

            updateSize: function () {
                var size = this._map.getSize();
                this.scene.resizeMap(size.x, size.y);
            },

            onTangramViewUpdate: function () {
                if (!this._map || this._updating_tangram) {
                    return;
                }
                this._updating_tangram = true;
                this._map.setView([this.scene.center.lat, this.scene.center.lng], this.scene.zoom, { animate: false });
                this.reverseTransform();
                this._updating_tangram = false;
            },

            render: function () {
                if (!this.scene) {
                    return;
                }
                this.scene.update();
            },

            // Reverse the CSS positioning Leaflet applies to the layer, since Tangram's WebGL canvas
            // is expected to be 'absolutely' positioned.
            reverseTransform: function () {
                if (!this._map || !this.scene || !this.scene.container) {
                    return;
                }

                var top_left = this._map.containerPointToLayerPoint([0, 0]);
                L.DomUtil.setPosition(this.scene.container, top_left);
            }

        });

        // Create the layer class
        LeafletLayer = layerBaseClass.extend(layerClassConfig);

        // Polyfill some 1.0 methods
        if (typeof LeafletLayer.remove !== 'function') {
            LeafletLayer.prototype.remove = function() {
                if (this._map) {
                    this._map.removeLayer(this);
                }
                this.fire('remove');
            };
        }

        LeafletLayer.layerBaseClass = layerBaseClass;
        LeafletLayer.leafletVersion = leafletVersion;

        return new LeafletLayer(options);
    }
}
<commit_msg>revert to old zoom functions<commit_after>import Utils from './utils/utils';
import Scene from './scene';
import Geo from './geo';

// Exports must appear outside a function, but will only be defined in main thread (below)
export var LeafletLayer;
export function leafletLayer(options) {
    return extendLeaflet(options);
}

function extendLeaflet(options) {

    // If LeafletLayer is already defined when this is called just return that immediately
    // e.g. if you call leafletLayer multiple times (which is valid)
    if (typeof LeafletLayer !== 'undefined') {
        return new LeafletLayer(options);
    }

    // Leaflet layer functionality is only defined in main thread
    if (Utils.isMainThread) {

        let L = options.leaflet || window.L;

        // Determine if we are extending the leaflet 0.7.x TileLayer class, or the newer
        // leaflet 1.x GridLayer class.
        let layerBaseClass = L.GridLayer ? L.GridLayer : L.TileLayer;
        let leafletVersion = layerBaseClass === L.GridLayer ? '1.x' : '0.7.x';
        let layerClassConfig = {};

        // If extending leaflet 0.7.x TileLayer, make add/remove tile no ops
        if (layerBaseClass === L.TileLayer) {
            layerClassConfig._addTile = function(){};
            layerClassConfig._removeTile = function(){};
        }

        // Define custom layer methods
        Object.assign(layerClassConfig, {

            initialize: function (options) {
                // Defaults
                options.showDebug = (!options.showDebug ? false : true);
                options.wheelDebounceTime = options.wheelDebounceTime || 40;

                L.setOptions(this, options);
                this.createScene();
                this.hooks = {};
                this._updating_tangram = false;

                // Force leaflet zoom animations off
                this._zoomAnimated = false;

                this.debounceViewReset = Utils.debounce(() => {
                    this._map.fire('zoomend');
                    this._map.fire('moveend');
                }, this.options.wheelDebounceTime);
            },

            createScene: function () {
                this.scene = Scene.create(
                    this.options.scene,
                    {
                        numWorkers: this.options.numWorkers,
                        preUpdate: this.options.preUpdate,
                        postUpdate: this.options.postUpdate,
                        continuousZoom: (LeafletLayer.leafletVersion === '1.x'),
                        highDensityDisplay: this.options.highDensityDisplay,
                        logLevel: this.options.logLevel,
                        // advanced option, app will have to manually called scene.update() per frame
                        disableRenderLoop: this.options.disableRenderLoop,
                        // advanced option, will require library to be served as same host as page
                        allowCrossDomainWorkers: this.options.allowCrossDomainWorkers
                    });
            },

            // Finish initializing scene and setup events when layer is added to map
            onAdd: function (map) {
                if (!this.scene) {
                    this.createScene();
                }

                layerBaseClass.prototype.onAdd.apply(this, arguments);

                this.hooks.resize = () => {
                    this._updating_tangram = true;
                    this.updateSize();
                    this._updating_tangram = false;
                };
                map.on('resize', this.hooks.resize);

                this.hooks.move = () => {
                    if (this._updating_tangram) {
                        return;
                    }

                    this._updating_tangram = true;
                    var view = map.getCenter();
                    view.zoom = Math.min(map.getZoom(), map.getMaxZoom() || Geo.max_zoom);

                    this.scene.setView(view);
                    this.scene.immediateRedraw();
                    this.reverseTransform();
                    this._updating_tangram = false;
                };
                map.on('move', this.hooks.move);

                this.hooks.zoomstart = () => {
                    if (this._updating_tangram) {
                        return;
                    }

                    this._updating_tangram = true;
                    this.scene.startZoom();
                    this._updating_tangram = false;
                };
                map.on('zoomstart', this.hooks.zoomstart);

                this.hooks.dragstart = () => {
                    this.scene.panning = true;
                };
                map.on('dragstart', this.hooks.dragstart);

                this.hooks.dragend = () => {
                    this.scene.panning = false;
                };
                map.on('dragend', this.hooks.dragend);

                // Force leaflet zoom animations off
                map._zoomAnimated = false;

                // Modify default leaflet scroll wheel behavior
                this.modifyScrollWheelBehavior(map);

                // Add GL canvas to layer container
                this.scene.container = this.getContainer();

                // Initial view
                this.updateView();

                // Subscribe to tangram events
                this.scene.subscribe({
                    move: this.onTangramViewUpdate.bind(this)
                });

                // Use leaflet's existing event system as the callback mechanism
                this.scene.load().then(() => {
                    this._updating_tangram = true;

                    this.updateSize();
                    this.updateView();
                    this.reverseTransform();

                    this._updating_tangram = false;

                    this.fire('init');
                }).catch(error => {
                    this.fire('error', error);
                });
            },

            onRemove: function (map) {
                layerBaseClass.prototype.onRemove.apply(this, arguments);

                map.off('resize', this.hooks.resize);
                map.off('move', this.hooks.move);
                map.off('zoomstart', this.hooks.zoomstart);
                map.off('dragstart', this.hooks.dragstart);
                map.off('dragend', this.hooks.dragend);
                this.hooks = {};

                if (this.scene) {
                    this.scene.destroy();
                    this.scene = null;
                }
            },

            createTile: function (coords) {
                var key = coords.x + '/' + coords.y + '/' + coords.z;
                var div = document.createElement('div');
                div.setAttribute('data-tile-key', key);
                div.style.width = '256px';
                div.style.height = '256px';

                if (this.options.showDebug) {
                    var debug_overlay = document.createElement('div');
                    debug_overlay.textContent = key;
                    debug_overlay.style.position = 'absolute';
                    debug_overlay.style.left = 0;
                    debug_overlay.style.top = 0;
                    debug_overlay.style.color = 'white';
                    debug_overlay.style.fontSize = '16px';
                    debug_overlay.style.textOutline = '1px #000000';
                    debug_overlay.style.padding = '8px';

                    div.appendChild(debug_overlay);
                    div.style.borderStyle = 'solid';
                    div.style.borderColor = 'white';
                    div.style.borderWidth = '1px';
                }

                return div;
            },

            // Modify leaflet's default scroll wheel behavior to have a much more sensitve/continuous zoom
            // Note: this should be deprecated once leaflet continuous zoom is more widely used and the
            // default behavior is presumably improved
            modifyScrollWheelBehavior: function (map) {
                if (this.scene.continuous_zoom && map.scrollWheelZoom && this.options.modifyScrollWheel !== false) {
                    let layer = this;
                    let enabled = map.scrollWheelZoom.enabled();
                    if (enabled) {
                        map.scrollWheelZoom.disable(); // disable before modifying
                    }

                    // modify prototype and current instance, so add/remove hooks work on existing references
                    L.Map.ScrollWheelZoom._onWheelScroll = map.scrollWheelZoom._onWheelScroll = function(e) {
                        // modify to skip debounce, as it seems to cause animation-sync issues in Chrome
                        // with Tangram continuous rendering
                        this._delta += L.DomEvent.getWheelDelta(e);
                        this._lastMousePos = this._map.mouseEventToContainerPoint(e);
                        this._performZoom();
                        L.DomEvent.stop(e);
                    };

                    L.Map.ScrollWheelZoom._performZoom = map.scrollWheelZoom._performZoom = function () {
                        var map = this._map,
                            delta = this._delta,
                            zoom = map.getZoom();

                        map.stop(); // stop panning and fly animations if any

                        // NOTE: this is the only real modification to default leaflet behavior
                        delta /= 40;

                        delta = Math.max(Math.min(delta, 4), -4);
                        delta = map._limitZoom(zoom + delta) - zoom;

                        this._delta = 0;
                        this._startTime = null;

                        if (!delta) { return; }

                        if (map.options.scrollWheelZoom === 'center') {
                            map.setZoom(zoom + delta);
                        } else {
                            map.setZoomAround(this._lastMousePos, zoom + delta);
                        }

                        layer.debounceViewReset();
                    };

                    if (enabled) {
                        map.scrollWheelZoom.enable(); // re-enable after modifying
                    }
                }
            },

            updateView: function () {
                var view = this._map.getCenter();
                view.zoom = Math.min(this._map.getZoom(), this._map.getMaxZoom() || Geo.max_zoom);
                this.scene.setView(view);
            },

            updateSize: function () {
                var size = this._map.getSize();
                this.scene.resizeMap(size.x, size.y);
            },

            onTangramViewUpdate: function () {
                if (!this._map || this._updating_tangram) {
                    return;
                }
                this._updating_tangram = true;
                this._map.setView([this.scene.center.lat, this.scene.center.lng], this.scene.zoom, { animate: false });
                this.reverseTransform();
                this._updating_tangram = false;
            },

            render: function () {
                if (!this.scene) {
                    return;
                }
                this.scene.update();
            },

            // Reverse the CSS positioning Leaflet applies to the layer, since Tangram's WebGL canvas
            // is expected to be 'absolutely' positioned.
            reverseTransform: function () {
                if (!this._map || !this.scene || !this.scene.container) {
                    return;
                }

                var top_left = this._map.containerPointToLayerPoint([0, 0]);
                L.DomUtil.setPosition(this.scene.container, top_left);
            }

        });

        // Create the layer class
        LeafletLayer = layerBaseClass.extend(layerClassConfig);

        // Polyfill some 1.0 methods
        if (typeof LeafletLayer.remove !== 'function') {
            LeafletLayer.prototype.remove = function() {
                if (this._map) {
                    this._map.removeLayer(this);
                }
                this.fire('remove');
            };
        }

        LeafletLayer.layerBaseClass = layerBaseClass;
        LeafletLayer.leafletVersion = leafletVersion;

        return new LeafletLayer(options);
    }
}
<|endoftext|>


<commit_before>// Copyright 2019-2020, University of Colorado Boulder

/**
 * Supertype for containers that hold dynamic elements that are PhET-iO instrumented. This type handles common
 * features like creating the archetype for the PhET-iO api, and managing created/disposed data stream events.
 *
 * "Dynamic" is an overloaded term, so allow me to explain what it means in the context of this type. A "dynamic element"
 * is an instrumented PhET-iO element that is conditionally in the PhET-iO api. Most commonly this is because elements
 * can be created and destroyed during the runtime of the sim. Another "dynamic element" for the PhET-iO project is when
 * an element may or may not be created based on a query parameter. In this case, even if the object then exists for the
 * lifetime of the sim, we may still call this "dynamic" as it pertains to this type, and the PhET-iO api.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import Emitter from '../../axon/js/Emitter.js';
import validate from '../../axon/js/validate.js';
import merge from '../../phet-core/js/merge.js';
import DynamicTandem from './DynamicTandem.js';
import phetioAPIValidation from './phetioAPIValidation.js';
import PhetioObject from './PhetioObject.js';
import Tandem from './Tandem.js';
import tandemNamespace from './tandemNamespace.js';

// constants
const DEFAULT_CONTAINER_SUFFIX = 'Container';

class PhetioDynamicElementContainer extends PhetioObject {

  /**
   * @param {function} createElement - function that creates a dynamic element to be housed in this container. All of
   * this dynamic element container's elements will be created from this function, including the archetype.
   * @param {Array.<*>|function.<[],Array.<*>>} defaultArguments arguments passed to create during API harvest
   * @param {Object} [options] - describe the Group itself
   */
  constructor( createElement, defaultArguments, options ) {

    options = merge( {
      phetioState: false, // elements are included in state, but the container will exist in the downstream sim.
      tandem: Tandem.REQUIRED,

      // By default, a PhetioDynamicElementContainer's elements are included in state such that on every setState call,
      // the elements are cleared out by the phetioStateEngine so elements in the state can be added to the empty group.
      // This option is for opting out of that behavior. When false, this container will not have its elements cleared
      // when beginning to set PhET-iO state. NOTE: Only use when it's guaranteed that all of the elements are
      // created on startup, and never at any point later during the sim's lifetime. When this is set to false, there
      // is no need for elements to support dynamic state. This may seem like a confusing option because you may be
      // thinking, "shouldn't all instances of PhetioDynamicElementContainer contain dynamic elements that are added
      // and removed throughout the lifetime of the sim?!?!" Please note the documentation above about the term "dynamic"
      // and note that this is an atypical option.
      supportsDynamicState: true,

      // {string} The container's tandem name must have this suffix, and the base tandem name for elements in
      // the container will consist of the group's tandem name with this suffix stripped off.
      containerSuffix: DEFAULT_CONTAINER_SUFFIX
    }, options );

    assert && assert( typeof createElement === 'function', 'createElement should be a function' );
    assert && assert( Array.isArray( defaultArguments ) || typeof defaultArguments === 'function', 'defaultArguments should be an array or a function' );
    if ( Array.isArray( defaultArguments ) ) {

      // createElement expects a Tandem as the first arg
      assert && assert( createElement.length === defaultArguments.length + 1, 'mismatched number of arguments' );
    }

    assert && assert( !!options.phetioType, 'phetioType must be supplied' );
    assert && assert( Array.isArray( options.phetioType.parameterTypes ), 'phetioType must supply its parameter types' );
    assert && assert( options.phetioType.parameterTypes.length === 1,
      'PhetioDynamicElementContainer\'s phetioType must have exactly one parameter type' );
    assert && assert( !!options.phetioType.parameterTypes[ 0 ],
      'PhetioDynamicElementContainer\'s phetioType\'s parameterType must be truthy' );
    assert && assert( options.tandem.name.endsWith( options.containerSuffix ),
      'PhetioDynamicElementContainer tandems should end with options.containerSuffix' );

    // options that depend on other options
    options = merge( {

      // {string} - tandem name for elements in the container is the container's tandem name without containerSuffix
      phetioDynamicElementName: options.tandem.name.slice( 0, options.tandem.name.length - options.containerSuffix.length )
    }, options );

    super( options );

    // @public (read-only phet-io internal) {boolean}
    this.supportsDynamicState = options.supportsDynamicState;

    // @protected {string}
    this.phetioDynamicElementName = options.phetioDynamicElementName;

    // @protected
    this.createElement = createElement;
    this.defaultArguments = defaultArguments;

    // @public (read-only) {PhetioObject|null} Can be used as an argument to create other archetypes, but otherwise
    // access should not be needed. This will only be non-null when generating the phet-io api, see createArchetype().
    this.archetype = this.createArchetype();

    // @public (read-only)
    this.elementCreatedEmitter = new Emitter( { parameters: [ { valueType: PhetioObject } ] } );
    this.elementDisposedEmitter = new Emitter( { parameters: [ { valueType: PhetioObject } ] } );

    // Emit to the data stream on element creation/disposal
    this.elementCreatedEmitter.addListener( element => this.createdEventListener( element ) );
    this.elementDisposedEmitter.addListener( element => this.disposedEventListener( element ) );
  }

  /**
   * @public
   */
  dispose() {

    // If hitting this assertion because of nested dynamic element containers, please discuss with a phet-io team member.
    assert && assert( false, 'PhetioDynamicElementContainers are not intended for disposal' );
  }

  /**
   * Dispose a contained element
   * @param {PhetioObject} element
   * @protected - should not be called directly for PhetioGroup or PhetioCapsule, but can be made public if other subtypes need to.
   */
  disposeElement( element ) {
    element.dispose();
    this.elementDisposedEmitter.emit( element );
  }

  /**
   * Archetypes are created to generate the baseline file, or to validate against an existing baseline file.  They are
   * PhetioObjects and registered with the phetioEngine, but not send out via notifications for phetioObjectAddedListeners,
   * because they are intended for internal usage only.  Archetypes should not be created in production code.
   * @returns {null|PhetioObject}
   * @private
   */
  createArchetype() {

    // Once the sim has started, any archetypes being created are likely done so because they are nested PhetioGroups.
    // TODO: why do we get this information from phetioAPIValidation? Does recent API file work change this? https://github.com/phetsims/phet-io/issues/1648
    if ( phetioAPIValidation.simHasStarted ) {
      return null;
    }

    // When generating the baseline, output the schema for the archetype
    if ( ( Tandem.PHET_IO_ENABLED && phet.preloads.phetio.queryParameters.phetioPrintAPI ) ||
         ( Tandem.PHET_IO_ENABLED && phet.preloads.phetio.queryParameters.phetioCreateArchetypes ) ) {
      const defaultArgs = Array.isArray( this.defaultArguments ) ? this.defaultArguments : this.defaultArguments();

      // The create function takes a tandem plus the default args
      assert && assert( this.createElement.length === defaultArgs.length + 1, 'mismatched number of arguments' );

      const archetype = this.createElement( this.tandem.createTandem( DynamicTandem.DYNAMIC_ARCHETYPE_NAME ), ...defaultArgs );

      // Mark the archetype for inclusion in the baseline schema
      archetype.markDynamicElementArchetype();
      return archetype;
    }
    else {
      return null;
    }
  }

  /**
   * Create a dynamic PhetioObject element for this container
   * @param {string} componentName
   * @param {Array.<*>} argsForCreateFunction
   * @param {function(new:ObjectIO)} containerParameterType
   * @returns {PhetioObject}
   * @public
   */
  createDynamicElement( componentName, argsForCreateFunction, containerParameterType ) {
    assert && assert( Array.isArray( argsForCreateFunction ), 'should be array' );

    // create with default state and substructure, details will need to be set by setter methods.
    const createdObjectTandem = new DynamicTandem( this.tandem, componentName, this.tandem.getExtendedOptions() );
    const createdObject = this.createElement( createdObjectTandem, ...argsForCreateFunction );

    // Make sure the new group element matches the schema for elements.
    validate( createdObject, containerParameterType.validator );

    assert && assert( createdObject.phetioType === containerParameterType,
      'dynamic element container expected its created instance\'s phetioType to match its parameterType.' );

    assert && this.assertDynamicPhetioObject( createdObject );

    return createdObject;
  }

  /**
   * A dynamic element should be an instrumented PhetioObject with phetioDynamicElement: true
   * @param {PhetioObject} phetioObject - object to be validated
   * @private
   */
  assertDynamicPhetioObject( phetioObject ) {
    if ( Tandem.PHET_IO_ENABLED ) {
      assert && assert( phetioObject instanceof PhetioObject, 'instance should be a PhetioObject' );
      assert && assert( phetioObject.isPhetioInstrumented(), 'instance should be instrumented' );
      assert && assert( phetioObject.phetioDynamicElement, 'instance should be marked as phetioDynamicElement:true' );
    }
  }

  /**
   * Emit a created or disposed event.
   * @param {PhetioObject} dynamicElement
   * @param {string} eventName
   * @param {Object} [additionalData] additional data for the event
   * @private
   */
  emitDataStreamEvent( dynamicElement, eventName, additionalData ) {
    this.phetioStartEvent( eventName, {
      data: merge( {
        phetioID: dynamicElement.tandem.phetioID
      }, additionalData )
    } );
    this.phetioEndEvent();
  }

  /**
   * Emit events when dynamic elements are created.
   * @param {PhetioObject} dynamicElement
   * @public
   */
  createdEventListener( dynamicElement ) {
    const additionalData = dynamicElement.phetioState ? {
      state: this.phetioType.parameterTypes[ 0 ].toStateObject( dynamicElement )
    } : null;
    this.emitDataStreamEvent( dynamicElement, 'created', additionalData );
  }

  /**
   * Emit events when dynamic elements are disposed.
   * @param {PhetioObject} dynamicElement
   * @public
   */
  disposedEventListener( dynamicElement ) {
    this.emitDataStreamEvent( dynamicElement, 'disposed' );
  }
}

tandemNamespace.register( 'PhetioDynamicElementContainer', PhetioDynamicElementContainer );
export default PhetioDynamicElementContainer;<commit_msg>Reuse existing DynamicTandems, see https://github.com/phetsims/tandem/issues/176<commit_after>// Copyright 2019-2020, University of Colorado Boulder

/**
 * Supertype for containers that hold dynamic elements that are PhET-iO instrumented. This type handles common
 * features like creating the archetype for the PhET-iO api, and managing created/disposed data stream events.
 *
 * "Dynamic" is an overloaded term, so allow me to explain what it means in the context of this type. A "dynamic element"
 * is an instrumented PhET-iO element that is conditionally in the PhET-iO api. Most commonly this is because elements
 * can be created and destroyed during the runtime of the sim. Another "dynamic element" for the PhET-iO project is when
 * an element may or may not be created based on a query parameter. In this case, even if the object then exists for the
 * lifetime of the sim, we may still call this "dynamic" as it pertains to this type, and the PhET-iO api.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 */

import Emitter from '../../axon/js/Emitter.js';
import validate from '../../axon/js/validate.js';
import merge from '../../phet-core/js/merge.js';
import DynamicTandem from './DynamicTandem.js';
import phetioAPIValidation from './phetioAPIValidation.js';
import PhetioObject from './PhetioObject.js';
import Tandem from './Tandem.js';
import tandemNamespace from './tandemNamespace.js';

// constants
const DEFAULT_CONTAINER_SUFFIX = 'Container';

class PhetioDynamicElementContainer extends PhetioObject {

  /**
   * @param {function} createElement - function that creates a dynamic element to be housed in this container. All of
   * this dynamic element container's elements will be created from this function, including the archetype.
   * @param {Array.<*>|function.<[],Array.<*>>} defaultArguments arguments passed to create during API harvest
   * @param {Object} [options] - describe the Group itself
   */
  constructor( createElement, defaultArguments, options ) {

    options = merge( {
      phetioState: false, // elements are included in state, but the container will exist in the downstream sim.
      tandem: Tandem.REQUIRED,

      // By default, a PhetioDynamicElementContainer's elements are included in state such that on every setState call,
      // the elements are cleared out by the phetioStateEngine so elements in the state can be added to the empty group.
      // This option is for opting out of that behavior. When false, this container will not have its elements cleared
      // when beginning to set PhET-iO state. NOTE: Only use when it's guaranteed that all of the elements are
      // created on startup, and never at any point later during the sim's lifetime. When this is set to false, there
      // is no need for elements to support dynamic state. This may seem like a confusing option because you may be
      // thinking, "shouldn't all instances of PhetioDynamicElementContainer contain dynamic elements that are added
      // and removed throughout the lifetime of the sim?!?!" Please note the documentation above about the term "dynamic"
      // and note that this is an atypical option.
      supportsDynamicState: true,

      // {string} The container's tandem name must have this suffix, and the base tandem name for elements in
      // the container will consist of the group's tandem name with this suffix stripped off.
      containerSuffix: DEFAULT_CONTAINER_SUFFIX
    }, options );

    assert && assert( typeof createElement === 'function', 'createElement should be a function' );
    assert && assert( Array.isArray( defaultArguments ) || typeof defaultArguments === 'function', 'defaultArguments should be an array or a function' );
    if ( Array.isArray( defaultArguments ) ) {

      // createElement expects a Tandem as the first arg
      assert && assert( createElement.length === defaultArguments.length + 1, 'mismatched number of arguments' );
    }

    assert && assert( !!options.phetioType, 'phetioType must be supplied' );
    assert && assert( Array.isArray( options.phetioType.parameterTypes ), 'phetioType must supply its parameter types' );
    assert && assert( options.phetioType.parameterTypes.length === 1,
      'PhetioDynamicElementContainer\'s phetioType must have exactly one parameter type' );
    assert && assert( !!options.phetioType.parameterTypes[ 0 ],
      'PhetioDynamicElementContainer\'s phetioType\'s parameterType must be truthy' );
    assert && assert( options.tandem.name.endsWith( options.containerSuffix ),
      'PhetioDynamicElementContainer tandems should end with options.containerSuffix' );

    // options that depend on other options
    options = merge( {

      // {string} - tandem name for elements in the container is the container's tandem name without containerSuffix
      phetioDynamicElementName: options.tandem.name.slice( 0, options.tandem.name.length - options.containerSuffix.length )
    }, options );

    super( options );

    // @public (read-only phet-io internal) {boolean}
    this.supportsDynamicState = options.supportsDynamicState;

    // @protected {string}
    this.phetioDynamicElementName = options.phetioDynamicElementName;

    // @protected
    this.createElement = createElement;
    this.defaultArguments = defaultArguments;

    // @public (read-only) {PhetioObject|null} Can be used as an argument to create other archetypes, but otherwise
    // access should not be needed. This will only be non-null when generating the phet-io api, see createArchetype().
    this.archetype = this.createArchetype();

    // @public (read-only)
    this.elementCreatedEmitter = new Emitter( { parameters: [ { valueType: PhetioObject } ] } );
    this.elementDisposedEmitter = new Emitter( { parameters: [ { valueType: PhetioObject } ] } );

    // Emit to the data stream on element creation/disposal
    this.elementCreatedEmitter.addListener( element => this.createdEventListener( element ) );
    this.elementDisposedEmitter.addListener( element => this.disposedEventListener( element ) );
  }

  /**
   * @public
   */
  dispose() {

    // If hitting this assertion because of nested dynamic element containers, please discuss with a phet-io team member.
    assert && assert( false, 'PhetioDynamicElementContainers are not intended for disposal' );
  }

  /**
   * Dispose a contained element
   * @param {PhetioObject} element
   * @protected - should not be called directly for PhetioGroup or PhetioCapsule, but can be made public if other subtypes need to.
   */
  disposeElement( element ) {
    element.dispose();
    this.elementDisposedEmitter.emit( element );
  }

  /**
   * Archetypes are created to generate the baseline file, or to validate against an existing baseline file.  They are
   * PhetioObjects and registered with the phetioEngine, but not send out via notifications for phetioObjectAddedListeners,
   * because they are intended for internal usage only.  Archetypes should not be created in production code.
   * @returns {null|PhetioObject}
   * @private
   */
  createArchetype() {

    // Once the sim has started, any archetypes being created are likely done so because they are nested PhetioGroups.
    // TODO: why do we get this information from phetioAPIValidation? Does recent API file work change this? https://github.com/phetsims/phet-io/issues/1648
    if ( phetioAPIValidation.simHasStarted ) {
      return null;
    }

    // When generating the baseline, output the schema for the archetype
    if ( ( Tandem.PHET_IO_ENABLED && phet.preloads.phetio.queryParameters.phetioPrintAPI ) ||
         ( Tandem.PHET_IO_ENABLED && phet.preloads.phetio.queryParameters.phetioCreateArchetypes ) ) {
      const defaultArgs = Array.isArray( this.defaultArguments ) ? this.defaultArguments : this.defaultArguments();

      // The create function takes a tandem plus the default args
      assert && assert( this.createElement.length === defaultArgs.length + 1, 'mismatched number of arguments' );

      const archetype = this.createElement( this.tandem.createTandem( DynamicTandem.DYNAMIC_ARCHETYPE_NAME ), ...defaultArgs );

      // Mark the archetype for inclusion in the baseline schema
      archetype.markDynamicElementArchetype();
      return archetype;
    }
    else {
      return null;
    }
  }

  /**
   * Create a dynamic PhetioObject element for this container
   * @param {string} componentName
   * @param {Array.<*>} argsForCreateFunction
   * @param {function(new:ObjectIO)} containerParameterType
   * @returns {PhetioObject}
   * @public
   */
  createDynamicElement( componentName, argsForCreateFunction, containerParameterType ) {
    assert && assert( Array.isArray( argsForCreateFunction ), 'should be array' );

    // create with default state and substructure, details will need to be set by setter methods.

    let createdObjectTandem ;
    if ( !this.tandem.hasChild( componentName ) ) {
      createdObjectTandem = new DynamicTandem( this.tandem, componentName, this.tandem.getExtendedOptions() );
    }
    else {
      createdObjectTandem = this.tandem.createTandem( componentName, this.tandem.getExtendedOptions() );
      assert && assert( createdObjectTandem instanceof DynamicTandem, 'createdObjectTandem should be an instance of DynamicTandem' );
    }

    const createdObject = this.createElement( createdObjectTandem, ...argsForCreateFunction );

    // Make sure the new group element matches the schema for elements.
    validate( createdObject, containerParameterType.validator );

    assert && assert( createdObject.phetioType === containerParameterType,
      'dynamic element container expected its created instance\'s phetioType to match its parameterType.' );

    assert && this.assertDynamicPhetioObject( createdObject );

    return createdObject;
  }

  /**
   * A dynamic element should be an instrumented PhetioObject with phetioDynamicElement: true
   * @param {PhetioObject} phetioObject - object to be validated
   * @private
   */
  assertDynamicPhetioObject( phetioObject ) {
    if ( Tandem.PHET_IO_ENABLED ) {
      assert && assert( phetioObject instanceof PhetioObject, 'instance should be a PhetioObject' );
      assert && assert( phetioObject.isPhetioInstrumented(), 'instance should be instrumented' );
      assert && assert( phetioObject.phetioDynamicElement, 'instance should be marked as phetioDynamicElement:true' );
    }
  }

  /**
   * Emit a created or disposed event.
   * @param {PhetioObject} dynamicElement
   * @param {string} eventName
   * @param {Object} [additionalData] additional data for the event
   * @private
   */
  emitDataStreamEvent( dynamicElement, eventName, additionalData ) {
    this.phetioStartEvent( eventName, {
      data: merge( {
        phetioID: dynamicElement.tandem.phetioID
      }, additionalData )
    } );
    this.phetioEndEvent();
  }

  /**
   * Emit events when dynamic elements are created.
   * @param {PhetioObject} dynamicElement
   * @public
   */
  createdEventListener( dynamicElement ) {
    const additionalData = dynamicElement.phetioState ? {
      state: this.phetioType.parameterTypes[ 0 ].toStateObject( dynamicElement )
    } : null;
    this.emitDataStreamEvent( dynamicElement, 'created', additionalData );
  }

  /**
   * Emit events when dynamic elements are disposed.
   * @param {PhetioObject} dynamicElement
   * @public
   */
  disposedEventListener( dynamicElement ) {
    this.emitDataStreamEvent( dynamicElement, 'disposed' );
  }
}

tandemNamespace.register( 'PhetioDynamicElementContainer', PhetioDynamicElementContainer );
export default PhetioDynamicElementContainer;<|endoftext|>


<commit_before>/**
 * ownCloud - Music app
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Pauli Järvinen <pauli.jarvinen@gmail.com>
 * @copyright 2019, 2020 Pauli Järvinen
 *
 */

/**
 * This directive observes whether the host element is within the view-port or not.
 * When the status changes to either direction, it notifies the registered listeners.
 */
angular.module('Music').directive('inViewObserver', ['$rootScope', '$timeout', 'inViewService',
function($rootScope, $timeout, inViewService) {

	var _instances = []; // in creation order i.e. top-most first
	var _firstIndexInView = 0;
	var _lastIndexInView = -1;
	// tracking the range of visible items reduces the workload when there are a huge number of instances,
	// but it cannot be used while some of the instances may be hidden (e.g. with "display: none")
	var _trackVisibleRange = true;

	// Drop all instances when view switching or artists reloading begins
	$rootScope.$on('deactivateView', eraseInstances);
	$rootScope.$on('artistsUpdating', eraseInstances);

	function eraseInstances() {
		// cancel any pending notifications first
		_(_instances).each(function(inst) {
			if (inst.pendingEnterView) {
				$timeout.cancel(inst.pendingEnterView);
			}
		});

		_instances = [];
		_firstIndexInView = 0;
		_lastIndexInView = -1;
	}

	var throttledOnScroll = _.throttle(onScroll, 50, {leading: false});

	var scrollContainer = OC_Music_Utils.newLayoutStructure() ? document : document.getElementById('app-content');
	scrollContainer.addEventListener('scroll', throttledOnScroll);
	$rootScope.$on('resize', throttledOnScroll);
	$rootScope.$on('trackListCollapsed', throttledOnScroll);

	$rootScope.$on('inViewObserver_visibilityEvent', function(event, itemsMayBeHidden) {
		_trackVisibleRange = !itemsMayBeHidden;

		resetAll();
		if (_trackVisibleRange) {
			initInViewRange(/*skipDelays=*/true);
		} else {
			updateStatusForAll(/*skipDelays=*/true);
		}
	});

	$rootScope.$on('inViewObserver_revealElement', function(event, element) {
		var inst = _(_instances).find({element: element});

		// cancel any pending "enter view" because it's about to happen immediately
		if (inst.pendingEnterView) {
			$timeout.cancel(inst.pendingEnterView);
			inst.pendingEnterView = null;
			inst.inViewPort = false;
		}

		// nothing to do if the instance is already within the viewport and and notified about it
		if (!inst.inViewPort) {
			onEnterView(inst);
			inst.inViewPort = true;
		}
	});

	var debouncedNotifyLeave = _.debounce(function() {
		_(_instances).each(function(inst) {
			if (inst.leaveViewPending) {
				onLeaveView(inst);
			}
		});
	}, 1000);

	function onScroll() {
		// do not react while the layout building is still ongoing
		if (!$rootScope.loading) {

			if (_trackVisibleRange) {
				if (!validInViewRange()) {
					initInViewRange();
				} else {
					updateInViewRange();
				}
			}
			else {
				updateStatusForAll();
			}

			// leave notifications are post-poned until when the scrolling stops
			debouncedNotifyLeave();
		}
	}

	function validInViewRange() {
		return _firstIndexInView <= _lastIndexInView;
	}

	/**
	 * Initial setup of the in-view-port statuses of the available instances
	 */
	function initInViewRange(skipDelays/*optional*/) {
		var length = _instances.length;
		var i;

		// loop from the begining until we find the first instance in viewport
		for (i = 0; i < length; ++i) {
			if (updateInViewStatus(_instances[i], skipDelays)) {
				_firstIndexInView = i;
				_lastIndexInView = i;
				break;
			}
		}

		// if some instance was found, then continue looping until we have found
		// all the instances in viewport
		for (++i; i < length; ++i) {
			if (updateInViewStatus(_instances[i], skipDelays)) {
				_lastIndexInView = i;
			} else {
				break;
			}
		}
	}

	/**
	 * Update in-view-port status when we have a valid previous in-view-range
	 */
	function updateInViewRange() {
		var prevFirst = _firstIndexInView;
		var prevLast = _lastIndexInView;
		var i;
		var length = _instances.length;

		// Check if instances in the beginning of the range have slided off
		for (i = _firstIndexInView; i <= _lastIndexInView; ++i) {
			if (!updateInViewStatus(_instances[i])) {
				++_firstIndexInView;
			} else {
				break;
			}
		}

		// Check if instances in the end of the range have slided off
		for (i = _lastIndexInView; i > _firstIndexInView; --i) {
			if (!updateInViewStatus(_instances[i])) {
				--_lastIndexInView;
			} else {
				break;
			}
		}

		if (!validInViewRange()) {
			// None of the previous instances were anymore in the view-port.
			// We have lost the track of the scrolling direction and location
			// and have to start from scratch.
			initInViewRange();
		}
		else {
			// There may be more in-view instances above if the first index
			// did not move downwards
			if (prevFirst === _firstIndexInView) {
				for (i = _firstIndexInView - 1; i >= 0; --i) {
					if (updateInViewStatus(_instances[i])) {
						--_firstIndexInView;
					} else {
						break;
					}
				}
			}

			// There may be more in-view instances below if the last index
			// did not move upwards
			if (prevLast === _lastIndexInView) {
				for (i = _lastIndexInView + 1; i < length; ++i) {
					if (updateInViewStatus(_instances[i])) {
						++_lastIndexInView;
					} else {
						break;
					}
				}
			}
		}
	}

	/**
	 * Update in-view-port status of the given instance
	 */
	function updateInViewStatus(inst, skipDelays/*optional*/) {
		skipDelays = skipDelays || false;

		var wasInViewPort = inst.inViewPort;
		inst.inViewPort = instanceInViewPort(inst) && !instanceIsInvisible(inst);

		if (!wasInViewPort && inst.inViewPort) {
			if (!inst.leaveViewPending) {
				// element entered the viewport, notify the listeners with small delay,
				// unless immediate action has been requested
				if (skipDelays) {
					onEnterView(inst);
				} else {
					inst.pendingEnterView = $timeout(onEnterView, 250, true, inst);
				}
			}
			inst.leaveViewPending = false;
		}
		else if (wasInViewPort && !inst.inViewPort) {
			if (inst.pendingEnterView) {
				// element left the viewport before onEnterView was called, cancel the pending call
				$timeout.cancel(inst.pendingEnterView);
				inst.pendingEnterView = null;
			}
			else {
				// element left the viewport after it had been notified that it has entered the view
				if (skipDelays) {
					onLeaveView(inst);
				} else {
					inst.leaveViewPending = true;
				}
			}
		}

		return inst.inViewPort;
	}

	function instanceInViewPort(inst) {
		var margin = inst.viewPortMargin;
		return inViewService.isElementInViewPort(inst.element, margin, margin);
	}

	function instanceIsInvisible(inst) {
		var el = inst.element;
		// IE uses currentStyle, all the other browsers the getComputedStyle
		return el.currentStyle
			? (el.currentStyle.display == 'none')
			: (getComputedStyle(el, null).display == 'none');
	}

	function onEnterView(inst) {
		inst.pendingEnterView = null;

		if (!instanceIsInvisible(inst)) {
			_(inst.listeners).each(function(listener) {
				listener.onEnterView();
			});
		}
	}

	function onLeaveView(inst) {
		inst.leaveViewPending = false;
		_(inst.listeners).each(function(listener) {
			listener.onLeaveView();
		});
	}

	function resetAll() {
		_(_instances).each(function(inst) {
			if (inst.pendingEnterView) {
				$timeout.cancel(inst.pendingEnterView);
				inst.pendingEnterView = null;
			}
			if (!instanceIsInvisible(inst)) {
				onLeaveView(inst);
				inst.inViewPort = false;
			}
		});
		_firstIndexInView = 0;
		_lastIndexInView = -1;
	}

	function updateStatusForAll(skipDelays/*optional*/) {
		_(_instances).each(function(inst) {
			updateInViewStatus(inst, skipDelays);
		});
	}

	return {
		scope: {},
		controller: function($scope, $element) {
			this.inViewPort = false;
			this.leaveViewPending = false;
			this.pendingEnterView = null;
			this.element = $element[0];
			this.listeners = [];

			/**
			 * Listener must have two function-type properties: onEnterView and onLeaveView
			 */
			this.registerListener = function(listener) {
				this.listeners.push(listener);
			};

			_instances.push(this);
		},
		link: function(scope, element, attributes, controller) {
			controller.viewPortMargin = Number(attributes.inViewObserverMargin) || 500;

			// Remove this instance from the static array if this would still be there upon destruction.
			// This seems to happen whn the album view contents are updated during/after scanning.
			scope.$on('$destroy', function() {
				var index = _instances.indexOf(controller);
				if (index !== -1) {
					_instances.splice(index, 1);
				}
			});

			// The visibilites should be evaluated after all the ng-repeated instances
			// have been linked. There may be no actual `scroll` or `resize` events after
			// page load, in case there's so few items that no scrollbar appears.
			if (scope.$parent.$last && !$rootScope.loading) {
				throttledOnScroll();
			}
		}
	};
}]);
<commit_msg>Fix 'All tracks' view not always showing the tracks<commit_after>/**
 * ownCloud - Music app
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Pauli Järvinen <pauli.jarvinen@gmail.com>
 * @copyright 2019, 2020 Pauli Järvinen
 *
 */

/**
 * This directive observes whether the host element is within the view-port or not.
 * When the status changes to either direction, it notifies the registered listeners.
 */
angular.module('Music').directive('inViewObserver', ['$rootScope', '$timeout', 'inViewService',
function($rootScope, $timeout, inViewService) {

	var _instances = []; // in creation order i.e. top-most first
	var _firstIndexInView = 0;
	var _lastIndexInView = -1;
	// tracking the range of visible items reduces the workload when there are a huge number of instances,
	// but it cannot be used while some of the instances may be hidden (e.g. with "display: none")
	var _trackVisibleRange = true;

	// Drop all instances when view switching or artists reloading begins
	$rootScope.$on('deactivateView', eraseInstances);
	$rootScope.$on('artistsUpdating', eraseInstances);

	function eraseInstances() {
		// cancel any pending notifications first
		_(_instances).each(function(inst) {
			if (inst.pendingEnterView) {
				$timeout.cancel(inst.pendingEnterView);
			}
		});

		_instances = [];
		_firstIndexInView = 0;
		_lastIndexInView = -1;
	}

	var throttledOnScroll = _.throttle(onScroll, 50, {leading: false});

	var scrollContainer = OC_Music_Utils.newLayoutStructure() ? document : document.getElementById('app-content');
	scrollContainer.addEventListener('scroll', throttledOnScroll);
	$rootScope.$on('resize', throttledOnScroll);
	$rootScope.$on('trackListCollapsed', throttledOnScroll);
	$rootScope.$on('artistsLoaded', throttledOnScroll);
	$rootScope.$watch('loading', throttledOnScroll);

	$rootScope.$on('inViewObserver_visibilityEvent', function(event, itemsMayBeHidden) {
		_trackVisibleRange = !itemsMayBeHidden;

		resetAll();
		if (_trackVisibleRange) {
			initInViewRange(/*skipDelays=*/true);
		} else {
			updateStatusForAll(/*skipDelays=*/true);
		}
	});

	$rootScope.$on('inViewObserver_revealElement', function(event, element) {
		var inst = _(_instances).find({element: element});

		// cancel any pending "enter view" because it's about to happen immediately
		if (inst.pendingEnterView) {
			$timeout.cancel(inst.pendingEnterView);
			inst.pendingEnterView = null;
			inst.inViewPort = false;
		}

		// nothing to do if the instance is already within the viewport and and notified about it
		if (!inst.inViewPort) {
			onEnterView(inst);
			inst.inViewPort = true;
		}
	});

	var debouncedNotifyLeave = _.debounce(function() {
		_(_instances).each(function(inst) {
			if (inst.leaveViewPending) {
				onLeaveView(inst);
			}
		});
	}, 1000);

	function onScroll() {
		// do not react while the layout building is still ongoing
		if (!$rootScope.loading) {

			if (_trackVisibleRange) {
				if (!validInViewRange()) {
					initInViewRange();
				} else {
					updateInViewRange();
				}
			}
			else {
				updateStatusForAll();
			}

			// leave notifications are post-poned until when the scrolling stops
			debouncedNotifyLeave();
		}
	}

	function validInViewRange() {
		return _firstIndexInView <= _lastIndexInView;
	}

	/**
	 * Initial setup of the in-view-port statuses of the available instances
	 */
	function initInViewRange(skipDelays/*optional*/) {
		var length = _instances.length;
		var i;

		// loop from the begining until we find the first instance in viewport
		for (i = 0; i < length; ++i) {
			if (updateInViewStatus(_instances[i], skipDelays)) {
				_firstIndexInView = i;
				_lastIndexInView = i;
				break;
			}
		}

		// if some instance was found, then continue looping until we have found
		// all the instances in viewport
		for (++i; i < length; ++i) {
			if (updateInViewStatus(_instances[i], skipDelays)) {
				_lastIndexInView = i;
			} else {
				break;
			}
		}
	}

	/**
	 * Update in-view-port status when we have a valid previous in-view-range
	 */
	function updateInViewRange() {
		var prevFirst = _firstIndexInView;
		var prevLast = _lastIndexInView;
		var i;
		var length = _instances.length;

		// Check if instances in the beginning of the range have slided off
		for (i = _firstIndexInView; i <= _lastIndexInView; ++i) {
			if (!updateInViewStatus(_instances[i])) {
				++_firstIndexInView;
			} else {
				break;
			}
		}

		// Check if instances in the end of the range have slided off
		for (i = _lastIndexInView; i > _firstIndexInView; --i) {
			if (!updateInViewStatus(_instances[i])) {
				--_lastIndexInView;
			} else {
				break;
			}
		}

		if (!validInViewRange()) {
			// None of the previous instances were anymore in the view-port.
			// We have lost the track of the scrolling direction and location
			// and have to start from scratch.
			initInViewRange();
		}
		else {
			// There may be more in-view instances above if the first index
			// did not move downwards
			if (prevFirst === _firstIndexInView) {
				for (i = _firstIndexInView - 1; i >= 0; --i) {
					if (updateInViewStatus(_instances[i])) {
						--_firstIndexInView;
					} else {
						break;
					}
				}
			}

			// There may be more in-view instances below if the last index
			// did not move upwards
			if (prevLast === _lastIndexInView) {
				for (i = _lastIndexInView + 1; i < length; ++i) {
					if (updateInViewStatus(_instances[i])) {
						++_lastIndexInView;
					} else {
						break;
					}
				}
			}
		}
	}

	/**
	 * Update in-view-port status of the given instance
	 */
	function updateInViewStatus(inst, skipDelays/*optional*/) {
		skipDelays = skipDelays || false;

		var wasInViewPort = inst.inViewPort;
		inst.inViewPort = instanceInViewPort(inst) && !instanceIsInvisible(inst);

		if (!wasInViewPort && inst.inViewPort) {
			if (!inst.leaveViewPending) {
				// element entered the viewport, notify the listeners with small delay,
				// unless immediate action has been requested
				if (skipDelays) {
					onEnterView(inst);
				} else {
					inst.pendingEnterView = $timeout(onEnterView, 250, true, inst);
				}
			}
			inst.leaveViewPending = false;
		}
		else if (wasInViewPort && !inst.inViewPort) {
			if (inst.pendingEnterView) {
				// element left the viewport before onEnterView was called, cancel the pending call
				$timeout.cancel(inst.pendingEnterView);
				inst.pendingEnterView = null;
			}
			else {
				// element left the viewport after it had been notified that it has entered the view
				if (skipDelays) {
					onLeaveView(inst);
				} else {
					inst.leaveViewPending = true;
				}
			}
		}

		return inst.inViewPort;
	}

	function instanceInViewPort(inst) {
		var margin = inst.viewPortMargin;
		return inViewService.isElementInViewPort(inst.element, margin, margin);
	}

	function instanceIsInvisible(inst) {
		var el = inst.element;
		// IE uses currentStyle, all the other browsers the getComputedStyle
		return el.currentStyle
			? (el.currentStyle.display == 'none')
			: (getComputedStyle(el, null).display == 'none');
	}

	function onEnterView(inst) {
		inst.pendingEnterView = null;

		if (!instanceIsInvisible(inst)) {
			_(inst.listeners).each(function(listener) {
				listener.onEnterView();
			});
		}
	}

	function onLeaveView(inst) {
		inst.leaveViewPending = false;
		_(inst.listeners).each(function(listener) {
			listener.onLeaveView();
		});
	}

	function resetAll() {
		_(_instances).each(function(inst) {
			if (inst.pendingEnterView) {
				$timeout.cancel(inst.pendingEnterView);
				inst.pendingEnterView = null;
			}
			if (!instanceIsInvisible(inst)) {
				onLeaveView(inst);
				inst.inViewPort = false;
			}
		});
		_firstIndexInView = 0;
		_lastIndexInView = -1;
	}

	function updateStatusForAll(skipDelays/*optional*/) {
		_(_instances).each(function(inst) {
			updateInViewStatus(inst, skipDelays);
		});
	}

	return {
		scope: {},
		controller: function($scope, $element) {
			this.inViewPort = false;
			this.leaveViewPending = false;
			this.pendingEnterView = null;
			this.element = $element[0];
			this.listeners = [];

			/**
			 * Listener must have two function-type properties: onEnterView and onLeaveView
			 */
			this.registerListener = function(listener) {
				this.listeners.push(listener);
			};

			_instances.push(this);
		},
		link: function(scope, element, attributes, controller) {
			controller.viewPortMargin = Number(attributes.inViewObserverMargin) || 500;

			// Remove this instance from the static array if this would still be there upon destruction.
			// This seems to happen when the album view contents are updated during/after scanning.
			scope.$on('$destroy', function() {
				var index = _instances.indexOf(controller);
				if (index !== -1) {
					_instances.splice(index, 1);
				}
			});

			// The visibilites should be evaluated after all the ng-repeated instances
			// have been linked. There may be no actual `scroll` or `resize` events after
			// page load, in case there's so few items that no scrollbar appears.
			if (scope.$parent.$last && !$rootScope.loading) {
				throttledOnScroll();
			}
		}
	};
}]);
<|endoftext|>