<!-- Copyright (c) 2015 Google Inc. All rights reserved. -->

<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../google-apis/google-maps-api.html">
<link rel="import" href="google-map-point.html">

<!--
The `google-map-poly` element represents a series of connected line segments (aka a polyline) which
may also be closed to form a polygon (provided there are at least three points). It is used as a
child of `google-map` and will contain at least two `google-map-point` child elements.

<b>Example</b>—a simple line:

    <google-map latitude="37.77493" longitude="-122.41942">
      <google-map-poly>
        <google-map-point latitude="37.77493" longitude="-122.41942"></google-map-point>
        <google-map-point latitude="38.77493" longitude="-123.41942"></google-map-point>
      </google-map-poly>
    </google-map>

<b>Example</b>—a semi-translucent blue triangle:

    <google-map latitude="37.77493" longitude="-122.41942">
      <google-map-poly closed fill-color="blue" fill-opacity=".5">
        <google-map-point latitude="36.77493" longitude="-121.41942"></google-map-point>
        <google-map-point latitude="38.77493" longitude="-122.41942"></google-map-point>
        <google-map-point latitude="36.77493" longitude="-123.41942"></google-map-point>
      </google-map-poly>
    </google-map>
-->

<dom-module id="google-map-poly">
  <style>
    :host {
      display: none;
    }
  </style>
  <template>
    <content id="points" select="google-map-point"></content>
  </template>
</polymer-element>

<script>
  Polymer({
    is: 'google-map-poly',

    /**
     * Fired when the `path` property is built based on child `google-map-point` elements, either
     * initially or when they are changed.
     * @event google-map-poly-path-built
     * @param {MVCArray.<LatLng>} path The poly path.
     */
    /**
     * Fired when the user finishes adding vertices to the poly. The host component can use the
     * provided path to rebuild its list of points.
     * @event google-map-poly-path-updated
     * @param {MVCArray.<LatLng>} path The poly path.
     */
    /**
     * Fired when the DOM `click` event is fired on the poly. Requires the clickEvents attribute to
     * be true.
     * @event google-map-poly-click
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired when the DOM `dblclick` event is fired on the poly. Requires the clickEvents attribute
     * to be true.
     * @event google-map-poly-dblclick
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired repeatedly while the user drags the poly. Requires the dragEvents attribute to be true.
     * @event google-map-poly-drag
     * @param {google.maps.MouseEvent} event The mouse event.
     */
    /**
     * Fired when the user stops dragging the poly. Requires the dragEvents attribute to be true.
     * @event google-map-poly-dragend
     * @param {google.maps.MouseEvent} event The mouse event.
     */
    /**
     * Fired when the user starts dragging the poly. Requires the dragEvents attribute to be true.
     * @event google-map-poly-dragstart
     * @param {google.maps.MouseEvent} event The mouse event.
     */
    /**
     * Fired when the DOM `mousedown` event is fired on the poly. Requires the mouseEvents attribute
     * to be true.
     * @event google-map-poly-mousedown
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired when the DOM `mousemove` event is fired on the poly. Requires the mouseEvents attribute
     * to be true.
     * @event google-map-poly-mousemove
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired on poly mouseout. Requires the mouseEvents attribute to be true.
     * @event google-map-poly-mouseout
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired on poly mouseover. Requires the mouseEvents attribute to be true.
     * @event google-map-poly-mouseover
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired when the DOM `mouseup` event is fired on the poly. Requires the mouseEvents attribute
     * to be true.
     * @event google-map-poly-mouseup
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    /**
     * Fired when the poly is right-clicked on. Requires the clickEvents attribute to be true.
     * @event google-map-poly-rightclick
     * @param {google.maps.PolyMouseEvent} event The poly event.
     */
    properties: {
      /**
       * A Google Maps polyline or polygon object (depending on value of "closed" attribute).
       * @type google.maps.Polyline|google.maps.Polygon
       */
      poly: {
        type: Object,
        readOnly: true
      },

      /**
       * An array of the Google Maps LatLng objects that define the poly shape.
       * @type MVCArray.<LatLng>
       */
      path: {
        type: Object,
        readOnly: true
      },

      /**
       * The Google map object.
       * @type google.maps.Map
       */
      map: {
        type: Object,
        observer: '_mapChanged'
      },

      /**
       * When true, the poly will generate mouse events.
       */
      clickable: {
        type: Boolean,
        value: false,
        observer: '_clickableChanged'
      },

      /**
       * When true, the google-map-poly-*click events will be automatically registered.
       */
       clickEvents: {
         type: Boolean,
         value: false,
         observer: '_clickEventsChanged'
       },

      /**
       * When true, the path will be closed by connecting the last point to the first one and
       * treating the poly as a polygon.
       */
      closed: {
        type: Boolean,
        value: false,
        observer: '_closedChanged'
      },

      /**
       * When true, the poly may be dragged to a new position.
       */
      draggable: {
        type: Boolean,
        value: false,
      },

      /**
       * When true, the google-map-poly-drag* events will be automatically registered.
       */
      dragEvents: {
        type: Boolean,
        value: false,
        observer: '_dragEventsChanged'
      },

      /**
       * When true, the poly's vertices may be individually moved or new ones added.
       */
      editable: {
        type: Boolean,
        value: false,
        observer: '_editableChanged'
      },

      /**
       * When true, indicates that the user has begun editing the poly path (adding vertices).
       */
      editing: {
        type: Boolean,
        value: false,
        notify: true,
        readOnly: true
      },

      /**
       * If the path is closed, the polygon fill color. All CSS3 colors are supported except for
       * extended named colors.
       */
      fillColor: {
        type: String,
        value: '',
        observer: '_fillColorChanged'
      },

      /**
       * If the path is closed, the polygon fill opacity (between 0.0 and 1.0).
       */
      fillOpacity: {
        type: Number,
        value: 0,
        observer: '_fillOpacityChanged'
      },

      /**
       * When true, the poly's edges are interpreted as geodesic and will follow the curvature of
       * the Earth. When not set, the poly's edges are rendered as straight lines in screen space.
       * Note that the poly of a geodesic poly may appear to change when dragged, as the dimensions
       * are maintained relative to the surface of the earth.
       */
      geodesic: {
        type: Boolean,
        value: false,
        observer: '_geodesicChanged'
      },

      /**
       * If the path is not closed, the icons to be rendered along the polyline.
       */
      icons: {
        type: Array,
        value: null,
        observer: '_iconsChanged'
      },

      /**
       * When true, the google-map-poly-mouse* events will be automatically registered.
       */
       mouseEvents: {
         type: Boolean,
         value: false,
         observer: '_mouseEventsChanged'
       },

      /**
       * The color to draw the poly's stroke with. All CSS3 colors are supported except for extended
       * named colors.
       */
      strokeColor: {
        type: String,
        value: 'black',
        observer: '_strokeColorChanged'
      },

      /**
       * The stroke opacity (between 0.0 and 1.0).
       */
      strokeOpacity: {
        type: Number,
        value: 1,
        observer: '_strokeOpacityChanged'
      },

      /**
       * The stroke position (center, inside, or outside).
       */
      strokePosition: {
        type: String,
        value: 'center',
        observer: '_strokePositionChanged'
      },

      /**
       * The stroke width in pixels.
       */
      strokeWeight: {
        type: Number,
        value: 3,
        observer: '_strokeWeightChanged'
      },

      /**
       * The Z-index relative to other objects on the map.
       */
      zIndex: {
        type: Number,
        value: 0,
        observer: '_zIndexChanged'
      }
    },

    // Lifecycle event handlers.

    detached: function() {
      this.poly.setMap(null);
      if (this._pointsObserver) {
        this._pointsObserver.disconnect();
        this._pointsObserver = null;
      }
      for (var name in this._listeners) {
        this._clearListener(name);
      }
    },

    attached: function() {
      // If element is added back to DOM, put it back on the map.
      this.poly && this.poly.setMap(this.map);
    },

    // Attribute/property change watchers.

    attributeChanged: function(attrName, oldVal, newVal) {
      if (!this.poly) {
        return;
      }

      // Cannot use *Changed watchers for native properties.
      switch (attrName) {
        case 'hidden':
          this.poly.setVisible(!this.hidden);
          break;
        case 'draggable':
          this.poly.setDraggable(this.draggable);
          break;
      }
    },

    _clickableChanged: function() {
      this.poly && this.poly.set('clickable', this.clickable);
    },

    _clickEventsChanged: function() {
      if (this.poly) {
        if (this.clickEvents) {
          this._forwardEvent('click');
          this._forwardEvent('dblclick');
          this._forwardEvent('rightclick');
        } else {
          this._clearListener('click');
          this._clearListener('dblclick');
          this._clearListener('rightclick');
        }
      }
    },

    _closedChanged: function() {
      this._mapChanged();
    },

    _dragEventsChanged: function() {
      if (this.poly) {
        if (this.clickEvents) {
          this._forwardEvent('drag');
          this._forwardEvent('dragend');
          this._forwardEvent('dragstart');
        } else {
          this._clearListener('drag');
          this._clearListener('dragend');
          this._clearListener('dragstart');
        }
      }
    },

    _editableChanged: function() {
      this.poly && this.poly.setEditable(this.editable);
    },

    _fillColorChanged: function() {
      this.poly && this.poly.set('fillColor', this.fillColor);
    },

    _fillOpacityChanged: function() {
      this.poly && this.poly.set('fillOpacity', this.fillOpacity);
    },

    _geodesicChanged: function() {
      this.poly && this.poly.set('geodesic', this.geodesic);
    },

    _iconsChanged: function() {
      this.poly && this.poly.set('icons', this.icons);
    },

    _mapChanged: function() {
      // Poly will be rebuilt, so disconnect existing one from old map and listeners.
      if (this.poly) {
        this.poly.setMap(null);
        google.maps.event.clearInstanceListeners(this.poly);
      }

      if (this.map && this.map instanceof google.maps.Map) {
        this._createPoly();
      }
    },

    _mouseEventsChanged: function() {
      if (this.poly) {
        if (this.mouseEvents) {
          this._forwardEvent('mousedown');
          this._forwardEvent('mousemove');
          this._forwardEvent('mouseout');
          this._forwardEvent('mouseover');
          this._forwardEvent('mouseup');
        } else {
          this._clearListener('mousedown');
          this._clearListener('mousemove');
          this._clearListener('mouseout');
          this._clearListener('mouseover');
          this._clearListener('mouseup');
        }
      }
    },

    _strokeColorChanged: function() {
      this.poly && this.poly.set('strokeColor', this.strokeColor);
    },

    _strokeOpacityChanged: function() {
      this.poly && this.poly.set('strokeOpacity', this.strokeOpacity);
    },

    _strokePositionChanged: function() {
      this.poly && this.poly.set('strokePosition', this._convertStrokePosition());
    },

    _strokeWeightChanged: function() {
      this.poly && this.poly.set('strokeWeight', this.strokeWeight);
    },

    _zIndexChanged: function() {
      this.poly && this.poly.set('zIndex', this.zIndex);
    },

    // Helper logic.

    _buildPathFromPoints: function() {
      this._points = Array.prototype.slice.call(Polymer.dom(this.$.points).getDistributedNodes());

      // Build path from current points (ignoring vertex insertions while doing so).
      this._building = true;
      this.path.clear();
      for (var i = 0, point; point = this._points[i]; ++i) {
        this.path.push(point.getPosition());
      }
      this._building = false;

      this.fire('google-map-poly-path-built', this.path);

      // Watch for future updates.
      if (this._pointsObserver) {
        return;
      }
      this._pointsObserver = new MutationObserver(this._buildPathFromPoints.bind(this));
      this._pointsObserver.observe(this, {
        childList: true
      });
    },

    _clearListener: function(name) {
      if (this._listeners[name]) {
        google.maps.event.removeListener(this._listeners[name]);
        this._listeners[name] = null;
      }
    },

    _convertStrokePosition: function() {
      return google.maps.StrokePosition && this.strokePosition ?
          google.maps.StrokePosition[this.strokePosition.toUpperCase()] : 0;
    },

    _createPoly: function() {
      // Build poly's path and register mutation listeners on first creation.
      if (!this.path) {
        this._setPath(new google.maps.MVCArray());
        google.maps.event.addListener(this.path, 'insert_at', this._startEditing.bind(this));
        google.maps.event.addListener(this.path, 'set_at', this._updatePoint.bind(this));
        this._buildPathFromPoints();
      }

      var options = {
        clickable: this.clickable || this.draggable,  // draggable must be clickable to work.
        draggable: this.draggable,
        editable: this.editable,
        geodesic: this.geodesic,
        map: this.map,
        path: this.path,
        strokeColor: this.strokeColor,
        strokeOpacity: this.strokeOpacity,
        strokePosition: this._convertStrokePosition(),
        strokeWeight: this.strokeWeight,
        visible: !this.hidden,
        zIndex: this.zIndex
      };

      if (this.closed) {
        options.fillColor = this.fillColor;
        options.fillOpacity = this.fillOpacity;
        this._setPoly(new google.maps.Polygon(options));
      } else {
        options.icons = this.icons;
        this._setPoly(new google.maps.Polyline(options));
      }

      this._listeners = {};
    },

    _forwardEvent: function(name) {
      this._listeners[name] = google.maps.event.addListener(this.poly, name, function(event) {
        this.fire('google-map-poly-' + name, event);
      }.bind(this));
    },

    _startEditing: function(index) {
      if (this._building) {
        // Ignore changes while building path.
        return;
      }

      // Signal start of editing when first vertex inserted, end when map clicked.
      if (!this.editing) {
        this._setEditing(true);
        // The poly path and google-map-point elements lose sync once the user starts adding points,
        // so invalidate the _points array.
        this._points = null;
        google.maps.event.addListenerOnce(this.map, 'click', function() {
          this._setEditing(false);
          this.fire('google-map-poly-path-updated', this.path);
        }.bind(this));
      }
    },

    _updatePoint: function(index, vertex) {
      // Ignore changes if path is out of sync with google-map-point elements.
      if (!this._points) {
        return;
      }

      // Update existing point so bound properties are updated. too.
      this._points[index].latitude = vertex.lat();
      this._points[index].longitude = vertex.lng();
    }
  });
</script>