<commit_before>/* @flow strict-local */
import type { GetState, Dispatch, Narrow, Topic, Action, Outbox, Stream } from '../types';
import * as api from '../api';
import { INIT_TOPICS } from '../actionConstants';
import { isStreamNarrow } from '../utils/narrow';
import { getAuth, getStreams } from '../selectors';
import { deleteOutboxMessage } from '../actions';
import { getOutbox } from '../directSelectors';

export const initTopics = (topics: Topic[], streamId: number): Action => ({
  type: INIT_TOPICS,
  topics,
  streamId,
});

export const fetchTopics = (streamId: number) => async (dispatch: Dispatch, getState: GetState) => {
  const auth = getAuth(getState());
  const { topics } = await api.getTopics(auth, streamId);
  dispatch(initTopics(topics, streamId));
};

export const fetchTopicsForActiveStream = (narrow: Narrow) => async (
  dispatch: Dispatch,
  getState: GetState,
) => {
  const state = getState();

  if (!isStreamNarrow(narrow)) {
    return;
  }

  const streams = getStreams(state);
  const stream = streams.find(sub => narrow[0].operand === sub.name);
  if (!stream) {
    return;
  }
  dispatch(fetchTopics(stream.stream_id));
};

export const deleteMessagesForTopic = (streamName: string, topic: string) => async (
  dispatch: Dispatch,
  getState: GetState,
) => {
  const state = getState();
  const outbox = getOutbox(state);
  outbox.forEach((outboxMessage: Outbox) => {
    if (outboxMessage.display_recipient === streamName && outboxMessage.subject === topic) {
      dispatch(deleteOutboxMessage(outboxMessage.id));
    }
  });
  const currentStream: Stream | void = getStreams(state).find(
    (stream: Stream) => stream.name === streamName,
  );
  if (currentStream) {
    await api.deleteTopic(getAuth(state), currentStream.stream_id, topic);
  }
};
<commit_msg>topic actions [nfc]: Check outbox message type first.<commit_after>/* @flow strict-local */
import type { GetState, Dispatch, Narrow, Topic, Action, Outbox, Stream } from '../types';
import * as api from '../api';
import { INIT_TOPICS } from '../actionConstants';
import { isStreamNarrow } from '../utils/narrow';
import { getAuth, getStreams } from '../selectors';
import { deleteOutboxMessage } from '../actions';
import { getOutbox } from '../directSelectors';

export const initTopics = (topics: Topic[], streamId: number): Action => ({
  type: INIT_TOPICS,
  topics,
  streamId,
});

export const fetchTopics = (streamId: number) => async (dispatch: Dispatch, getState: GetState) => {
  const auth = getAuth(getState());
  const { topics } = await api.getTopics(auth, streamId);
  dispatch(initTopics(topics, streamId));
};

export const fetchTopicsForActiveStream = (narrow: Narrow) => async (
  dispatch: Dispatch,
  getState: GetState,
) => {
  const state = getState();

  if (!isStreamNarrow(narrow)) {
    return;
  }

  const streams = getStreams(state);
  const stream = streams.find(sub => narrow[0].operand === sub.name);
  if (!stream) {
    return;
  }
  dispatch(fetchTopics(stream.stream_id));
};

export const deleteMessagesForTopic = (streamName: string, topic: string) => async (
  dispatch: Dispatch,
  getState: GetState,
) => {
  const state = getState();
  const outbox = getOutbox(state);
  outbox.forEach((outboxMessage: Outbox) => {
    if (
      outboxMessage.type === 'stream'
      && outboxMessage.display_recipient === streamName
      && outboxMessage.subject === topic
    ) {
      dispatch(deleteOutboxMessage(outboxMessage.id));
    }
  });
  const currentStream: Stream | void = getStreams(state).find(
    (stream: Stream) => stream.name === streamName,
  );
  if (currentStream) {
    await api.deleteTopic(getAuth(state), currentStream.stream_id, topic);
  }
};
<|endoftext|>


<commit_before>import debounce from "lodash/debounce";
import PropTypes from "prop-types";
import React, { Component } from "react";
import { connect } from "react-redux";

import styles from "../../styles/aoi/SearchAOIToolbar.css";
import { Typeahead, Menu } from "react-bootstrap-typeahead";
import { getGeonames } from "../../actions/aoi/searchToolbarActions";
import { TypeaheadMenuItem } from "./TypeaheadMenuItem";
import SearchAOIButton from "./SearchAOIButton";
import {
  setSearchAOIButtonSelected,
  setAllButtonsDefault
} from "../../actions/aoi/mapToolActions";

export class SearchAOIToolbar extends Component {
  state = {
    value: "",
    suggestions: []
  };

  componentWillMount() {
    this.debouncer = debounce(e => {
      this.handleChange(e);
    }, 500);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.geonames.fetched) {
      this.setState({ suggestions: nextProps.geonames.geonames });
    } else {
      if (this.state.suggestions.length > 0) {
        this.setState({ suggestions: [] });
      }
    }
    if (nextProps.toolbarIcons.search !== this.props.toolbarIcons.search) {
      if (nextProps.toolbarIcons.search === "DEFAULT") {
        this.refs.typeahead.getInstance().clear();
      }
    }
  }

  handleChange = e => {
    // if it matches minx,miny,maxx,maxy
    const match = e.split(",").map(Number);
    if (match.length === 4 && match[0] < match[2] && match[1] < match[3]) {
      this.props.setSearchAOIButtonSelected();
      this.props.handleSearch({
        name: "Manually Entered Bounds",
        bbox: {
          west: match[0],
          south: match[1],
          east: match[2],
          north: match[3]
        }
      });
    }
    // If 2 or more characters are entered then make request for suggested names.
    if (e.length >= 2) {
      this.props.getGeonames(e);
    } else {
      // If one or zero characters are entered then dont provide suggestions
      // If there are suggestions remove them
      if (this.state.suggestions.length > 0) {
        this.setState({ suggestions: [] });
      }
    }
  };

  handleEnter = e => {
    this.setState({ suggestions: [] });
    if (e.length > 0) {
      this.props.setSearchAOIButtonSelected();
      this.props.handleSearch(e[0]);
      this.refs.typeahead.getInstance().blur();
    }
  };

  render() {
    return (

          <Typeahead
            ref="typeahead"
            disabled={this.props.toolbarIcons.search === "INACTIVE"}
            options={this.state.suggestions}
            onChange={this.handleEnter}
            placeholder={"Search for a location or enter minX,minY,maxX,maxY"}
            onInputChange={this.debouncer}
            labelKey={"name"}
            paginate={false}
            emptyLabel={""}
            minLength={2}
            renderMenu={(results, menuProps) => {
              return (
                <Menu {...menuProps}>
                  {results.map((result, index) =>
                    <TypeaheadMenuItem
                      result={result}
                      index={index}
                      key={index}
                    />
                  )}
                </Menu>
              );
            }}
          />

          <SearchAOIButton handleCancel={this.props.handleCancel} />

    );
  }
}

SearchAOIToolbar.propTypes = {
  toolbarIcons: PropTypes.object,
  geonames: PropTypes.object,
  getGeonames: PropTypes.func,
  handleSearch: PropTypes.func,
  handleCancel: PropTypes.func,
  setSearchAOIButtonSelected: PropTypes.func
};

function mapStateToProps(state) {
  return {
    geonames: state.geonames,
    toolbarIcons: state.toolbarIcons
  };
}

export default connect(mapStateToProps, {
  getGeonames,
  setAllButtonsDefault,
  setSearchAOIButtonSelected
})(SearchAOIToolbar);
<commit_msg>Space out bbox description<commit_after>import debounce from "lodash/debounce";
import PropTypes from "prop-types";
import React, { Component } from "react";
import { connect } from "react-redux";

import styles from "../../styles/aoi/SearchAOIToolbar.css";
import { Typeahead, Menu } from "react-bootstrap-typeahead";
import { getGeonames } from "../../actions/aoi/searchToolbarActions";
import { TypeaheadMenuItem } from "./TypeaheadMenuItem";
import SearchAOIButton from "./SearchAOIButton";
import {
  setSearchAOIButtonSelected,
  setAllButtonsDefault
} from "../../actions/aoi/mapToolActions";

export class SearchAOIToolbar extends Component {
  state = {
    value: "",
    suggestions: []
  };

  componentWillMount() {
    this.debouncer = debounce(e => {
      this.handleChange(e);
    }, 500);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.geonames.fetched) {
      this.setState({ suggestions: nextProps.geonames.geonames });
    } else {
      if (this.state.suggestions.length > 0) {
        this.setState({ suggestions: [] });
      }
    }
    if (nextProps.toolbarIcons.search !== this.props.toolbarIcons.search) {
      if (nextProps.toolbarIcons.search === "DEFAULT") {
        this.refs.typeahead.getInstance().clear();
      }
    }
  }

  handleChange = e => {
    // if it matches minx,miny,maxx,maxy
    const match = e.split(",").map(Number);
    if (match.length === 4 && match[0] < match[2] && match[1] < match[3]) {
      this.props.setSearchAOIButtonSelected();
      this.props.handleSearch({
        name: "Manually Entered Bounds",
        bbox: {
          west: match[0],
          south: match[1],
          east: match[2],
          north: match[3]
        }
      });
    }
    // If 2 or more characters are entered then make request for suggested names.
    if (e.length >= 2) {
      this.props.getGeonames(e);
    } else {
      // If one or zero characters are entered then dont provide suggestions
      // If there are suggestions remove them
      if (this.state.suggestions.length > 0) {
        this.setState({ suggestions: [] });
      }
    }
  };

  handleEnter = e => {
    this.setState({ suggestions: [] });
    if (e.length > 0) {
      this.props.setSearchAOIButtonSelected();
      this.props.handleSearch(e[0]);
      this.refs.typeahead.getInstance().blur();
    }
  };

  render() {
    return (

          <Typeahead
            ref="typeahead"
            disabled={this.props.toolbarIcons.search === "INACTIVE"}
            options={this.state.suggestions}
            onChange={this.handleEnter}
            placeholder={"Search for a location or enter a bounding box as 'minX, minY, maxX, maxY'"}
            onInputChange={this.debouncer}
            labelKey={"name"}
            paginate={false}
            emptyLabel={""}
            minLength={2}
            renderMenu={(results, menuProps) => {
              return (
                <Menu {...menuProps}>
                  {results.map((result, index) =>
                    <TypeaheadMenuItem
                      result={result}
                      index={index}
                      key={index}
                    />
                  )}
                </Menu>
              );
            }}
          />

          <SearchAOIButton handleCancel={this.props.handleCancel} />

    );
  }
}

SearchAOIToolbar.propTypes = {
  toolbarIcons: PropTypes.object,
  geonames: PropTypes.object,
  getGeonames: PropTypes.func,
  handleSearch: PropTypes.func,
  handleCancel: PropTypes.func,
  setSearchAOIButtonSelected: PropTypes.func
};

function mapStateToProps(state) {
  return {
    geonames: state.geonames,
    toolbarIcons: state.toolbarIcons
  };
}

export default connect(mapStateToProps, {
  getGeonames,
  setAllButtonsDefault,
  setSearchAOIButtonSelected
})(SearchAOIToolbar);
<|endoftext|>


<commit_before>import $ from 'jquery';
import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helpers';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
import { formatBytes } from 'nomad-ui/helpers/format-bytes';
import moment from 'moment';

let node;

moduleForAcceptance('Acceptance | client detail', {
  beforeEach() {
    server.create('node', 'forceIPv4');
    node = server.db.nodes[0];

    // Related models
    server.create('agent');
    server.create('job', { createAllocations: false });
    server.createList('allocation', 3, { nodeId: node.id });
  },
});

test('/clients/:id should have a breadcrumb trail linking back to clients', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.equal(
      find('[data-test-breadcrumb="clients"]').textContent,
      'Clients',
      'First breadcrumb says clients'
    );
    assert.equal(
      find('[data-test-breadcrumb="client"]').textContent,
      node.id.split('-')[0],
      'Second breadcrumb says the node short id'
    );
  });

  andThen(() => {
    click(find('[data-test-breadcrumb="clients"]'));
  });

  andThen(() => {
    assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
  });
});

test('/clients/:id should list immediate details for the node in the title', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.ok(find('[data-test-title]').textContent.includes(node.name), 'Title includes name');
    assert.ok(find('[data-test-title]').textContent.includes(node.id), 'Title includes id');
    assert.ok(find(`[data-test-node-status="${node.status}"]`), 'Title includes status light');
  });
});

test('/clients/:id should list additional detail for the node below the title', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.equal(
      findAll('.inline-definitions .pair')[0].textContent,
      `Status ${node.status}`,
      'Status is in additional details'
    );
    assert.ok(
      $('[data-test-status-definition] .status-text').hasClass(`node-${node.status}`),
      'Status is decorated with a status class'
    );
    assert.equal(
      find('[data-test-address-definition]').textContent,
      `Address ${node.httpAddr}`,
      'Address is in additional detals'
    );
    assert.equal(
      find('[data-test-datacenter-definition]').textContent,
      `Datacenter ${node.datacenter}`,
      'Datacenter is in additional details'
    );
  });
});

test('/clients/:id should list all allocations on the node', function(assert) {
  const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length;

  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.equal(
      findAll('[data-test-allocation]').length,
      allocationsCount,
      `Allocations table lists all ${allocationsCount} associated allocations`
    );
  });
});

test('each allocation should have high-level details for the allocation', function(assert) {
  const allocation = server.db.allocations
    .where({ nodeId: node.id })
    .sortBy('modifyIndex')
    .reverse()[0];

  const allocStats = server.db.clientAllocationStats.find(allocation.id);
  const taskGroup = server.db.taskGroups.findBy({
    name: allocation.taskGroup,
    jobId: allocation.jobId,
  });

  const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
  const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0);
  const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0);

  visit(`/clients/${node.id}`);

  andThen(() => {
    const allocationRow = $(find('[data-test-allocation]'));
    assert.equal(
      allocationRow
        .find('[data-test-short-id]')
        .text()
        .trim(),
      allocation.id.split('-')[0],
      'Allocation short ID'
    );
    assert.equal(
      allocationRow
        .find('[data-test-modify-time]')
        .text()
        .trim(),
      moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'),
      'Allocation modify time'
    );
    assert.equal(
      allocationRow
        .find('[data-test-name]')
        .text()
        .trim(),
      allocation.name,
      'Allocation name'
    );
    assert.equal(
      allocationRow
        .find('[data-test-client-status]')
        .text()
        .trim(),
      allocation.clientStatus,
      'Client status'
    );
    assert.equal(
      allocationRow
        .find('[data-test-job]')
        .text()
        .trim(),
      server.db.jobs.find(allocation.jobId).name,
      'Job name'
    );
    assert.ok(
      allocationRow
        .find('[data-test-task-group]')
        .text()
        .includes(allocation.taskGroup),
      'Task group name'
    );
    assert.ok(
      allocationRow
        .find('[data-test-job-version]')
        .text()
        .includes(allocation.jobVersion),
      'Job Version'
    );
    assert.equal(
      allocationRow
        .find('[data-test-cpu]')
        .text()
        .trim(),
      Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
      'CPU %'
    );
    assert.equal(
      allocationRow.find('[data-test-cpu] .tooltip').attr('aria-label'),
      `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`,
      'Detailed CPU information is in a tooltip'
    );
    assert.equal(
      allocationRow
        .find('[data-test-mem]')
        .text()
        .trim(),
      allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
      'Memory used'
    );
    assert.equal(
      allocationRow.find('[data-test-mem] .tooltip').attr('aria-label'),
      `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`,
      'Detailed memory information is in a tooltip'
    );
  });
});

test('each allocation should show job information even if the job is incomplete and already in the store', function(assert) {
  // First, visit clients to load the allocations for each visible node.
  // Don't load the job belongsTo of the allocation! Leave it unfulfilled.

  visit('/clients');

  // Then, visit jobs to load all jobs, which should implicitly fulfill
  // the job belongsTo of each allocation pointed at each job.

  visit('/jobs');

  // Finally, visit a node to assert that the job name and task group name are
  // present. This will require reloading the job, since task groups aren't a
  // part of the jobs list response.

  visit(`/clients/${node.id}`);

  andThen(() => {
    const allocationRow = $(find('[data-test-allocation]'));
    const allocation = server.db.allocations
      .where({ nodeId: node.id })
      .sortBy('modifyIndex')
      .reverse()[0];

    assert.ok(
      allocationRow
        .find('[data-test-job]')
        .text()
        .includes(server.db.jobs.find(allocation.jobId).name),
      'Job name'
    );
    assert.ok(
      allocationRow
        .find('[data-test-task-group]')
        .text()
        .includes(allocation.taskGroup),
      'Task group name'
    );
  });
});

test('each allocation should link to the allocation detail page', function(assert) {
  const allocation = server.db.allocations
    .where({ nodeId: node.id })
    .sortBy('modifyIndex')
    .reverse()[0];

  visit(`/clients/${node.id}`);

  andThen(() => {
    click('[data-test-short-id] a');
  });

  andThen(() => {
    assert.equal(
      currentURL(),
      `/allocations/${allocation.id}`,
      'Allocation rows link to allocation detail pages'
    );
  });
});

test('each allocation should link to the job the allocation belongs to', function(assert) {
  visit(`/clients/${node.id}`);

  const allocation = server.db.allocations.where({ nodeId: node.id })[0];
  const job = server.db.jobs.find(allocation.jobId);

  andThen(() => {
    click('[data-test-job]');
  });

  andThen(() => {
    assert.equal(
      currentURL(),
      `/jobs/${job.id}`,
      'Allocation rows link to the job detail page for the allocation'
    );
  });
});

test('/clients/:id should list all attributes for the node', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.ok(find('[data-test-attributes]'), 'Attributes table is on the page');
  });
});

test('/clients/:id lists all meta attributes', function(assert) {
  node = server.create('node', 'forceIPv4', 'withMeta');

  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.ok(find('[data-test-meta]'), 'Meta attributes table is on the page');
    assert.notOk(find('[data-test-empty-meta-message]'), 'Meta attributes is not empty');

    const firstMetaKey = Object.keys(node.meta)[0];
    assert.equal(
      find('[data-test-meta] [data-test-key]').textContent.trim(),
      firstMetaKey,
      'Meta attributes for the node are bound to the attributes table'
    );
    assert.equal(
      find('[data-test-meta] [data-test-value]').textContent.trim(),
      node.meta[firstMetaKey],
      'Meta attributes for the node are bound to the attributes table'
    );
  });
});

test('/clients/:id shows an empty message when there is no meta data', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.notOk(find('[data-test-meta]'), 'Meta attributes table is not on the page');
    assert.ok(find('[data-test-empty-meta-message]'), 'Meta attributes is empty');
  });
});

test('when the node is not found, an error message is shown, but the URL persists', function(assert) {
  visit('/clients/not-a-real-node');

  andThen(() => {
    assert.equal(
      server.pretender.handledRequests.findBy('status', 404).url,
      '/v1/node/not-a-real-node',
      'A request to the non-existent node is made'
    );
    assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
    assert.ok(find('[data-test-error]'), 'Error message is shown');
    assert.equal(
      find('[data-test-error-title]').textContent,
      'Not Found',
      'Error message is for 404'
    );
  });
});
<commit_msg>spelling: details<commit_after>import $ from 'jquery';
import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helpers';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
import { formatBytes } from 'nomad-ui/helpers/format-bytes';
import moment from 'moment';

let node;

moduleForAcceptance('Acceptance | client detail', {
  beforeEach() {
    server.create('node', 'forceIPv4');
    node = server.db.nodes[0];

    // Related models
    server.create('agent');
    server.create('job', { createAllocations: false });
    server.createList('allocation', 3, { nodeId: node.id });
  },
});

test('/clients/:id should have a breadcrumb trail linking back to clients', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.equal(
      find('[data-test-breadcrumb="clients"]').textContent,
      'Clients',
      'First breadcrumb says clients'
    );
    assert.equal(
      find('[data-test-breadcrumb="client"]').textContent,
      node.id.split('-')[0],
      'Second breadcrumb says the node short id'
    );
  });

  andThen(() => {
    click(find('[data-test-breadcrumb="clients"]'));
  });

  andThen(() => {
    assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients');
  });
});

test('/clients/:id should list immediate details for the node in the title', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.ok(find('[data-test-title]').textContent.includes(node.name), 'Title includes name');
    assert.ok(find('[data-test-title]').textContent.includes(node.id), 'Title includes id');
    assert.ok(find(`[data-test-node-status="${node.status}"]`), 'Title includes status light');
  });
});

test('/clients/:id should list additional detail for the node below the title', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.equal(
      findAll('.inline-definitions .pair')[0].textContent,
      `Status ${node.status}`,
      'Status is in additional details'
    );
    assert.ok(
      $('[data-test-status-definition] .status-text').hasClass(`node-${node.status}`),
      'Status is decorated with a status class'
    );
    assert.equal(
      find('[data-test-address-definition]').textContent,
      `Address ${node.httpAddr}`,
      'Address is in additional details'
    );
    assert.equal(
      find('[data-test-datacenter-definition]').textContent,
      `Datacenter ${node.datacenter}`,
      'Datacenter is in additional details'
    );
  });
});

test('/clients/:id should list all allocations on the node', function(assert) {
  const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length;

  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.equal(
      findAll('[data-test-allocation]').length,
      allocationsCount,
      `Allocations table lists all ${allocationsCount} associated allocations`
    );
  });
});

test('each allocation should have high-level details for the allocation', function(assert) {
  const allocation = server.db.allocations
    .where({ nodeId: node.id })
    .sortBy('modifyIndex')
    .reverse()[0];

  const allocStats = server.db.clientAllocationStats.find(allocation.id);
  const taskGroup = server.db.taskGroups.findBy({
    name: allocation.taskGroup,
    jobId: allocation.jobId,
  });

  const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id));
  const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0);
  const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0);

  visit(`/clients/${node.id}`);

  andThen(() => {
    const allocationRow = $(find('[data-test-allocation]'));
    assert.equal(
      allocationRow
        .find('[data-test-short-id]')
        .text()
        .trim(),
      allocation.id.split('-')[0],
      'Allocation short ID'
    );
    assert.equal(
      allocationRow
        .find('[data-test-modify-time]')
        .text()
        .trim(),
      moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'),
      'Allocation modify time'
    );
    assert.equal(
      allocationRow
        .find('[data-test-name]')
        .text()
        .trim(),
      allocation.name,
      'Allocation name'
    );
    assert.equal(
      allocationRow
        .find('[data-test-client-status]')
        .text()
        .trim(),
      allocation.clientStatus,
      'Client status'
    );
    assert.equal(
      allocationRow
        .find('[data-test-job]')
        .text()
        .trim(),
      server.db.jobs.find(allocation.jobId).name,
      'Job name'
    );
    assert.ok(
      allocationRow
        .find('[data-test-task-group]')
        .text()
        .includes(allocation.taskGroup),
      'Task group name'
    );
    assert.ok(
      allocationRow
        .find('[data-test-job-version]')
        .text()
        .includes(allocation.jobVersion),
      'Job Version'
    );
    assert.equal(
      allocationRow
        .find('[data-test-cpu]')
        .text()
        .trim(),
      Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed,
      'CPU %'
    );
    assert.equal(
      allocationRow.find('[data-test-cpu] .tooltip').attr('aria-label'),
      `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`,
      'Detailed CPU information is in a tooltip'
    );
    assert.equal(
      allocationRow
        .find('[data-test-mem]')
        .text()
        .trim(),
      allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed,
      'Memory used'
    );
    assert.equal(
      allocationRow.find('[data-test-mem] .tooltip').attr('aria-label'),
      `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`,
      'Detailed memory information is in a tooltip'
    );
  });
});

test('each allocation should show job information even if the job is incomplete and already in the store', function(assert) {
  // First, visit clients to load the allocations for each visible node.
  // Don't load the job belongsTo of the allocation! Leave it unfulfilled.

  visit('/clients');

  // Then, visit jobs to load all jobs, which should implicitly fulfill
  // the job belongsTo of each allocation pointed at each job.

  visit('/jobs');

  // Finally, visit a node to assert that the job name and task group name are
  // present. This will require reloading the job, since task groups aren't a
  // part of the jobs list response.

  visit(`/clients/${node.id}`);

  andThen(() => {
    const allocationRow = $(find('[data-test-allocation]'));
    const allocation = server.db.allocations
      .where({ nodeId: node.id })
      .sortBy('modifyIndex')
      .reverse()[0];

    assert.ok(
      allocationRow
        .find('[data-test-job]')
        .text()
        .includes(server.db.jobs.find(allocation.jobId).name),
      'Job name'
    );
    assert.ok(
      allocationRow
        .find('[data-test-task-group]')
        .text()
        .includes(allocation.taskGroup),
      'Task group name'
    );
  });
});

test('each allocation should link to the allocation detail page', function(assert) {
  const allocation = server.db.allocations
    .where({ nodeId: node.id })
    .sortBy('modifyIndex')
    .reverse()[0];

  visit(`/clients/${node.id}`);

  andThen(() => {
    click('[data-test-short-id] a');
  });

  andThen(() => {
    assert.equal(
      currentURL(),
      `/allocations/${allocation.id}`,
      'Allocation rows link to allocation detail pages'
    );
  });
});

test('each allocation should link to the job the allocation belongs to', function(assert) {
  visit(`/clients/${node.id}`);

  const allocation = server.db.allocations.where({ nodeId: node.id })[0];
  const job = server.db.jobs.find(allocation.jobId);

  andThen(() => {
    click('[data-test-job]');
  });

  andThen(() => {
    assert.equal(
      currentURL(),
      `/jobs/${job.id}`,
      'Allocation rows link to the job detail page for the allocation'
    );
  });
});

test('/clients/:id should list all attributes for the node', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.ok(find('[data-test-attributes]'), 'Attributes table is on the page');
  });
});

test('/clients/:id lists all meta attributes', function(assert) {
  node = server.create('node', 'forceIPv4', 'withMeta');

  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.ok(find('[data-test-meta]'), 'Meta attributes table is on the page');
    assert.notOk(find('[data-test-empty-meta-message]'), 'Meta attributes is not empty');

    const firstMetaKey = Object.keys(node.meta)[0];
    assert.equal(
      find('[data-test-meta] [data-test-key]').textContent.trim(),
      firstMetaKey,
      'Meta attributes for the node are bound to the attributes table'
    );
    assert.equal(
      find('[data-test-meta] [data-test-value]').textContent.trim(),
      node.meta[firstMetaKey],
      'Meta attributes for the node are bound to the attributes table'
    );
  });
});

test('/clients/:id shows an empty message when there is no meta data', function(assert) {
  visit(`/clients/${node.id}`);

  andThen(() => {
    assert.notOk(find('[data-test-meta]'), 'Meta attributes table is not on the page');
    assert.ok(find('[data-test-empty-meta-message]'), 'Meta attributes is empty');
  });
});

test('when the node is not found, an error message is shown, but the URL persists', function(assert) {
  visit('/clients/not-a-real-node');

  andThen(() => {
    assert.equal(
      server.pretender.handledRequests.findBy('status', 404).url,
      '/v1/node/not-a-real-node',
      'A request to the non-existent node is made'
    );
    assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists');
    assert.ok(find('[data-test-error]'), 'Error message is shown');
    assert.equal(
      find('[data-test-error-title]').textContent,
      'Not Found',
      'Error message is for 404'
    );
  });
});
<|endoftext|>


<commit_before>var Class = require('../utils/Class');
var NumberTweenBuilder = require('./builders/NumberTweenBuilder');
var PluginManager = require('../plugins/PluginManager');
var TimelineBuilder = require('./builders/TimelineBuilder');
var TWEEN_CONST = require('./tween/const');
var TweenBuilder = require('./builders/TweenBuilder');

//  Phaser.Tweens.TweenManager

/**
 * @classdesc
 * [description]
 *
 * @class TweenManager
 * @memberOf Phaser.Tweens
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Scene} scene - [description]
 */
var TweenManager = new Class({

    initialize:

    function TweenManager (scene)
    {
        //  The Scene that owns this plugin

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#scene
         * @type {Phaser.Scene}
         * @since 3.0.0
         */
        this.scene = scene;

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#systems
         * @type {[type]}
         * @since 3.0.0
         */
        this.systems = scene.sys;

        if (!scene.sys.settings.isBooted)
        {
            scene.sys.events.once('boot', this.boot, this);
        }

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#timeScale
         * @type {number}
         * @default 1
         * @since 3.0.0
         */
        this.timeScale = 1;

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_add
         * @type {array}
         * @private
         * @default []
         * @since 3.0.0
         */
        this._add = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_pending
         * @type {array}
         * @private
         * @default []
         * @since 3.0.0
         */
        this._pending = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_active
         * @type {array}
         * @private
         * @default []
         * @since 3.0.0
         */
        this._active = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_destroy
         * @type {array}
         * @private
         * @default []
         * @since 3.0.0
         */
        this._destroy = [];

        this._toProcess = 0;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#boot
     * @since 3.0.0
     */
    boot: function ()
    {
        var eventEmitter = this.systems.events;

        eventEmitter.on('preupdate', this.preUpdate, this);
        eventEmitter.on('update', this.update, this);
        eventEmitter.on('shutdown', this.shutdown, this);
        eventEmitter.on('destroy', this.destroy, this);

        this.timeScale = 1;
    },

    //  Create a Tween Timeline and return it, but do NOT add it to the active or pending Tween lists
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#createTimeline
     * @since 3.0.0
     *
     * @param {[type]} config - [description]
     *
     * @return {[type]} [description]
     */
    createTimeline: function (config)
    {
        return TimelineBuilder(this, config);
    },

    //  Create a Tween Timeline and add it to the active Tween list
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#timeline
     * @since 3.0.0
     *
     * @param {[type]} config - [description]
     *
     * @return {[type]} [description]
     */
    timeline: function (config)
    {
        var timeline = TimelineBuilder(this, config);

        if (!timeline.paused)
        {
            this._add.push(timeline);

            this._toProcess++;
        }

        return timeline;
    },

    //  Create a Tween and return it, but do NOT add it to the active or pending Tween lists
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#create
     * @since 3.0.0
     *
     * @param {[type]} config - [description]
     *
     * @return {[type]} [description]
     */
    create: function (config)
    {
        return TweenBuilder(this, config);
    },

    //  Create a Tween and add it to the active Tween list
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#add
     * @since 3.0.0
     *
     * @param {[type]} config - [description]
     *
     * @return {[type]} [description]
     */
    add: function (config)
    {
        var tween = TweenBuilder(this, config);

        this._add.push(tween);

        this._toProcess++;

        return tween;
    },

    //  Add an existing tween into the active Tween list
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#existing
     * @since 3.0.0
     *
     * @param {[type]} tween - [description]
     *
     * @return {[type]} [description]
     */
    existing: function (tween)
    {
        this._add.push(tween);

        this._toProcess++;

        return this;
    },

    //  Create a Tween and add it to the active Tween list
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#addCounter
     * @since 3.0.0
     *
     * @param {[type]} config - [description]
     *
     * @return {[type]} [description]
     */
    addCounter: function (config)
    {
        var tween = NumberTweenBuilder(this, config);

        this._add.push(tween);

        this._toProcess++;

        return tween;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#preUpdate
     * @since 3.0.0
     *
     * @return {[type]} [description]
     */
    preUpdate: function ()
    {
        if (this._toProcess === 0)
        {
            //  Quick bail
            return;
        }

        var list = this._destroy;
        var active = this._active;
        var i;
        var tween;

        //  Clear the 'destroy' list
        for (i = 0; i < list.length; i++)
        {
            tween = list[i];

            //  Remove from the 'active' array
            var idx = active.indexOf(tween);

            if (idx !== -1)
            {
                tween.state = TWEEN_CONST.REMOVED;
                active.splice(idx, 1);
            }
        }

        list.length = 0;

        //  Process the addition list
        //  This stops callbacks and out of sync events from populating the active array mid-way during the update

        list = this._add;

        for (i = 0; i < list.length; i++)
        {
            tween = list[i];

            //  Return true if the Tween should be started right away, otherwise false
            if (tween.init())
            {
                tween.play();

                this._active.push(tween);
            }
            else
            {
                this._pending.push(tween);
            }
        }

        list.length = 0;

        this._toProcess = 0;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#update
     * @since 3.0.0
     *
     * @param {[type]} timestamp - [description]
     * @param {[type]} delta - [description]
     *
     * @return {[type]} [description]
     */
    update: function (timestamp, delta)
    {
        //  Process active tweens
        var list = this._active;
        var tween;

        //  Scale the delta
        delta *= this.timeScale;

        for (var i = 0; i < list.length; i++)
        {
            tween = list[i];

            //  If Tween.update returns 'true' then it means it has completed,
            //  so move it to the destroy list
            if (tween.update(timestamp, delta))
            {
                this._destroy.push(tween);
                this._toProcess++;
            }
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#makeActive
     * @since 3.0.0
     *
     * @param {[type]} tween - [description]
     *
     * @return {[type]} [description]
     */
    makeActive: function (tween)
    {
        if (this._add.indexOf(tween) !== -1 || this._active.indexOf(tween) !== -1)
        {
            return;
        }

        var idx = this._pending.indexOf(tween);

        if (idx !== -1)
        {
            this._pending.splice(idx, 1);
        }

        this._add.push(tween);

        tween.state = TWEEN_CONST.PENDING_ADD;

        this._toProcess++;

        return this;
    },

    // Passes all Tweens to the given callback.

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#each
     * @since 3.0.0
     *
     * @param {function} callback - [description]
     * @param {object} [thisArg] - [description]
     * @param {...*} [arguments] - [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#each
     * @since 3.0.0
     *
     * @param {[type]} callback - [description]
     * @param {[type]} thisArg - [description]
     */
    each: function (callback, thisArg)
    {
        var args = [ null ];

        for (var i = 1; i < arguments.length; i++)
        {
            args.push(arguments[i]);
        }

        for (var texture in this.list)
        {
            args[0] = this.list[texture];

            callback.apply(thisArg, args);
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getAllTweens
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.Tween[]} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getAllTweens
     * @since 3.0.0
     *
     * @return {[type]} [description]
     */
    getAllTweens: function ()
    {
        var list = this._active;
        var output = [];

        for (var i = 0; i < list.length; i++)
        {
            output.push(list[i]);
        }

        return output;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getGlobalTimeScale
     * @since 3.0.0
     *
     * @return {number} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getGlobalTimeScale
     * @since 3.0.0
     *
     * @return {[type]} [description]
     */
    getGlobalTimeScale: function ()
    {
        return this.timeScale;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getTweensOf
     * @since 3.0.0
     *
     * @param {object|array} target - [description]
     *
     * @return {Phaser.Tweens.Tween[]} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getTweensOf
     * @since 3.0.0
     *
     * @param {[type]} target - [description]
     *
     * @return {[type]} [description]
     */
    getTweensOf: function (target)
    {
        var list = this._active;
        var tween;
        var output = [];
        var i;

        if (Array.isArray(target))
        {
            for (i = 0; i < list.length; i++)
            {
                tween = list[i];

                for (var t = 0; t < target.length; i++)
                {
                    if (tween.hasTarget(target[t]))
                    {
                        output.push(tween);
                    }
                }
            }
        }
        else
        {
            for (i = 0; i < list.length; i++)
            {
                tween = list[i];

                if (tween.hasTarget(target))
                {
                    output.push(tween);
                }
            }
        }

        return output;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#isTweening
     * @since 3.0.0
     *
     * @param {any} target - [description]
     *
     * @return {boolean} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#isTweening
     * @since 3.0.0
     *
     * @param {[type]} target - [description]
     *
     * @return {[type]} [description]
     */
    isTweening: function (target)
    {
        var list = this._active;
        var tween;

        for (var i = 0; i < list.length; i++)
        {
            tween = list[i];

            if (tween.hasTarget(target) && tween.isPlaying())
            {
                return true;
            }
        }

        return false;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#killAll
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#killAll
     * @since 3.0.0
     *
     * @return {[type]} [description]
     */
    killAll: function ()
    {
        var tweens = this.getAllTweens();

        for (var i = 0; i < tweens.length; i++)
        {
            tweens[i].stop();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#killTweensOf
     * @since 3.0.0
     *
     * @param {object|array} target - [description]
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#killTweensOf
     * @since 3.0.0
     *
     * @param {[type]} target - [description]
     *
     * @return {[type]} [description]
     */
    killTweensOf: function (target)
    {
        var tweens = this.getTweensOf(target);

        for (var i = 0; i < tweens.length; i++)
        {
            tweens[i].stop();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#pauseAll
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#pauseAll
     * @since 3.0.0
     *
     * @return {[type]} [description]
     */
    pauseAll: function ()
    {
        var list = this._active;

        for (var i = 0; i < list.length; i++)
        {
            list[i].pause();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#resumeAll
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#resumeAll
     * @since 3.0.0
     *
     * @return {[type]} [description]
     */
    resumeAll: function ()
    {
        var list = this._active;

        for (var i = 0; i < list.length; i++)
        {
            list[i].resume();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#setGlobalTimeScale
     * @since 3.0.0
     *
     * @param {float} value - [description]
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#setGlobalTimeScale
     * @since 3.0.0
     *
     * @param {[type]} value - [description]
     *
     * @return {[type]} [description]
     */
    setGlobalTimeScale: function (value)
    {
        this.timeScale = value;

        return this;
    },

    //  Scene that owns this manager is shutting down

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#shutdown
     * @since 3.0.0
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#shutdown
     * @since 3.0.0
     */
    shutdown: function ()
    {
        this.killAll();

        this._add = [];
        this._pending = [];
        this._active = [];
        this._destroy = [];

        this._toProcess = 0;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#destroy
     * @since 3.0.0
     */
    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.shutdown();
    }

});

PluginManager.register('TweenManager', TweenManager, 'tweens');

module.exports = TweenManager;
<commit_msg>Added jsdocs<commit_after>var Class = require('../utils/Class');
var NumberTweenBuilder = require('./builders/NumberTweenBuilder');
var PluginManager = require('../plugins/PluginManager');
var TimelineBuilder = require('./builders/TimelineBuilder');
var TWEEN_CONST = require('./tween/const');
var TweenBuilder = require('./builders/TweenBuilder');

//  Phaser.Tweens.TweenManager

/**
 * @classdesc
 * [description]
 *
 * @class TweenManager
 * @memberOf Phaser.Tweens
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Scene} scene - [description]
 */
var TweenManager = new Class({

    initialize:

    function TweenManager (scene)
    {
        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#scene
         * @type {Phaser.Scene}
         * @since 3.0.0
         */
        this.scene = scene;

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#systems
         * @type {Phaser.Scenes.Systems}
         * @since 3.0.0
         */
        this.systems = scene.sys;

        if (!scene.sys.settings.isBooted)
        {
            scene.sys.events.once('boot', this.boot, this);
        }

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#timeScale
         * @type {number}
         * @default 1
         * @since 3.0.0
         */
        this.timeScale = 1;

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_add
         * @type {array}
         * @private
         * @since 3.0.0
         */
        this._add = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_pending
         * @type {array}
         * @private
         * @since 3.0.0
         */
        this._pending = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_active
         * @type {array}
         * @private
         * @since 3.0.0
         */
        this._active = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_destroy
         * @type {array}
         * @private
         * @since 3.0.0
         */
        this._destroy = [];

        /**
         * [description]
         *
         * @name Phaser.Tweens.TweenManager#_toProcess
         * @type {integer}
         * @private
         * @default 0
         * @since 3.0.0
         */
        this._toProcess = 0;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#boot
     * @since 3.0.0
     */
    boot: function ()
    {
        var eventEmitter = this.systems.events;

        eventEmitter.on('preupdate', this.preUpdate, this);
        eventEmitter.on('update', this.update, this);
        eventEmitter.on('shutdown', this.shutdown, this);
        eventEmitter.on('destroy', this.destroy, this);

        this.timeScale = 1;
    },

    /**
     * Create a Tween Timeline and return it, but do NOT add it to the active or pending Tween lists.
     *
     * @method Phaser.Tweens.TweenManager#createTimeline
     * @since 3.0.0
     *
     * @param {object} config - [description]
     *
     * @return {Phaser.Tweens.Timeline} [description]
     */
    createTimeline: function (config)
    {
        return TimelineBuilder(this, config);
    },

    /**
     * Create a Tween Timeline and add it to the active Tween list/
     *
     * @method Phaser.Tweens.TweenManager#timeline
     * @since 3.0.0
     *
     * @param {object} config - [description]
     *
     * @return {Phaser.Tweens.Timeline} [description]
     */
    timeline: function (config)
    {
        var timeline = TimelineBuilder(this, config);

        if (!timeline.paused)
        {
            this._add.push(timeline);

            this._toProcess++;
        }

        return timeline;
    },

    /**
     * Create a Tween and return it, but do NOT add it to the active or pending Tween lists.
     *
     * @method Phaser.Tweens.TweenManager#create
     * @since 3.0.0
     *
     * @param {object} config - [description]
     *
     * @return {Phaser.Tweens.Tween} [description]
     */
    create: function (config)
    {
        return TweenBuilder(this, config);
    },

    /**
     * Create a Tween and add it to the active Tween list.
     *
     * @method Phaser.Tweens.TweenManager#add
     * @since 3.0.0
     *
     * @param {[type]} config - [description]
     *
     * @return {Phaser.Tweens.Tween} [description]
     */
    add: function (config)
    {
        var tween = TweenBuilder(this, config);

        this._add.push(tween);

        this._toProcess++;

        return tween;
    },

    /**
     * Add an existing tween into the active Tween list.
     *
     * @method Phaser.Tweens.TweenManager#existing
     * @since 3.0.0
     *
     * @param {Phaser.Tweens.Tween} tween - [description]
     *
     * @return {Phaser.Tweens.TweenManager} This Tween Manager object.
     */
    existing: function (tween)
    {
        this._add.push(tween);

        this._toProcess++;

        return this;
    },

    /**
     * Create a Tween and add it to the active Tween list.
     *
     * @method Phaser.Tweens.TweenManager#addCounter
     * @since 3.0.0
     *
     * @param {object} config - [description]
     *
     * @return {Phaser.Tweens.Tween} [description]
     */
    addCounter: function (config)
    {
        var tween = NumberTweenBuilder(this, config);

        this._add.push(tween);

        this._toProcess++;

        return tween;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#preUpdate
     * @since 3.0.0
     */
    preUpdate: function ()
    {
        if (this._toProcess === 0)
        {
            //  Quick bail
            return;
        }

        var list = this._destroy;
        var active = this._active;
        var i;
        var tween;

        //  Clear the 'destroy' list
        for (i = 0; i < list.length; i++)
        {
            tween = list[i];

            //  Remove from the 'active' array
            var idx = active.indexOf(tween);

            if (idx !== -1)
            {
                tween.state = TWEEN_CONST.REMOVED;
                active.splice(idx, 1);
            }
        }

        list.length = 0;

        //  Process the addition list
        //  This stops callbacks and out of sync events from populating the active array mid-way during the update

        list = this._add;

        for (i = 0; i < list.length; i++)
        {
            tween = list[i];

            //  Return true if the Tween should be started right away, otherwise false
            if (tween.init())
            {
                tween.play();

                this._active.push(tween);
            }
            else
            {
                this._pending.push(tween);
            }
        }

        list.length = 0;

        this._toProcess = 0;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#update
     * @since 3.0.0
     *
     * @param {number} timestamp - [description]
     * @param {number} delta - [description]
     */
    update: function (timestamp, delta)
    {
        //  Process active tweens
        var list = this._active;
        var tween;

        //  Scale the delta
        delta *= this.timeScale;

        for (var i = 0; i < list.length; i++)
        {
            tween = list[i];

            //  If Tween.update returns 'true' then it means it has completed,
            //  so move it to the destroy list
            if (tween.update(timestamp, delta))
            {
                this._destroy.push(tween);
                this._toProcess++;
            }
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#makeActive
     * @since 3.0.0
     *
     * @param {Phaser.Tweens.Tween} tween - [description]
     *
     * @return {Phaser.Tweens.TweenManager} This Tween Manager object.
     */
    makeActive: function (tween)
    {
        if (this._add.indexOf(tween) !== -1 || this._active.indexOf(tween) !== -1)
        {
            return;
        }

        var idx = this._pending.indexOf(tween);

        if (idx !== -1)
        {
            this._pending.splice(idx, 1);
        }

        this._add.push(tween);

        tween.state = TWEEN_CONST.PENDING_ADD;

        this._toProcess++;

        return this;
    },

    /**
     * Passes all Tweens to the given callback.
     *
     * @method Phaser.Tweens.TweenManager#each
     * @since 3.0.0
     *
     * @param {function} callback - [description]
     * @param {object} [scope] - [description]
     * @param {...*} [arguments] - [description]
     */
    each: function (callback, scope)
    {
        var args = [ null ];

        for (var i = 1; i < arguments.length; i++)
        {
            args.push(arguments[i]);
        }

        for (var texture in this.list)
        {
            args[0] = this.list[texture];

            callback.apply(scope, args);
        }
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getAllTweens
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.Tween[]} [description]
     */
    getAllTweens: function ()
    {
        var list = this._active;
        var output = [];

        for (var i = 0; i < list.length; i++)
        {
            output.push(list[i]);
        }

        return output;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getGlobalTimeScale
     * @since 3.0.0
     *
     * @return {number} [description]
     */
    getGlobalTimeScale: function ()
    {
        return this.timeScale;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#getTweensOf
     * @since 3.0.0
     *
     * @param {object|array} target - [description]
     *
     * @return {Phaser.Tweens.Tween[]} [description]
     */
    getTweensOf: function (target)
    {
        var list = this._active;
        var tween;
        var output = [];
        var i;

        if (Array.isArray(target))
        {
            for (i = 0; i < list.length; i++)
            {
                tween = list[i];

                for (var t = 0; t < target.length; i++)
                {
                    if (tween.hasTarget(target[t]))
                    {
                        output.push(tween);
                    }
                }
            }
        }
        else
        {
            for (i = 0; i < list.length; i++)
            {
                tween = list[i];

                if (tween.hasTarget(target))
                {
                    output.push(tween);
                }
            }
        }

        return output;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#isTweening
     * @since 3.0.0
     *
     * @param {object} target - [description]
     *
     * @return {boolean} [description]
     */
    isTweening: function (target)
    {
        var list = this._active;
        var tween;

        for (var i = 0; i < list.length; i++)
        {
            tween = list[i];

            if (tween.hasTarget(target) && tween.isPlaying())
            {
                return true;
            }
        }

        return false;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#killAll
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    killAll: function ()
    {
        var tweens = this.getAllTweens();

        for (var i = 0; i < tweens.length; i++)
        {
            tweens[i].stop();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#killTweensOf
     * @since 3.0.0
     *
     * @param {object|array} target - [description]
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    killTweensOf: function (target)
    {
        var tweens = this.getTweensOf(target);

        for (var i = 0; i < tweens.length; i++)
        {
            tweens[i].stop();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#pauseAll
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    pauseAll: function ()
    {
        var list = this._active;

        for (var i = 0; i < list.length; i++)
        {
            list[i].pause();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#resumeAll
     * @since 3.0.0
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    resumeAll: function ()
    {
        var list = this._active;

        for (var i = 0; i < list.length; i++)
        {
            list[i].resume();
        }

        return this;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#setGlobalTimeScale
     * @since 3.0.0
     *
     * @param {float} value - [description]
     *
     * @return {Phaser.Tweens.TweenManager} [description]
     */
    setGlobalTimeScale: function (value)
    {
        this.timeScale = value;

        return this;
    },

    /**
     * Scene that owns this manager is shutting down.
     *
     * @method Phaser.Tweens.TweenManager#shutdown
     * @since 3.0.0
     */
    shutdown: function ()
    {
        this.killAll();

        this._add = [];
        this._pending = [];
        this._active = [];
        this._destroy = [];

        this._toProcess = 0;
    },

    /**
     * [description]
     *
     * @method Phaser.Tweens.TweenManager#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        this.shutdown();
    }

});

PluginManager.register('TweenManager', TweenManager, 'tweens');

module.exports = TweenManager;
<|endoftext|>


<commit_before>import _clone from 'lodash-es/clone';
import _map from 'lodash-es/map';
import { geoExtent, geoLineIntersection } from '../geo';
import { set as d3_set } from 'd3-collection';
import { utilDisplayLabel } from '../util';
import { t } from '../util/locale';
import {
    ValidationIssueType,
    ValidationIssueSeverity,
    validationIssue,
    validationIssueFix
} from './validation_issue';
import { osmNode } from '../osm';
import { actionAddMidpoint } from '../actions';
import { geoChooseEdge } from '../geo';

export function validationHighwayCrossingOtherWays(context) {
    // Check if the edge going from n1 to n2 crosses (without a connection node)
    // any edge on way. Return the corss point if so.
    function findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited) {
        var crossCoords = [];
        for (var j = 0; j < way.nodes.length - 1; j++) {
            var nidA = way.nodes[j],
                nidB = way.nodes[j + 1];
            if (nidA === n1.id || nidA === n2.id ||
                nidB === n1.id || nidB === n2.id) {
                // n1 or n2 is a connection node; skip
                continue;
            }

            var edgePair = edgePairString(n1.id, n2.id, nidA, nidB);
            if (edgePairsVisited.has(edgePair)) continue;
            edgePairsVisited.add(edgePair);

            var nA = graph.entity(nidA),
                nB = graph.entity(nidB),
                point = geoLineIntersection([n1.loc, n2.loc], [nA.loc, nB.loc]);
            if (point) crossCoords.push(point);
        }
        return crossCoords;
    }

    // n1 and n2 from one edge, nA and nB from the other edge
    function edgePairString(n1, n2, nA, nB) {
        return n1 > nA ? n1 + n2 + nA + nB : nA + nB + n1 + n2;
    }

    // returns the way or its parent relation, whichever has a useful feature type
    function getFeatureWithFeatureTypeTagsForWay(way, graph) {
        if (getFeatureTypeForTags(way.tags) === null) {
            // if the way doesn't match a feature type, check is parent relations
            var parentRels = graph.parentRelations(way);
            for (var i = 0; i < parentRels.length; i++) {
                var rel = parentRels[i];
                if (getFeatureTypeForTags(rel.tags) !== null) {
                    return rel;
                }
            }
        }
        return way;
    }

    function hasTag(tags, key) {
        return tags[key] !== undefined && tags[key] !== 'no';
    }

    function getFeatureTypeForCrossingCheck(way, graph) {
        var tags = getFeatureWithFeatureTypeTagsForWay(way, graph).tags;
        return getFeatureTypeForTags(tags);
    }

    function getFeatureTypeForTags(tags) {
        if (hasTag(tags, 'highway')) return 'highway';
        if (hasTag(tags, 'building')) return 'building';
        if (hasTag(tags, 'railway')) return 'railway';
        if (hasTag(tags, 'waterway') || tags.natural === 'water') return 'water';

        return null;
    }

    function extendTagsByInferredLayer(tags, way) {
        if (!hasTag(tags, 'layer')) {
            tags.layer = way.layer().toString();
        }
        return tags;
    }

    function isLegitCrossing(way1, featureType1, way2, featureType2, graph) {
        var tags1 = _clone(getFeatureWithFeatureTypeTagsForWay(way1, graph).tags),
            tags2 = _clone(getFeatureWithFeatureTypeTagsForWay(way2, graph).tags);
        tags1 = extendTagsByInferredLayer(tags1, way1);
        tags2 = extendTagsByInferredLayer(tags2, way2);

        // For better readability, not chaining all the true conditions into one if statement.
        if ((featureType1 === 'highway' && featureType2 === 'highway') ||
            (featureType1 === 'highway' && featureType2 === 'railway') ||
            (featureType1 === 'railway' && featureType2 === 'railway')) {
            // Legit cases:
            // (1) they're on different layers
            // (2) only one of the two ways is on a bridge
            // (3) only one of the two ways is in a tunnel
            if (tags1.layer !== tags2.layer) return true;
            if (hasTag(tags1, 'bridge') && !hasTag(tags2, 'bridge')) return true;
            if (!hasTag(tags1, 'bridge') && hasTag(tags2, 'bridge')) return true;
            if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
            if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
        }
        if ((featureType1 === 'highway' && featureType2 === 'water') ||
            (featureType1 === 'railway' && featureType2 === 'water')) {
            // Legit cases:
            // (1) highway/railway is on a bridge
            // (2) only one of the two ways is in a tunnel
            // (3) both are in tunnels but on different layers
            if (hasTag(tags1, 'bridge')) return true;
            if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
            if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
            if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
        }
        if ((featureType1 === 'highway' && featureType2 === 'building') ||
            (featureType1 === 'railway' && featureType2 === 'building')) {
            // Legit cases:
            // (1) highway/railway has a bridge or tunnel tag
            // (2) highway/railway has a covered tag
            if (hasTag(tags1, 'bridge') || hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
        }
        if (featureType1 === 'water' && featureType2 === 'water') {
            // Legit cases:
            // (1) only one of the water is in a tunnel
            // (2) both are in tunnels but on differnt layers
            if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
            if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
            if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
        }
        if (featureType1 === 'water' && featureType2 === 'building') {
            // Legit cases:
            // (1) water is in a tunnel
            // (2) water has a covered tag
            if (hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
        }
        if (featureType1 === 'building' && featureType2 === 'building') {
            // Legit case: they're on different layers
            if (tags1.layer !== tags2.layer) return true;
        }
        return false;
    }

    function findCrossingsByWay(entity, graph, tree, edgePairsVisited) {
        var edgeCrossInfos = [];
        if (entity.type !== 'way') return edgeCrossInfos;

        var entFeatureType = getFeatureTypeForCrossingCheck(entity, graph);
        if (entFeatureType === null) return edgeCrossInfos;

        for (var i = 0; i < entity.nodes.length - 1; i++) {
            var nid1 = entity.nodes[i],
                nid2 = entity.nodes[i + 1],
                n1 = graph.entity(nid1),
                n2 = graph.entity(nid2),
                extent = geoExtent([
                    [
                        Math.min(n1.loc[0], n2.loc[0]),
                        Math.min(n1.loc[1], n2.loc[1])
                    ],
                    [
                        Math.max(n1.loc[0], n2.loc[0]),
                        Math.max(n1.loc[1], n2.loc[1])
                    ]
                ]),
                intersected = tree.intersects(extent, graph);
            for (var j = 0; j < intersected.length; j++) {
                if (intersected[j].type !== 'way') continue;

                // only check crossing highway, waterway, building, and railway
                var way = intersected[j],
                    wayFeatureType = getFeatureTypeForCrossingCheck(way, graph);
                if (wayFeatureType === null ||
                    isLegitCrossing(entity, entFeatureType, way, wayFeatureType, graph) ||
                    isLegitCrossing(way, wayFeatureType, entity, entFeatureType, graph)) {
                    continue;
                }

                var crossCoords = findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited);
                for (var k = 0; k < crossCoords.length; k++) {
                    edgeCrossInfos.push({
                        ways: [entity, way],
                        featureTypes: [entFeatureType, wayFeatureType],
                        cross_point: crossCoords[k],
                    });
                }
            }
        }
        return edgeCrossInfos;
    }

    var validation = function(entitiesToCheck, graph, tree) {
        // create one issue per crossing point
        var edgePairsVisited = d3_set(),
            issues = [];
        for (var i = 0; i < entitiesToCheck.length; i++) {
            var crosses = findCrossingsByWay(entitiesToCheck[i], graph, tree, edgePairsVisited);
            for (var j = 0; j < crosses.length; j++) {
                var crossing = crosses[j];

                // use the entities with the tags that define the feature type
                var entities = _map(crossing.ways, function(way) {
                    return getFeatureWithFeatureTypeTagsForWay(way, graph);
                });
                entities = entities.sort(function(entity1, entity2) {
                    return utilDisplayLabel(entity1, context) > utilDisplayLabel(entity2, context);
                });

                var crossingTypeID = crossing.featureTypes.sort().join('-') + '_crossing';

                var messageDict = {};
                messageDict[crossing.featureTypes[0]] = utilDisplayLabel(entities[0], context);
                var key2 = crossing.featureTypes[1];
                if (crossing.featureTypes[0] === crossing.featureTypes[1]) {
                    key2 += '2';
                }
                messageDict[key2] = utilDisplayLabel(entities[1], context);

                issues.push(new validationIssue({
                    type: ValidationIssueType.crossing_ways,
                    severity: ValidationIssueSeverity.warning,
                    message: t('issues.'+crossingTypeID+'.message', messageDict),
                    tooltip: t('issues.'+crossingTypeID+'.tooltip'),
                    entities: entities,
                    info: {'ways': crossing.ways},
                    coordinates: crossing.cross_point,
                    fixes: [
                        new validationIssueFix({
                            title: t('issues.fix.add_connection_vertex.title'),
                            action: function() {
                                var loc = this.issue.coordinates;
                                var ways = this.issue.info.ways;

                                context.perform(
                                    function actionConnectCrossingWays(graph) {

                                        var node = osmNode();
                                        graph = graph.replace(node);

                                        var way0 = ways[0];
                                        var choice0 = geoChooseEdge(context.childNodes(way0), loc, context.projection);
                                        var edge0 = [way0.nodes[choice0.index - 1], way0.nodes[choice0.index]];
                                        graph = actionAddMidpoint({loc: choice0.loc, edge: edge0}, node)(graph);

                                        var way1 = ways[1];
                                        var choice1 = geoChooseEdge(context.childNodes(way1), loc, context.projection);
                                        var edge1 = [way1.nodes[choice1.index - 1], way1.nodes[choice1.index]];
                                        graph = actionAddMidpoint({loc: choice1.loc, edge: edge1}, node)(graph);

                                        return graph;
                                    },
                                    t('issues.fix.add_connection_vertex.undo_redo')
                                );
                            }
                        })
                    ]
                }));
            }
        }

        return issues;
    };

    return validation;
}
<commit_msg>Re-add multipolygon crossing validation<commit_after>import _clone from 'lodash-es/clone';
import _map from 'lodash-es/map';
import _flattenDeep from 'lodash-es/flatten';
import { geoExtent, geoLineIntersection } from '../geo';
import { set as d3_set } from 'd3-collection';
import { utilDisplayLabel } from '../util';
import { t } from '../util/locale';
import {
    ValidationIssueType,
    ValidationIssueSeverity,
    validationIssue,
    validationIssueFix
} from './validation_issue';
import { osmNode } from '../osm';
import { actionAddMidpoint } from '../actions';
import { geoChooseEdge } from '../geo';

export function validationHighwayCrossingOtherWays(context) {
    // Check if the edge going from n1 to n2 crosses (without a connection node)
    // any edge on way. Return the corss point if so.
    function findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited) {
        var crossCoords = [];
        for (var j = 0; j < way.nodes.length - 1; j++) {
            var nidA = way.nodes[j],
                nidB = way.nodes[j + 1];
            if (nidA === n1.id || nidA === n2.id ||
                nidB === n1.id || nidB === n2.id) {
                // n1 or n2 is a connection node; skip
                continue;
            }

            var edgePair = edgePairString(n1.id, n2.id, nidA, nidB);
            if (edgePairsVisited.has(edgePair)) continue;
            edgePairsVisited.add(edgePair);

            var nA = graph.entity(nidA),
                nB = graph.entity(nidB),
                point = geoLineIntersection([n1.loc, n2.loc], [nA.loc, nB.loc]);
            if (point) crossCoords.push(point);
        }
        return crossCoords;
    }

    // n1 and n2 from one edge, nA and nB from the other edge
    function edgePairString(n1, n2, nA, nB) {
        return n1 > nA ? n1 + n2 + nA + nB : nA + nB + n1 + n2;
    }

    // returns the way or its parent relation, whichever has a useful feature type
    function getFeatureWithFeatureTypeTagsForWay(way, graph) {
        if (getFeatureTypeForTags(way.tags) === null) {
            // if the way doesn't match a feature type, check is parent relations
            var parentRels = graph.parentRelations(way);
            for (var i = 0; i < parentRels.length; i++) {
                var rel = parentRels[i];
                if (getFeatureTypeForTags(rel.tags) !== null) {
                    return rel;
                }
            }
        }
        return way;
    }

    function hasTag(tags, key) {
        return tags[key] !== undefined && tags[key] !== 'no';
    }

    function getFeatureTypeForCrossingCheck(way, graph) {
        var tags = getFeatureWithFeatureTypeTagsForWay(way, graph).tags;
        return getFeatureTypeForTags(tags);
    }

    function getFeatureTypeForTags(tags) {
        if (hasTag(tags, 'highway')) return 'highway';
        if (hasTag(tags, 'building')) return 'building';
        if (hasTag(tags, 'railway')) return 'railway';
        if (hasTag(tags, 'waterway') || tags.natural === 'water') return 'water';

        return null;
    }

    function extendTagsByInferredLayer(tags, way) {
        if (!hasTag(tags, 'layer')) {
            tags.layer = way.layer().toString();
        }
        return tags;
    }

    function isLegitCrossing(way1, featureType1, way2, featureType2, graph) {
        var tags1 = _clone(getFeatureWithFeatureTypeTagsForWay(way1, graph).tags),
            tags2 = _clone(getFeatureWithFeatureTypeTagsForWay(way2, graph).tags);
        tags1 = extendTagsByInferredLayer(tags1, way1);
        tags2 = extendTagsByInferredLayer(tags2, way2);

        // For better readability, not chaining all the true conditions into one if statement.
        if ((featureType1 === 'highway' && featureType2 === 'highway') ||
            (featureType1 === 'highway' && featureType2 === 'railway') ||
            (featureType1 === 'railway' && featureType2 === 'railway')) {
            // Legit cases:
            // (1) they're on different layers
            // (2) only one of the two ways is on a bridge
            // (3) only one of the two ways is in a tunnel
            if (tags1.layer !== tags2.layer) return true;
            if (hasTag(tags1, 'bridge') && !hasTag(tags2, 'bridge')) return true;
            if (!hasTag(tags1, 'bridge') && hasTag(tags2, 'bridge')) return true;
            if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
            if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
        }
        if ((featureType1 === 'highway' && featureType2 === 'water') ||
            (featureType1 === 'railway' && featureType2 === 'water')) {
            // Legit cases:
            // (1) highway/railway is on a bridge
            // (2) only one of the two ways is in a tunnel
            // (3) both are in tunnels but on different layers
            if (hasTag(tags1, 'bridge')) return true;
            if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
            if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
            if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
        }
        if ((featureType1 === 'highway' && featureType2 === 'building') ||
            (featureType1 === 'railway' && featureType2 === 'building')) {
            // Legit cases:
            // (1) highway/railway has a bridge or tunnel tag
            // (2) highway/railway has a covered tag
            if (hasTag(tags1, 'bridge') || hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
        }
        if (featureType1 === 'water' && featureType2 === 'water') {
            // Legit cases:
            // (1) only one of the water is in a tunnel
            // (2) both are in tunnels but on differnt layers
            if (hasTag(tags1, 'tunnel') && !hasTag(tags2, 'tunnel')) return true;
            if (!hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel')) return true;
            if (hasTag(tags1, 'tunnel') && hasTag(tags2, 'tunnel') && tags1.layer !== tags2.layer) return true;
        }
        if (featureType1 === 'water' && featureType2 === 'building') {
            // Legit cases:
            // (1) water is in a tunnel
            // (2) water has a covered tag
            if (hasTag(tags1, 'tunnel') || hasTag(tags1, 'covered')) return true;
        }
        if (featureType1 === 'building' && featureType2 === 'building') {
            // Legit case: they're on different layers
            if (tags1.layer !== tags2.layer) return true;
        }
        return false;
    }

    function findCrossingsByWay(entity, graph, tree, edgePairsVisited) {
        var edgeCrossInfos = [];
        if (entity.type !== 'way') return edgeCrossInfos;

        var entFeatureType = getFeatureTypeForCrossingCheck(entity, graph);
        if (entFeatureType === null) return edgeCrossInfos;

        for (var i = 0; i < entity.nodes.length - 1; i++) {
            var nid1 = entity.nodes[i],
                nid2 = entity.nodes[i + 1],
                n1 = graph.entity(nid1),
                n2 = graph.entity(nid2),
                extent = geoExtent([
                    [
                        Math.min(n1.loc[0], n2.loc[0]),
                        Math.min(n1.loc[1], n2.loc[1])
                    ],
                    [
                        Math.max(n1.loc[0], n2.loc[0]),
                        Math.max(n1.loc[1], n2.loc[1])
                    ]
                ]),
                intersected = tree.intersects(extent, graph);
            for (var j = 0; j < intersected.length; j++) {
                if (intersected[j].type !== 'way') continue;

                // only check crossing highway, waterway, building, and railway
                var way = intersected[j],
                    wayFeatureType = getFeatureTypeForCrossingCheck(way, graph);
                if (wayFeatureType === null ||
                    isLegitCrossing(entity, entFeatureType, way, wayFeatureType, graph) ||
                    isLegitCrossing(way, wayFeatureType, entity, entFeatureType, graph)) {
                    continue;
                }

                var crossCoords = findEdgeToWayCrossCoords(n1, n2, way, graph, edgePairsVisited);
                for (var k = 0; k < crossCoords.length; k++) {
                    edgeCrossInfos.push({
                        ways: [entity, way],
                        featureTypes: [entFeatureType, wayFeatureType],
                        cross_point: crossCoords[k],
                    });
                }
            }
        }
        return edgeCrossInfos;
    }

    var validation = function(entitiesToCheck, graph, tree) {
        // create one issue per crossing point
        var edgePairsVisited = d3_set(),
            issues = [];
        var waysToCheck = _flattenDeep(_map(entitiesToCheck, function(entity) {
            if (!getFeatureTypeForTags(entity.tags)) {
                return [];
            }
            if (entity.type === 'way') {
                return entity;
            } else if (entity.type === 'relation' && entity.tags.type === 'multipolygon') {
                return _map(entity.members, function(member) {
                    if (context.hasEntity(member.id)) {
                        var entity = context.entity(member.id);
                        if (entity.type === 'way') {
                            return entity;
                        }
                    }
                    return [];
                });
            }
            return [];
        }));
        for (var i = 0; i < waysToCheck.length; i++) {
            var crosses = findCrossingsByWay(waysToCheck[i], graph, tree, edgePairsVisited);
            for (var j = 0; j < crosses.length; j++) {
                var crossing = crosses[j];

                // use the entities with the tags that define the feature type
                var entities = _map(crossing.ways, function(way) {
                    return getFeatureWithFeatureTypeTagsForWay(way, graph);
                });
                entities = entities.sort(function(entity1, entity2) {
                    return utilDisplayLabel(entity1, context) > utilDisplayLabel(entity2, context);
                });

                var crossingTypeID = crossing.featureTypes.sort().join('-') + '_crossing';

                var messageDict = {};
                messageDict[crossing.featureTypes[0]] = utilDisplayLabel(entities[0], context);
                var key2 = crossing.featureTypes[1];
                if (crossing.featureTypes[0] === crossing.featureTypes[1]) {
                    key2 += '2';
                }
                messageDict[key2] = utilDisplayLabel(entities[1], context);

                issues.push(new validationIssue({
                    type: ValidationIssueType.crossing_ways,
                    severity: ValidationIssueSeverity.warning,
                    message: t('issues.'+crossingTypeID+'.message', messageDict),
                    tooltip: t('issues.'+crossingTypeID+'.tooltip'),
                    entities: entities,
                    info: {'ways': crossing.ways},
                    coordinates: crossing.cross_point,
                    fixes: [
                        new validationIssueFix({
                            title: t('issues.fix.add_connection_vertex.title'),
                            action: function() {
                                var loc = this.issue.coordinates;
                                var ways = this.issue.info.ways;

                                context.perform(
                                    function actionConnectCrossingWays(graph) {

                                        var node = osmNode();
                                        graph = graph.replace(node);

                                        var way0 = ways[0];
                                        var choice0 = geoChooseEdge(context.childNodes(way0), loc, context.projection);
                                        var edge0 = [way0.nodes[choice0.index - 1], way0.nodes[choice0.index]];
                                        graph = actionAddMidpoint({loc: choice0.loc, edge: edge0}, node)(graph);

                                        var way1 = ways[1];
                                        var choice1 = geoChooseEdge(context.childNodes(way1), loc, context.projection);
                                        var edge1 = [way1.nodes[choice1.index - 1], way1.nodes[choice1.index]];
                                        graph = actionAddMidpoint({loc: choice1.loc, edge: edge1}, node)(graph);

                                        return graph;
                                    },
                                    t('issues.fix.add_connection_vertex.undo_redo')
                                );
                            }
                        })
                    ]
                }));
            }
        }

        return issues;
    };

    return validation;
}
<|endoftext|>