<commit_before>import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/distinct';
import 'rxjs/add/observable/fromEvent';
import * as bbox from '@turf/bbox';
import * as union from '@turf/union';
import * as polylabel from 'polylabel';
import area from '@turf/area';
import { coordAll } from '@turf/meta';

import { MapLayerGroup } from '../data/map-layer-group';
import { MapFeature } from './map-feature';

@Injectable()
export class MapService {
  embedded = false;
  private map: mapboxgl.Map;
  private _isLoading = new BehaviorSubject<boolean>(true);
  isLoading$ = this._isLoading.asObservable();
  private colors = ['#e24000', '#434878', '#2c897f'];
  get mapCreated() { return this.map !== undefined; }

  constructor() { }

  /** Expose any MapboxGL API functions that are needed  */
  // https://www.mapbox.com/mapbox-gl-js/api/#map#setpaintproperty
  setLayerStyle(...args: any[]) { return this.map.setPaintProperty.apply(this.map, arguments); }
  // https://www.mapbox.com/mapbox-gl-js/api/#map#setfilter
  setLayerFilter(...args: any[]) { return this.map.setFilter.apply(this.map, arguments); }
  // https://www.mapbox.com/mapbox-gl-js/api/#map#setzoom
  setZoomLevel(...args: any[]) { return this.map.setZoom.apply(this.map, arguments); }
  enableZoom() { return (this.map ? this.map.scrollZoom.enable() : null); }
  disableZoom() { return (this.map ? this.map.scrollZoom.disable() : null); }

  /**
   * Create new Mapbox GL map from options object
   * @param options
   */
  createMap(options: Object) {
    const map = new mapboxgl.Map(options);
    map.dragRotate.disable();
    map.touchZoomRotate.disableRotation();
    return map;
  }

  setLoading(state: boolean) {
    this._isLoading.next(state);
  }

  /**
   * Sets the map instance for the service to control
   * @param map mapbox instance
   */
  setMapInstance(map) { this.map = map; }

  /**
   * Set the visibility for a mapbox layer
   * @param layerId layer id of the mapbox layer
   * @param visible sets the layer visible if true, or hides if false
   */
  setLayerVisibility(layerId: string, visible: boolean) {
    const visibility = visible ? 'visible' : 'none';
    if (this.map.getLayer(layerId)) {
      this.map.setLayoutProperty(layerId, 'visibility', visibility);
    }
  }

  /**
   * Set the visibility for a layer group
   * @param layerGroup the group of layers
   * @param visible sets the group visible if true, or hides if false
   */
  setLayerGroupVisibility(layerGroup: MapLayerGroup, visible: boolean) {
    layerGroup['layerIds'].forEach((layerId: string) => {
      this.setLayerVisibility(layerId, visible);
    });
  }

  /**
   * Update the layer styles of the layers within a group
   * @param layerGroup the layer group
   * @param styleProperty the paint style property to change (e.g. "fill-color")
   * @param newStyle the new property style (e.g. "#000000")
   */
  setLayerGroupStyle(layerGroup: MapLayerGroup, styleProperty: string, newStyle: any) {
    layerGroup['layerIds'].forEach((layerId) => {
      this.setLayerStyle(layerId, styleProperty, newStyle);
    });
  }

  /**
   * Update only the data property referenced in a map style
   * @param layerId
   * @param styleProperty
   * @param dataProperty
   */
  setLayerDataProperty(layerId: string, styleProperty: string, property: string) {
    const layerProperty = { ...this.map.getPaintProperty(layerId, styleProperty) };
    layerProperty['property'] = property;
    this.map.setPaintProperty(layerId, styleProperty, layerProperty);
  }

  /**
   * Updates the data property used in filters. Mostly used for updating null layers
   * @param layerId
   * @param property
   */
  setLayerFilterProperty(layerId: string, property: string) {
    const layerFilter = this.map.getFilter(layerId);
    layerFilter[1] = property;
    this.map.setFilter(layerId, layerFilter);
  }

  /**
   * Returns a boolean indicating if a layer has any features matching
   * the query
   *
   * @param layerId ID of vector tile layer to query
   * @param feature feature with attributes to query
   */
  hasRenderedFeatures(layerId: string, feature: MapFeature): boolean {
    return this.map.queryRenderedFeatures(undefined, {
      layers: [layerId],
      filter: [
        'all',
        ['==', 'n', feature.properties.n],
        ['==', 'pl', feature.properties.pl]
      ]
    }).length > 0;
  }

  /**
   * Queries a layer for all features matching the name and parent-location of
   * a supplied feature, returns a GeoJSON feature combining the geographies of
   * all matching features. Used to consolidate GeoJSON features split by tiling
   *
   * @param layerId ID of vector tile layer to query
   * @param feature feature with attributes to query
   */
  getUnionFeature(layerId: string, feature: MapFeature): GeoJSON.Feature<GeoJSON.Polygon> | null {
    // Check if feature already has needed data first
    const geomBbox = bbox(feature);
    const featBbox = this.featureBbox(feature);
    const addReduce = (a, b) => a + b;
    if (
      this.bboxArea(geomBbox) >= (this.bboxArea(featBbox) * 0.98) &&
      geomBbox.reduce(addReduce, 0) !== featBbox.reduce(addReduce, 0)
    ) {
      return feature as GeoJSON.Feature<GeoJSON.Polygon>;
    }
    const queryFeatures = this.map.queryRenderedFeatures(undefined, {
      layers: [layerId],
      filter: ['==', 'GEOID', feature.properties['GEOID']]
    });
    if (queryFeatures.length > 0) {
      return queryFeatures.reduce((currFeat, nextFeat) => {
        return union(
          currFeat as GeoJSON.Feature<GeoJSON.Polygon>,
          nextFeat as GeoJSON.Feature<GeoJSON.Polygon>
        );
      }) as GeoJSON.Feature<GeoJSON.Polygon>;
    }
    return null;
  }

  /**
   * Gets the bounds of the current map view and returns an array
   */
  getBoundsArray() {
    return this.map.getBounds().toArray();
  }

  /**
   * Returns source data
   * @param sourceId ID of source to return
   */
  getSourceData(sourceId) {
    return (this.map.getSource(sourceId) as mapboxgl.GeoJSONSource)['_data']['features'];
  }

  /**
   * Set the data of a GeoJSON layer source, or empty the data if no
   * feature supplied
   * @param sourceId ID of GeoJSON source to modify
   * @param features Array of MapFeature objects<commit_msg>Ignore topology exceptions for highlight features<commit_after>import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/distinct';
import 'rxjs/add/observable/fromEvent';
import * as bbox from '@turf/bbox';
import * as union from '@turf/union';
import * as polylabel from 'polylabel';
import area from '@turf/area';
import { coordAll } from '@turf/meta';

import { MapLayerGroup } from '../data/map-layer-group';
import { MapFeature } from './map-feature';

@Injectable()
export class MapService {
  embedded = false;
  private map: mapboxgl.Map;
  private _isLoading = new BehaviorSubject<boolean>(true);
  isLoading$ = this._isLoading.asObservable();
  private colors = ['#e24000', '#434878', '#2c897f'];
  get mapCreated() { return this.map !== undefined; }

  constructor() { }

  /** Expose any MapboxGL API functions that are needed  */
  // https://www.mapbox.com/mapbox-gl-js/api/#map#setpaintproperty
  setLayerStyle(...args: any[]) { return this.map.setPaintProperty.apply(this.map, arguments); }
  // https://www.mapbox.com/mapbox-gl-js/api/#map#setfilter
  setLayerFilter(...args: any[]) { return this.map.setFilter.apply(this.map, arguments); }
  // https://www.mapbox.com/mapbox-gl-js/api/#map#setzoom
  setZoomLevel(...args: any[]) { return this.map.setZoom.apply(this.map, arguments); }
  enableZoom() { return (this.map ? this.map.scrollZoom.enable() : null); }
  disableZoom() { return (this.map ? this.map.scrollZoom.disable() : null); }

  /**
   * Create new Mapbox GL map from options object
   * @param options
   */
  createMap(options: Object) {
    const map = new mapboxgl.Map(options);
    map.dragRotate.disable();
    map.touchZoomRotate.disableRotation();
    return map;
  }

  setLoading(state: boolean) {
    this._isLoading.next(state);
  }

  /**
   * Sets the map instance for the service to control
   * @param map mapbox instance
   */
  setMapInstance(map) { this.map = map; }

  /**
   * Set the visibility for a mapbox layer
   * @param layerId layer id of the mapbox layer
   * @param visible sets the layer visible if true, or hides if false
   */
  setLayerVisibility(layerId: string, visible: boolean) {
    const visibility = visible ? 'visible' : 'none';
    if (this.map.getLayer(layerId)) {
      this.map.setLayoutProperty(layerId, 'visibility', visibility);
    }
  }

  /**
   * Set the visibility for a layer group
   * @param layerGroup the group of layers
   * @param visible sets the group visible if true, or hides if false
   */
  setLayerGroupVisibility(layerGroup: MapLayerGroup, visible: boolean) {
    layerGroup['layerIds'].forEach((layerId: string) => {
      this.setLayerVisibility(layerId, visible);
    });
  }

  /**
   * Update the layer styles of the layers within a group
   * @param layerGroup the layer group
   * @param styleProperty the paint style property to change (e.g. "fill-color")
   * @param newStyle the new property style (e.g. "#000000")
   */
  setLayerGroupStyle(layerGroup: MapLayerGroup, styleProperty: string, newStyle: any) {
    layerGroup['layerIds'].forEach((layerId) => {
      this.setLayerStyle(layerId, styleProperty, newStyle);
    });
  }

  /**
   * Update only the data property referenced in a map style
   * @param layerId
   * @param styleProperty
   * @param dataProperty
   */
  setLayerDataProperty(layerId: string, styleProperty: string, property: string) {
    const layerProperty = { ...this.map.getPaintProperty(layerId, styleProperty) };
    layerProperty['property'] = property;
    this.map.setPaintProperty(layerId, styleProperty, layerProperty);
  }

  /**
   * Updates the data property used in filters. Mostly used for updating null layers
   * @param layerId
   * @param property
   */
  setLayerFilterProperty(layerId: string, property: string) {
    const layerFilter = this.map.getFilter(layerId);
    layerFilter[1] = property;
    this.map.setFilter(layerId, layerFilter);
  }

  /**
   * Returns a boolean indicating if a layer has any features matching
   * the query
   *
   * @param layerId ID of vector tile layer to query
   * @param feature feature with attributes to query
   */
  hasRenderedFeatures(layerId: string, feature: MapFeature): boolean {
    return this.map.queryRenderedFeatures(undefined, {
      layers: [layerId],
      filter: [
        'all',
        ['==', 'n', feature.properties.n],
        ['==', 'pl', feature.properties.pl]
      ]
    }).length > 0;
  }

  /**
   * Queries a layer for all features matching the name and parent-location of
   * a supplied feature, returns a GeoJSON feature combining the geographies of
   * all matching features. Used to consolidate GeoJSON features split by tiling
   *
   * @param layerId ID of vector tile layer to query
   * @param feature feature with attributes to query
   */
  getUnionFeature(layerId: string, feature: MapFeature): GeoJSON.Feature<GeoJSON.Polygon> | null {
    // Check if feature already has needed data first
    const geomBbox = bbox(feature);
    const featBbox = this.featureBbox(feature);
    const addReduce = (a, b) => a + b;
    if (
      this.bboxArea(geomBbox) >= (this.bboxArea(featBbox) * 0.98) &&
      geomBbox.reduce(addReduce, 0) !== featBbox.reduce(addReduce, 0)
    ) {
      return feature as GeoJSON.Feature<GeoJSON.Polygon>;
    }
    const queryFeatures = this.map.queryRenderedFeatures(undefined, {
      layers: [layerId],
      filter: ['==', 'GEOID', feature.properties['GEOID']]
    });
    if (queryFeatures.length > 0) {
      // Combine features, ignoring any TopologyExceptions
      try {
        return queryFeatures.reduce((currFeat, nextFeat) => {
          return union(
            currFeat as GeoJSON.Feature<GeoJSON.Polygon>,
            nextFeat as GeoJSON.Feature<GeoJSON.Polygon>
          );
        }) as GeoJSON.Feature<GeoJSON.Polygon>;
      } catch (e) { }
    }
    return null;
  }

  /**
   * Gets the bounds of the current map view and returns an array
   */
  getBoundsArray() {
    return this.map.getBounds().toArray();
  }

  /**
   * Returns source data
   * @param sourceId ID of source to return
   */
  getSourceData(sourceId) {
    return (this.map.getSource(sourceId) as mapboxgl.GeoJSONSource)['_data']['features'];
  }

  /**
   * Set the data of a GeoJSON layer source, or empty the data if no
   * feature supplied
   * @param sourceId ID of GeoJSON source to modify
   * @param features Array of MapFeature objects
{'fraction_non_alphanumeric': 0.12444754594091649, 'fraction_numerical': 0.004497169884469256, 'mean_word_length': 3.3648054145516073, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 29, 'https://': 6, 'lorem ipsum': 0, 'www.': 6, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6869993', 'n_tokens_mistral': 3999, 'n_tokens_neox': 3886, 'n_words': 1100}