Source: ol/FinderApp.js

/**
 * @module nyc/ol/FinderApp
 */

import $ from 'jquery'
import nyc from 'nyc'
import MapMgr from 'nyc/ol/MapMgr'
import Dialog from 'nyc/Dialog'
import Share from 'nyc/Share'
import Tabs from 'nyc/Tabs'
import Directions from 'nyc/Directions'
import Translate from 'nyc/lang/Translate'
import Goog from 'nyc/lang/Goog'
import Filters from 'nyc/ol/Filters'

/**
 * @desc A class that provides a template for creating basic finder apps
 * @public
 * @class
 */
class FinderApp extends MapMgr {
  /**
   * @desc Create an instance of FinderApp
   * @public
   * @constructor
   * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Constructor options
   */
  constructor(options) {
    $('body').append(FinderApp.HTML).addClass('fnd')
    options.listTarget = '#facilities div'
    options.mapTarget = '#map'
    options.mouseWheelZoom = options.mouseWheelZoom === undefined ? true : options.mouseWheelZoom
    super(options)
    global.finderApp = this
    this.pager.find('h2.info').addClass('screen-reader-only')
    const title = $('<div></div>').html(options.title)
    nyc.noSpaceBarScroll()
    $('#banner').html(title)
    $('#home').attr('title', title.text())
    $('.fnd #home').on('click', () => document.location.reload())
    /**
     * @desc The filters for filtering the facilities
     * @public
     * @member {module:nyc/ol/Filters~Filters}
     */
    this.filters = this.createFilters(options.filterChoiceOptions)
    /**
     * @desc The tabs containing the map, facilities and filters
     * @public
     * @member {module:nyc/Tabs~Tabs}
     */
    this.tabs = this.createTabs(options)
    $('#map').attr('tabindex', -1)
    this.adjustTabs()
    this.showSplash(options.splashOptions)
    new Share({target: '#map'})
    new Goog({
      target: '#map',
      languages: options.languages || Translate.DEFAULT_LANGUAGES,
      button: true
    })
    /**
     * @private
     * @member {module:nyc/Directions~Directions}
     */
    this.directions = null
    /**
     * @private
     * @member {string}
     */
    this.directionsUrl = options.directionsUrl
    /**
     * @private
     * @member {google.maps.TravelMode}
     */
    this.defaultDirectionsMode = options.defaultDirectionsMode
    this.pager.on('change', this.setFacilitiesLabel)
  }
  /**
   * @desc Reset the facilities list
   * @public
   * @method
   * @param {Object} event Event object
   */
  resetList(event) {
    super.resetList(event)
    if (event instanceof Filters) {
      $('#tabs .btns .btn-2').addClass('filtered')
    }
  }
  /**
   * @public
   * @override
   * @method
   * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Constructor options
   * @return {ol.format.Feature} The parent format
   */
  createParentFormat(options) {
    return options.facilityFormat.parentFormat || options.facilityFormat
  }
  /**
   * @public
   * @override
   * @method
   * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Constructor options
   * @param {ol.format.Feature} format The OpenLayers feature format
   * @return {Array<Object<string, fuction>>} Decorations
   */
  createDecorations(options) {
    const format = options.facilityFormat
    let decorations = [MapMgr.FEATURE_DECORATIONS, {app: this}]
    if (format.parentFormat && format.parentFormat.decorations) {
      decorations = decorations.concat(format.parentFormat.decorations)
    }
    if (format.decorations) {
      decorations = decorations.concat(format.decorations)
    }
    if (options.decorations) {
      decorations = decorations.concat(options.decorations)
    }
    return decorations
  }
  /**
   * @desc Centers and zooms to the provided feature
   * @public
   * @method
   * @param {ol.Feature} feature OpenLayers feature
   */
  zoomTo(feature) {
    super.zoomTo(feature)
    if ($('#tabs .btns h2:first-of-type').css('display') !== 'none') {
      this.tabs.open('#map')
    }
  }
  /**
   * @desc Handles geocoded and geolocated events
   * @access protected
   * @method
   * @param {module:nyc/Locator~Locator.Result} location Location
   */
  located(location) {
    super.located(location)
    this.focusFacilities()
  }
  /**
   * @desc Provides directions to the provided facility feature
   * @public
   * @override
   * @method
   * @param {ol.Feature} feature OpenLayers feature
   * @param {JQuery} returnFocus The DOM element that should receive focus when leaving the directions view
   */
  directionsTo(feature, returnFocus) {
    this.directions = this.directions || new Directions({
      url: this.directionsUrl,
      toggle: '#tabs',
      mode: this.defaultDirectionsMode
    })
    const to = feature.getFullAddress()
    const name = feature.getName()
    const from = this.getFromAddr()
    this.directions.directions({
      from: from,
      to: to,
      facility: name,
      origin: this.location,
      destination: {
        name: feature.getName(),
        coordinate: feature.getGeometry().getCoordinates()
      },
      returnFocus: returnFocus
    })
  }
  /**
   * @desc Creates the filters for the facility features
   * @access protected
   * @method
   * @param {Array<module:nyc/ol/Filters~Filters.ChoiceOptions>=} choiceOptions Choice options
   * @return {module:nyc/ol/Filters~Filters} Filters
   */
  createFilters(choiceOptions) {
    if (choiceOptions) {
      const me = this
      $('#filters').empty()
      const apply = $('<button class="apply">Apply</button>')
      const filters = new Filters({
        target: '#filters',
        source: me.source,
        choiceOptions: choiceOptions
      })
      filters.on('change', me.resetList, me)
      $('#filters').append(apply)
      apply.on('click tap keypress', () => {
        me.focusFacilities(true)
      })
      return filters
    }
  }
  /**
   * @desc Creates the tabs for the map, facilities and filters
   * @access protected
   * @method
   * @param {module:nyc/ol/FinderApp~FinderApp.Options} options Options
   * @return {module:nyc/Tabs~Tabs} Tabs
   */
  createTabs(options) {
    const pages = [
      {tab: '#map', title: 'Map'},
      {tab: '#facilities', title: options.facilityTabTitle || 'Facilities'}
    ]
    if (options.splashOptions) {
      $('#tabs').attr('aria-hidden', true)
    }
    if (this.filters) {
      pages.push({tab: '#filters', title: options.filterTabTitle || 'Filters'})
    }
    const tabs = new Tabs({target: '#tabs', tabs: pages})
    tabs.on('change', this.tabChange, this)
    $(window).resize($.proxy(this.adjustTabs, this))
    return tabs
  }
  /**
   * @private
   * @method
   */
  setFacilitiesLabel() {
    $('#facilities>div[role="region"]').attr(
      'aria-label',
      $('#facilities h2.info').html()
    )
  }
  /**
   * @private
   * @method
   * @returns {boolean} The display state
   */
  isMobile() {
    return $('#tabs .btns>h2:first-of-type').css('display') === 'block'
  }
  /**
   * @private
   * @method
   * @returns {module:nyc/Dialog~Dialog.ShowOptions} Dialog options
   */
  mobileDiaOpts() {
    const location = this.location
    const locationName = location.name
    const feature = this.source.sort(location.coordinate)[0]
    const distance = feature.distanceHtml(true).html()
    return {
      message: `<strong>${feature.getName()}</strong><br>
        is located ${distance} from your location<br>
        <strong>${locationName}</strong>`,
      buttonText: [
        `View ${$('#tab-btn-1').html()} list`,
        'View the map'
      ]
    }
  }
  /**
   * @private
   * @method
   * @param {boolean} applyBtn The filter apply button was pressed by a screen reader user
   */
  focusFacilities(applyBtn) {
    const tabs = this.tabs
    this.setFacilitiesLabel()
    if (!applyBtn && this.isMobile()) {
      const options = this.mobileDiaOpts()
      new Dialog({css: 'shw-lst'})
        .yesNo(options).then(showFacilities => {
          if (showFacilities) {
            tabs.open('#facilities')
          }
        })
    } else {
      tabs.open('#facilities')
    }
  }
  /**
   * @private
   * @method
   * @param {module:nyc/Dialog~Dialog.Options} options Dialog options
   */
  showSplash(options) {
    const input = this.locationMgr.search.input
    if (options) {
      options.buttonText = options.buttonText || ['Continue']
      new Dialog({css: 'splash'}).ok(options).then(() => {
        $('#tabs').attr('aria-hidden', false)
        input.focus()
      })
    } else {
      input.focus()
    }
  }
  /**
   * @private
   * @method
   * @return {boolean} True if the tabs fill the screen
   */
  tabsFillScreen() {
    return Math.abs(this.tabs.getContainer().width() - $(window).width()) < 1
  }
  /**
   * @private
   * @method
   */
  adjustTabs() {
    /*
     * when input gets focus screen resizes on android
     * causing input to lose focus when tabs are adjusted
     * so we don't adjust tabs when input has focus
     */
    if ($('#directions').css('display') !== 'block' && !nyc.activeElement().isTextInput) {
      this.tabs.open(this.tabsFillScreen() ? '#map' : '#facilities')
    }
  }
  /**
   * @desc Handles the tab change event
   * @access protected
   * @method
   * @param {module:nyc/Tabs~Tabs} tabs Tabs
   */
  tabChange(tabs) {
    if (!this.tabsFillScreen()) {
      $('#map').attr('aria-hidden', false)
    }
    this.map.setSize([$('#map').width(), $('#map').height()])
  }
}

/**
 * @desc Constructor options for {@link module:nyc/ol/FinderApp~FinderApp}
 * @public
 * @typedef {Object}
 * @property {string} title The app title
 * @property {module:nyc/Dialog~Dialog.Options} splashOptions Content to display as a splash page
 * @property {string} [facilityTabTitle=Facilities] Title for the facilites list
 * @property {string} facilityUrl The URL for the facility features data
 * @property {ol.format.Feature=} facilityFormat The format of the facilities data
 * @property {ol.style.Style} facilityStyle The styling for the facilities layer
 * @property {module:nyc/Search~Search.FeatureSearchOptions|boolean} [facilitySearch=false] Search options for feature searches or true to use default search options
 * @property {string} [filterTabTitle=Filters] Title for the filters tab
 * @property {Array<module:nyc/ol/Filters~Filters.ChoiceOptions>=} filterChoiceOptions Filter definitions
 * @property {string} geoclientUrl The URL for the Geoclient geocoder with approriate keys
 * @property {string} directionsUrl The URL for the Google directions API with approriate keys
 * @property {google.maps.TravelMode} defaultDirectionsMode The mode for Google directions
 * @property {boolean} [mouseWheelZoom=true] Allow mouse wheel map zooming
 */
FinderApp.Options

/**
 * @private
 * @const
 * @type {string}
 */
FinderApp.HTML = '<h1 id="banner" role="banner"></h1>' +
'<a id="home" role="button" href="#">' +
  '<span class="screen-reader-only">Reload page</span>' +
  '<svg xmlns="http://www.w3.org/2000/svg" width="152" height="52">' +
    '<g transform="translate(1.5,0)">' +
      '<polygon points="15.5,1.2 3.1,1.2 0,4.3 0,47.7 3.1,50.8 15.5,50.8 18.6,47.7 18.6,35.3 34.1,50.8 46.6,50.8 49.7,47.7 49.7,4.3 46.6,1.2 34.1,1.2 31,4.3 31,16.7 "/>' +
      '<polygon points="83.8,47.7 83.8,38.4 99.3,22.9 99.3,10.5 99.3,4.3 96.2,1.2 83.8,1.2 80.7,4.3 80.7,10.5 74.5,16.7 68.3,10.5 68.3,4.3 65.2,1.2 52.8,1.2 49.7,4.3 49.7,22.9 65.2,38.4 65.2,47.7 68.3,50.8 80.7,50.8 "/>' +
      '<polygon points="145.9,29.1 130.4,29.1 130.4,32.2 118,32.2 118,19.8 130.4,19.8 130.4,22.9 145.9,22.9 149,19.8 149,10.5 139.7,1.2 108.6,1.2 99.3,10.5 99.3,41.5 108.6,50.8 139.7,50.8 149,41.5 149,32.2 "/>' +
    '</g>' +
  '</svg>' +
'</a>' +
'<div id="map"></div>' +
'<div id="tabs"></div>' +
'<div id="facilities"><div role="region"></div></div>' +
'<div id="filters"></div>'

export default FinderApp