Source: ol/Basemap.js

import $ from 'jquery'
import View from 'ol/View'
import Map from 'ol/Map'
import olms from 'ol-mapbox-style'
import DoubleClickZoom from 'ol/interaction/DoubleClickZoom'
import DragPan from 'ol/interaction/DragPan'
import DragZoom from 'ol/interaction/DragZoom'
import KeyboardPan from 'ol/interaction/KeyboardPan'
import KeyboardZoom from 'ol/interaction/KeyboardZoom'
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom'
import PinchZoom from 'ol/interaction/PinchZoom'
import LocalStorage from 'nyc/ol/LocalStorage'
import LayerTile from 'ol/layer/Tile'
import SourceXYZ from 'ol/source/XYZ'
import nycOl from 'nyc/ol'
import GeoJSON from 'ol/format/GeoJSON'
import proj4 from 'proj4'

class Basemap extends Map {
  constructor(options) {
    options = options || {}
    const viewProvided = options.view instanceof View
    Basemap.setupView(options)
    if (!options.interactions) {
      options.interactions = [
        new DoubleClickZoom(),
        new DragPan(),
        new PinchZoom(),
        new KeyboardPan(),
        new KeyboardZoom(),
        new MouseWheelZoom(),
        new DragZoom()
      ]
    }

    super(options)

    /**
     * @private
     * @member {boolean}
     */
    this.mvt = options.mvt
    /**
     * @private
     * @member {Object<string, LayerTile>}
     */
    this.labels = {}
    /**
     * @private
     * @member {number}
     */
    this.latestPhoto = 0
    /**
    * @private
    * @member {LayerTile}
    */
    this.base = null
    /**
     * @private
     * @member {Object<string, OlLayerTile>}
     */
    this.photos = {}
    /**
     * @private
     * @member {module:nyc/LocalStorage~LocalStorage}
     */
    this.storage = new LocalStorage()

    if (options.mvt) {
      olms(this, Basemap.MVT_STYLE_URL).then(map => {
        const layers = map.getLayers().getArray()
        this.base = layers[layers.length - 1]
        this.setupPhotos(options)
      }).catch(err => console.error(err))
    } else {
      this.base = new LayerTile({
        extent: this.layerExtent(Basemap.UNIVERSE_EXTENT, options.view),
        source: new SourceXYZ({url: Basemap.BASE_URL})
      })
      this.addLayer(this.base)
      Object.entries(Basemap.LABEL_URLS).forEach(([labelType, url]) => {
        this.labels[labelType] = new LayerTile({
          extent: this.layerExtent(Basemap.LABEL_EXTENT, options.view),
          source: new SourceXYZ({url}),
          zIndex: 1000,
          visible: labelType === Basemap.LabelType.BASE
        })
        this.addLayer(this.labels[labelType])
      })
    }
    this.setupPhotos(options)
    this.hookupEvents(this.getTargetElement())
    this.defaultExtent(viewProvided)
  }
  /**
   * @desc Returns the base layers
   * @public
   * @method
   * @return {nyc.Basemap.BaseLayers} Base layers
   */
  getBaseLayers() {
    return {
      base: this.base,
      labels: this.labels,
      photos: this.photos
    }
  }
  /**
   * @desc Get the storage used for loading and saving data
   * @access protected
   * @method
   * @return {module:nyc/ol/LocalStorage~LocalStorage} storage
   */
  getStorage() {
    return this.storage
  }
  /**
   * @private
   * @method
   * @param {Object} options Options
   */
  setupPhotos(options) {
    Object.entries(Basemap.PHOTO_URLS).forEach(([year, url]) => {
      const photo = new LayerTile({
        extent: this.layerExtent(Basemap.PHOTO_EXTENT, options.view),
        source: new SourceXYZ({url}),
        visible: false
      })
      if ((year.split('-')[0] * 1) > this.latestPhoto) {
        this.latestPhoto = year
      }
      photo.set('name', year)
      this.addLayer(photo)
      photo.on('change:visible', this.photoChange.bind(this))
      this.photos[year] = photo
    })
  }
  /**
   * @private
   * @method
   * @param {ol.Extent} extent Extent
   * @param {ol.View|undefined} view The OpenLayers view
   * @return {ol.Extent} The layer extent
   */
  layerExtent(extent, view) {
    if (view && view.getProjection().getCode() !== 'EPSG:3857') {
      const fr = 'EPSG:3857'
      const to = 'EPSG:2263'
      const bl = proj4(fr, to, [extent[0], extent[1]])
      const tr = proj4(fr, to, [extent[2], extent[3]])
      return [bl[0], bl[1], tr[0], tr[1]]
    }
    return extent
  }
  /**
   * @private
   * @method
   */
  photoChange() {
    let isPhoto = false
    Object.entries(this.photos).some(([year, layer]) => {
      isPhoto = layer.getVisible()
      if (isPhoto) {
        this.showLabels(Basemap.LabelType.PHOTO)
        return true
      }
    })
    if (!isPhoto) {
      this.showLabels(Basemap.LabelType.BASE)
    }
  }
  /**
   * @desc Show photo layer
   * @public
   * @method
   * @param {number=} year The photo year to show - shows the latest year if not provided
   */
  showPhoto(year) {
    this.hidePhoto()
    this.photos[(year || this.latestPhoto) + ''].setVisible(true)
    if (!this.mvt) {
      this.showLabels(Basemap.LabelType.PHOTO)
    }
  }
  /**
   * @desc Hide photo layer
   * @public
   * @method
   */
  hidePhoto() {
    if (!this.mvt) {
      this.showLabels(Basemap.LabelType.BASE)
    }
    Object.entries(this.photos).forEach(([year, layer]) => {
      layer.setVisible(false)
    })
  }
  /**
   * @desc Show the specified label layer
   * @public
   * @method
   * @param {nyc.Basemap.BaseLayers} labelType The label type to show
   */
  showLabels(labelType) {
    if (!this.mvt && this.labels.base) {
      this.labels.base.setVisible(labelType === Basemap.LabelType.BASE);
    }
    if (this.labels.photo) {
      this.labels.photo.setVisible(labelType === Basemap.LabelType.PHOTO);
    }
  }
  /**
   * @private
   * @method
   * @param {boolean} viewProvided was the view provided?
   */
  defaultExtent(viewProvided) {
    if (!viewProvided) {
      this.getView().fit(Basemap.EXTENT, this.getSize())
    }
  }
  /**
   * @desc Hook up events
   * @public
   * @method
   * @param {Element} node The DOM node for the map
   */
  hookupEvents(node) {
    $(node).on('drop', this.loadLayer.bind(this))
    $(node).on('dragover', (event) => {
      event.preventDefault()
    })
  }
  /**
   * @desc Loads a layer from a file
   * @public
   * @method
   * @param {jQuery.Event} event Event object
   */
  loadLayer(event) {
    const transfer = event.originalEvent.dataTransfer
    event.preventDefault()
    event.stopPropagation()
    if (transfer && transfer.files.length) {
      const files = transfer.files
      const ext = files[0].name.split('.').pop().toLowerCase()
      if (ext === 'json') {
        this.storage.loadGeoJsonFile(this, null, files[0])
      } else {
        this.storage.loadShapeFile(this, null, files)
      }
    }
  }
  /**
   * @desc Save source as GeoJSON
   * @public
   * @method
   * @param {ol.source.Vector} source The source
   * @param {string=} name The name of the file to write
   */
  saveGeoJson(source, name) {
    const format = new GeoJSON({
      featureProjection: this.getView().getProjection(),
      dataProjection: 'EPSG:4326'
    })
    this.storage.saveGeoJson(name || 'layer.json', format.writeFeatures(source.getFeatures()))
  }
  /**
   * @desc Returns the photo layers ordered by year
    * @public
    * @method
    * @return {Array<ol.layer.Base|L.Layer>} Array of photo layers
    */
  sortedPhotos() {
    const sorted = []
    Object.keys(this.photos).forEach(photo => {
      sorted.push(this.photos[photo])
    })
    /* sort descending on the first 4 digits - puts 2001-2 in the proper place */
    return sorted.sort((a, b) => {
      const aName = a.name || a.get('name')
      const bName = b.name || b.get('name')
      return bName.substr(0, 4) - aName.substr(0, 4)
    })
  }
}

Basemap.setupView = (options) => {
  if (!(options.view instanceof View)) {
    options.view = new View({
      center: Basemap.CENTER,
      minZoom: 8,
      maxZoom: 21,
      zoom: 8,
      constrainRotation: 1
    })
  }
}

/**
 * @private
 * @const
 * @type {ol.Extent}
 */
Basemap.UNIVERSE_EXTENT = [-8453323, 4774561, -7983695, 5165920]

/**
* @desc The bounds of New York City
* @public
* @const
* @type {ol.Extent}
*/
Basemap.EXTENT = [-8266522, 4937867, -8203781, 5000276]

/**
* @desc The center of New York City
* @public
* @const
* @type {ol.Coordinate}
*/
Basemap.CENTER = [-8235252, 4969073]

/**
* @private
* @const
* @type {ol.Extent}
*/
Basemap.LABEL_EXTENT = [-8268000, 4870900, -8005000, 5055500]

/**
* @private
* @const
* @type {ol.Extent}
*/
Basemap.PHOTO_EXTENT = [-8268357, 4937238, -8203099, 5001716]

/* @private
 * @const
 * @type {string}
 */
Basemap.BASE_URL = `https://${nycOl.TILE_HOSTS}/tms/1.0.0/carto/basemap/{z}/{x}/{-y}.jpg`

/**
 * @desc The URLs of the New York City aerial imagery map tiles
 * @private
 * @const
 * @type {Object<string, string>}
 */
Basemap.PHOTO_URLS = {
  '1924': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/1924/{z}/{x}/{-y}.png8`,
  '1951': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/1951/{z}/{x}/{-y}.png8`,
  '1996': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/1996/{z}/{x}/{-y}.png8`,
  '2001-2': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2001-2/{z}/{x}/{-y}.png8`,
  '2004': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2004/{z}/{x}/{-y}.png8`,
  '2006': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2006/{z}/{x}/{-y}.png8`,
  '2008': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2008/{z}/{x}/{-y}.png8`,
  '2010': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2010/{z}/{x}/{-y}.png8`,
  '2012': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2012/{z}/{x}/{-y}.png8`,
  '2014': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2014/{z}/{x}/{-y}.png8`,
  '2016': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2016/{z}/{x}/{-y}.png8`,
  '2018': `https://${nycOl.TILE_HOSTS}/tms/1.0.0/photo/2018/{z}/{x}/{-y}.png8`,
  '2020': 'https://tiles.arcgis.com/tiles/yG5s3afENB5iO9fj/arcgis/rest/services/NYC_Orthos_-_2020/MapServer/tile/{z}/{y}/{x}'
}

/**
 * @desc The URLs of the New York City base map label tiles
 * @private
 * @const
 * @type {Object<string, string>}
 */
Basemap.LABEL_URLS = {
  base: `https://${nycOl.TILE_HOSTS}/tms/1.0.0/carto/label/{z}/{x}/{-y}.png8`,
  photo: `https://${nycOl.TILE_HOSTS}/tms/1.0.0/carto/label-lt/{z}/{x}/{-y}.png8`
}

/**
 * @desc Enumerator for label types
 * @public
 * @enum {string}
 */
Basemap.LabelType = {
  /**
   * @desc Label type for base layer
   */
  BASE: 'base',
  /**
   * @desc Label type for photo layer
   */
  PHOTO: 'photo'
}

/**
 * @desc The URLs of the alternate base map tiles
 * @private
 * @const
 * @type {string}
 */
Basemap.MVT_STYLE_URL = 'https://www.arcgis.com/sharing/rest/content/items/df7862bfd7984baab51ff9df8e214278/resources/styles/root.json?f=json'

export default Basemap