annotation/pointAnnotation.js

const inherit = require('../inherit');
const util = require('../util');
const registerAnnotation = require('../registry').registerAnnotation;
const pointFeature = require('../pointFeature');

const annotation = require('./annotation').annotation;
const annotationState = require('./annotation').state;

/**
 * Point annotation specification.  Extends {@link geo.annotation.spec}.
 *
 * @typedef {object} geo.pointAnnotation.spec
 * @extends geo.annotation.spec
 * @property {geo.geoPosition} [position] A coordinate in map gcs coordinates.
 * @property {geo.geoPosition[]} [coordinates] An array with one coordinate to
 *    use in place of `position`.
 * @property {geo.pointFeature.styleSpec} [style] The style to apply to a
 *    finished point.  This uses styles for {@link geo.pointFeature}.
 * @property {boolean|number} [style.scaled=false] If `false`, the point is not
 *    scaled with zoom level.  If `true`, the radius is based on the zoom level
 *    at first instantiation.  If a number, the radius is used at the `scaled`
 *    zoom level.
 * @property {geo.pointFeature.styleSpec} [editStyle] The style to apply to a
 *    point in edit mode.
 */

/**
 * Point annotation class.
 *
 * @class
 * @alias geo.pointAnnotation
 * @extends geo.annotation
 *
 * @param {geo.pointAnnotation.spec?} [args] Options for the annotation.
 */
var pointAnnotation = function (args) {
  'use strict';
  if (!(this instanceof pointAnnotation)) {
    return new pointAnnotation(args);
  }

  args = util.deepMerge({}, this.constructor.defaults, args);
  args.position = args.position || (args.coordinates ? args.coordinates[0] : undefined);
  delete args.coordinates;
  annotation.call(this, 'point', args);

  var m_this = this;

  /**
   * Get a list of renderable features for this annotation.
   *
   * @returns {array} An array of features.
   */
  this.features = function () {
    var opt = m_this.options(),
        state = m_this.state(),
        features, style, scaleOnZoom;
    switch (state) {
      case annotationState.create:
        features = [];
        break;
      default:
        style = m_this.styleForState(state);
        if (opt.style.scaled || opt.style.scaled === 0) {
          if (opt.style.scaled === true) {
            opt.style.scaled = m_this.layer().map().zoom();
          }
          style = Object.assign({}, style, {
            radius: function () {
              var radius = opt.style.radius,
                  zoom = m_this.layer().map().zoom();
              if (util.isFunction(radius)) {
                radius = radius.apply(m_this, arguments);
              }
              radius *= Math.pow(2, zoom - opt.style.scaled);
              return radius;
            }
          });
          scaleOnZoom = true;
        }
        features = [{
          point: {
            x: opt.position.x,
            y: opt.position.y,
            style: style,
            scaleOnZoom: scaleOnZoom
          }
        }];
        if (state === annotationState.edit) {
          m_this._addEditHandles(
            features, [opt.position],
            {edge: false, center: false, resize: false, rotate: false});
        }
        break;
    }
    return features;
  };

  /**
   * Get and optionally set coordinates associated with this annotation in the
   * map gcs coordinate system.
   *
   * @param {geo.geoPosition[]} [coordinates] An optional array of coordinates
   *  to set.
   * @returns {geo.geoPosition[]} The current array of coordinates.
   */
  this._coordinates = function (coordinates) {
    if (coordinates && coordinates.length >= 1) {
      m_this.options('position', coordinates[0]);
    }
    if (m_this.state() === annotationState.create) {
      return [];
    }
    return [m_this.options('position')];
  };

  this._coordinateOption = 'position';

  /**
   * Handle a mouse click on this annotation.  If the event is processed,
   * evt.handled should be set to `true` to prevent further processing.
   *
   * @param {geo.event} evt The mouse click event.
   * @returns {boolean|string} `true` to update the annotation, `'done'` if
   *    the annotation was completed (changed from create to done state),
   *    `'remove'` if the annotation should be removed, falsy to not update
   *    anything.
   */
  this.mouseClick = function (evt) {
    if (m_this.state() !== annotationState.create) {
      return;
    }
    if (!evt.buttonsDown.left) {
      return;
    }
    evt.handled = true;
    m_this.options('position', evt.mapgcs);
    m_this.state(annotationState.done);
    return 'done';
  };

  /**
   * Return a list of styles that should be preserved in a geojson
   * representation of the annotation.
   *
   * @returns {string[]} A list of style names to store.
   */
  this._geojsonStyles = function () {
    return [
      'fill', 'fillColor', 'fillOpacity', 'radius', 'scaled', 'stroke',
      'strokeColor', 'strokeOpacity', 'strokeWidth'];
  };

  /**
   * Return the coordinates to be stored in a geojson geometry object.
   *
   * @param {string|geo.transform|null} [gcs] `undefined` to use the interface
   *    gcs, `null` to use the map gcs, or any other transform.
   * @returns {array} An array of flattened coordinates in the interface gcs
   *    coordinate system.  `undefined` if this annotation is incomplete.
   */
  this._geojsonCoordinates = function (gcs) {
    var src = m_this.coordinates(gcs);
    if (!src || m_this.state() === annotationState.create || src.length < 1 || src[0] === undefined) {
      return;
    }
    return [src[0].x, src[0].y];
  };

  /**
   * Return the geometry type that is used to store this annotation in geojson.
   *
   * @returns {string} A geojson geometry type.
   */
  this._geojsonGeometryType = function () {
    return 'Point';
  };
};
inherit(pointAnnotation, annotation);

/**
 * This object contains the default options to initialize the class.
 */
pointAnnotation.defaults = Object.assign({}, annotation.defaults, {
  style: {
    fill: true,
    fillColor: {r: 0, g: 1, b: 0},
    fillOpacity: 0.25,
    radius: 10,
    scaled: false,
    stroke: true,
    strokeColor: {r: 0, g: 0, b: 0},
    strokeOpacity: 1,
    strokeWidth: 3
  },
  createStyle: {
    fillColor: {r: 0.3, g: 0.3, b: 0.3},
    fillOpacity: 0.25,
    strokeColor: {r: 0, g: 0, b: 1}
  },
  highlightStyle: {
    fillColor: {r: 0, g: 1, b: 1},
    fillOpacity: 0.5,
    strokeWidth: 5
  }
});

var pointRequiredFeatures = {};
pointRequiredFeatures[pointFeature.capabilities.feature] = true;
registerAnnotation('point', pointAnnotation, pointRequiredFeatures);

module.exports = pointAnnotation;