import {Path} from './Path'; import * as Util from '../../core/Util'; import * as LineUtil from '../../geometry/LineUtil'; import {LatLng, toLatLng} from '../../geo/LatLng'; import {LatLngBounds} from '../../geo/LatLngBounds'; import {Bounds} from '../../geometry/Bounds'; import {Point} from '../../geometry/Point'; /* * @class Polyline * @aka L.Polyline * @inherits Path * * A class for drawing polyline overlays on a map. Extends `Path`. * * @example * * ```js * // create a red polyline from an array of LatLng points * var latlngs = [ * [45.51, -122.68], * [37.77, -122.43], * [34.04, -118.2] * ]; * * var polyline = L.polyline(latlngs, {color: 'red'}).addTo(map); * * // zoom the map to the polyline * map.fitBounds(polyline.getBounds()); * ``` * * You can also pass a multi-dimensional array to represent a `MultiPolyline` shape: * * ```js * // create a red polyline from an array of arrays of LatLng points * var latlngs = [ * [[45.51, -122.68], * [37.77, -122.43], * [34.04, -118.2]], * [[40.78, -73.91], * [41.83, -87.62], * [32.76, -96.72]] * ]; * ``` */ export var Polyline = Path.extend({ // @section // @aka Polyline options options: { // @option smoothFactor: Number = 1.0 // How much to simplify the polyline on each zoom level. More means // better performance and smoother look, and less means more accurate representation. smoothFactor: 1.0, // @option noClip: Boolean = false // Disable polyline clipping. noClip: false }, initialize: function (latlngs, options) { Util.setOptions(this, options); this._setLatLngs(latlngs); }, // @method getLatLngs(): LatLng[] // Returns an array of the points in the path, or nested arrays of points in case of multi-polyline. getLatLngs: function () { return this._latlngs; }, // @method setLatLngs(latlngs: LatLng[]): this // Replaces all the points in the polyline with the given array of geographical points. setLatLngs: function (latlngs) { this._setLatLngs(latlngs); return this.redraw(); }, // @method isEmpty(): Boolean // Returns `true` if the Polyline has no LatLngs. isEmpty: function () { return !this._latlngs.length; }, // @method closestLayerPoint: Point // Returns the point closest to `p` on the Polyline. closestLayerPoint: function (p) { var minDistance = Infinity, minPoint = null, closest = LineUtil._sqClosestPointOnSegment, p1, p2; for (var j = 0, jLen = this._parts.length; j < jLen; j++) { var points = this._parts[j]; for (var i = 1, len = points.length; i < len; i++) { p1 = points[i - 1]; p2 = points[i]; var sqDist = closest(p, p1, p2, true); if (sqDist < minDistance) { minDistance = sqDist; minPoint = closest(p, p1, p2); } } } if (minPoint) { minPoint.distance = Math.sqrt(minDistance); } return minPoint; }, // @method getCenter(): LatLng // Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) of the polyline. getCenter: function () { // throws error when not yet added to map as this center calculation requires projected coordinates if (!this._map) { throw new Error('Must add layer to map before using getCenter()'); } var i, halfDist, segDist, dist, p1, p2, ratio, points = this._rings[0], len = points.length; if (!len) { return null; } // polyline centroid algorithm; only uses the first ring if there are multiple for (i = 0, halfDist = 0; i < len - 1; i++) { halfDist += points[i].distanceTo(points[i + 1]) / 2; } // The line is so small in the current view that all points are on the same pixel. if (halfDist === 0) { return this._map.layerPointToLatLng(points[0]); } for (i = 0, dist = 0; i < len - 1; i++) { p1 = points[i]; p2 = points[i + 1]; segDist = p1.distanceTo(p2); dist += segDist; if (dist > halfDist) { ratio = (dist - halfDist) / segDist; return this._map.layerPointToLatLng([ p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y) ]); } } }, // @method getBounds(): LatLngBounds // Returns the `LatLngBounds` of the path. getBounds: function () { return this._bounds; }, // @method addLatLng(latlng: LatLng, latlngs? LatLng[]): this // Adds a given point to the polyline. By default, adds to the first ring of // the polyline in case of a multi-polyline, but can be overridden by passing // a specific ring as a LatLng array (that you can earlier access with [`getLatLngs`](#polyline-getlatlngs)). addLatLng: function (latlng, latlngs) { latlngs = latlngs || this._defaultShape(); latlng = toLatLng(latlng); latlngs.push(latlng); this._bounds.extend(latlng); return this.redraw(); }, _setLatLngs: function (latlngs) { this._bounds = new LatLngBounds(); this._latlngs = this._convertLatLngs(latlngs); }, _defaultShape: function () { return LineUtil.isFlat(this._latlngs) ? this._latlngs : this._latlngs[0]; }, // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way _convertLatLngs: function (latlngs) { var result = [], flat = LineUtil.isFlat(latlngs); for (var i = 0, len = latlngs.length; i < len; i++) { if (flat) { result[i] = toLatLng(latlngs[i]); this._bounds.extend(result[i]); } else { result[i] = this._convertLatLngs(latlngs[i]); } } return result; }, _project: function () { var pxBounds = new Bounds(); this._rings = []; this._projectLatlngs(this._latlngs, this._rings, pxBounds); var w = this._clickTolerance(), p = new Point(w, w); if (this._bounds.isValid() && pxBounds.isValid()) { pxBounds.min._subtract(p); pxBounds.max._add(p); this._pxBounds = pxBounds; } }, // recursively turns latlngs into a set of rings with projected coordinates _projectLatlngs: function (latlngs, result, projectedBounds) { var flat = latlngs[0] instanceof LatLng, len = latlngs.length, i, ring; if (flat) { ring = []; for (i = 0; i < len; i++) { ring[i] = this._map.latLngToLayerPoint(latlngs[i]); projectedBounds.extend(ring[i]); } result.push(ring); } else { for (i = 0; i < len; i++) { this._projectLatlngs(latlngs[i], result, projectedBounds); } } }, // clip polyline by renderer bounds so that we have less to render for performance _clipPoints: function () { var bounds = this._renderer._bounds; this._parts = []; if (!this._pxBounds || !this._pxBounds.intersects(bounds)) { return; } if (this.options.noClip) { this._parts = this._rings; return; } var parts = this._parts, i, j, k, len, len2, segment, points; for (i = 0, k = 0, len = this._rings.length; i < len; i++) { points = this._rings[i]; for (j = 0, len2 = points.length; j < len2 - 1; j++) { segment = LineUtil.clipSegment(points[j], points[j + 1], bounds, j, true); if (!segment) { continue; } parts[k] = parts[k] || []; parts[k].push(segment[0]); // if segment goes out of screen, or it's the last one, it's the end of the line part if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) { parts[k].push(segment[1]); k++; } } } }, // simplify each clipped part of the polyline for performance _simplifyPoints: function () { var parts = this._parts, tolerance = this.options.smoothFactor; for (var i = 0, len = parts.length; i < len; i++) { parts[i] = LineUtil.simplify(parts[i], tolerance); } }, _update: function () { if (!this._map) { return; } this._clipPoints(); this._simplifyPoints(); this._updatePath(); }, _updatePath: function () { this._renderer._updatePoly(this); }, // Needed by the `Canvas` renderer for interactivity _containsPoint: function (p, closed) { var i, j, k, len, len2, part, w = this._clickTolerance(); if (!this._pxBounds || !this._pxBounds.contains(p)) { return false; } // hit detection for polylines for (i = 0, len = this._parts.length; i < len; i++) { part = this._parts[i]; for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { if (!closed && (j === 0)) { continue; } if (LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) { return true; } } } return false; } }); // @factory L.polyline(latlngs: LatLng[], options?: Polyline options) // Instantiates a polyline object given an array of geographical points and // optionally an options object. You can create a `Polyline` object with // multiple separate lines (`MultiPolyline`) by passing an array of arrays // of geographic points. export function polyline(latlngs, options) { return new Polyline(latlngs, options); } // Retrocompat. Allow plugins to support Leaflet versions before and after 1.1. Polyline._flat = LineUtil._flat;