Source: ol/Basemap.js

/**
 * @module nyc/ol/Basemap
 */

import $ from 'jquery'

import nyc from 'nyc'
import nycOl from 'nyc/ol'
import BasemapHelper from 'nyc/BasemapHelper'
import LocalStorage from 'nyc/ol/LocalStorage'

import Map from 'ol/Map'
// import OlPluggableMap from 'ol/PluggableMap'

// import CompositeMapRenderer from 'ol/renderer/Composite'
// import CanvasTileLayerRenderer from 'ol/renderer/canvas/TileLayer'
// import CanvasVectorLayerRenderer from 'ol/renderer/canvas/VectorLayer'
// import CanvasVectorTileLayerRenderer from 'ol/renderer/canvas/VectorTileLayer'

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 OlView from 'ol/View'
import OlSourceXYZ from 'ol/source/XYZ'
import OlLayerTile from 'ol/layer/Tile'
import GeoJSON from 'ol/format/GeoJSON'

const proj4 = nyc.proj4

/**
 * @desc Class that provides an ol.Map with base layers, labels, and drag-and-drop
 * @public
 * @class
 * @extends ol.PluggableMap
 * @mixes module:nyc/BasemapHelper~BasemapHelper
 * @see http://openlayers.org/en/latest/apidoc/module-ol_PluggableMap.html
 */
class Basemap extends Map {
  /**
   * @desc Create an instance of Basemap
   * @public
   * @constructor
   * @param {olx.MapOptions} options Constructor options
   * @param {number} [preload=0] Preload option for base layer
   */
  constructor(options, preload) {
    const viewProvided = options.view instanceof OlView
    Basemap.setupView(options)
    if (!options.interactions) {
      options.interactions = [
        new DoubleClickZoom(),
        new DragPan(),
        new PinchZoom(),
        new KeyboardPan(),
        new KeyboardZoom(),
        new MouseWheelZoom(),
        new DragZoom()
      ]
    }
    super(options)
    nyc.mixin(this, [BasemapHelper])
    /**
     * @private
     * @member {number}
     */
    this.latestPhoto = 0
    /**
     * @private
     * @member {OlLayerTile}
     */
    this.base = null
    /**
     * @private
     * @member {OlLayerTile}
     */
    this.altBasemap = null
    /**
     * @private
     * @member {Object<string, OlLayerTile>}
     */
    this.labels = {}
    /**
     * @private
     * @member {Object<string, OlLayerTile>}
     */
    this.photos = {}
    /**
     * @private
     * @member {module:nyc/LocalStorage~LocalStorage}
     */
    this.storage = new LocalStorage()
    this.setupLayers(options, preload)
    this.defaultExtent(viewProvided)
    this.hookupEvents(this.getTargetElement())
  }
  // createRenderer() {
  //   const renderer = new CompositeMapRenderer(this)
  //   renderer.registerLayerRenderers([
  //     CanvasTileLayerRenderer,
  //     CanvasVectorLayerRenderer,
  //     CanvasVectorTileLayerRenderer
  //   ])
  //   return renderer
  // }
  /**
   * @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 Show photo layer
   * @public
   * @override
   * @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)
    this.showLabels('photo')
  }
  /**
   * @desc Hide photo layer
   * @public
   * @override
   * @method
   */
  hidePhoto() {
    this[this.altBasemap ? 'altBasemap' : 'base'].setVisible(true)
    this.showLabels(BasemapHelper.LabelType.BASE)
    Object.entries(this.photos).forEach(([year, layer]) => {
      layer.setVisible(false)
    })
  }
  /**
   * @desc Show the specified label layer
   * @public
   * @override
   * @method
   * @param {nyc.Basemap.BaseLayers} labelType The label type to show
   */
  showLabels(labelType) {
    if (!this.altBasemap) {
      this.labels.base.setVisible(labelType === BasemapHelper.LabelType.BASE)
    }
    this.labels.photo.setVisible(labelType === BasemapHelper.LabelType.PHOTO)
  }
  /**
   * @desc Returns the base layers
   * @public
   * @override
   * @method
   * @return {nyc.Basemap.BaseLayers} Base layers
   */
  getBaseLayers() {
    return {
      altBasemap: this.altBasemap,
      base: this.base,
      labels: this.labels,
      photos: this.photos
    }
  }
  /**
   * @desc Get the storage used for loading and saving data
   * @access protected
   * @override
   * @method
   * @return {nyc.ol.storage.Local} srorage
   */
  getStorage() {
    return this.storage
  }
  /**
   * @private
   * @method
   * @param {boolean} viewProvided was the view provided?
   */
  defaultExtent(viewProvided) {
    if (!viewProvided) {
      this.getView().fit(Basemap.EXTENT, this.getSize())
    }
  }
  /**
   * @private
   * @method
   * @param {Object} options Options
   * @param {number} [preload=0] Preload
   */
  setupLayers(options, preload) {
    const projection = 'EPSG:3857';
    if (options.altBasemap) {
      this.altBasemap = new OlLayerTile({
        source: new OlSourceXYZ({
          urls: Basemap.ALT_BASEMAP_URLS
        })
      })
      this.addLayer(this.altBasemap)
    } else {
      this.base = new OlLayerTile({
        extent: this.layerExtent(Basemap.UNIVERSE_EXTENT, options.view),
        source: new OlSourceXYZ({
          url: Basemap.BASE_URL,
          projection: projection
        }),
        preload: preload || 0
      })
      this.addLayer(this.base)
    }

    Object.entries(Basemap.LABEL_URLS).forEach(([labelType, url]) => {
      if (!this.altBasemap || labelType === 'photo') {
        this.labels[labelType] = new OlLayerTile({
          extent: this.layerExtent(Basemap.LABEL_EXTENT, options.view),
          source: new OlSourceXYZ({
            url: url,
            projection: projection
          }),
          zIndex: 1000,
          visible: labelType === BasemapHelper.LabelType.BASE
        })

        this.addLayer(this.labels[labelType])
      }
    })

    Object.entries(Basemap.PHOTO_URLS).forEach(([year, url]) => {
      const photo = new OlLayerTile({
        extent: this.layerExtent(Basemap.PHOTO_EXTENT, options.view),
        source: new OlSourceXYZ({
          url: url,
          projection: projection
        }),
        visible: false
      })
      if ((year.split('-')[0] * 1) > this.latestPhoto) {
        this.latestPhoto = year
      }
      photo.set('name', year)
      this.addLayer(photo)
      photo.on('change:visible', $.proxy(this.photoChange, 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(BasemapHelper.LabelType.PHOTO)
        return true
      }
    })
    if (!isPhoto) {
      this.showLabels(BasemapHelper.LabelType.BASE)
    }
  }
}

/**
 * @private
 * @static
 * @method
 * @param {Object} options Options
 */
Basemap.setupView = (options) => {
  if (!(options.view instanceof OlView)) {
    options.view = new OlView({
      center: Basemap.CENTER,
      minZoom: 8,
      maxZoom: 21,
      zoom: 8,
      constrainRotation: 1
    })
  }
}

/**
 * @desc The URL of the New York City base map tiles
 * @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`
}

/**
 * @desc The URLs of the alternate base map tiles
 * @private
 * @const
 * @type {Array<string>}
 */
Basemap.ALT_BASEMAP_URLS = [
  'https://cartodb-basemaps-1.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
  'https://cartodb-basemaps-2.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
  'https://cartodb-basemaps-3.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
  'https://cartodb-basemaps-4.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png'
]

/**
 * @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`
}

/**
 * @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]

export default Basemap