Draw.Polyline.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /**
  2. * @class L.Draw.Polyline
  3. * @aka Draw.Polyline
  4. * @inherits L.Draw.Feature
  5. */
  6. L.Draw.Polyline = L.Draw.Feature.extend({
  7. statics: {
  8. TYPE: 'polyline'
  9. },
  10. Poly: L.Polyline,
  11. options: {
  12. allowIntersection: true,
  13. repeatMode: false,
  14. drawError: {
  15. color: '#b00b00',
  16. timeout: 2500
  17. },
  18. icon: new L.DivIcon({
  19. iconSize: new L.Point(8, 8),
  20. className: 'leaflet-div-icon leaflet-editing-icon'
  21. }),
  22. touchIcon: new L.DivIcon({
  23. iconSize: new L.Point(20, 20),
  24. className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon'
  25. }),
  26. guidelineDistance: 20,
  27. maxGuideLineLength: 4000,
  28. shapeOptions: {
  29. stroke: true,
  30. color: '#3388ff',
  31. weight: 4,
  32. opacity: 0.5,
  33. fill: false,
  34. clickable: true
  35. },
  36. metric: true, // Whether to use the metric measurement system or imperial
  37. feet: true, // When not metric, to use feet instead of yards for display.
  38. nautic: false, // When not metric, not feet use nautic mile for display
  39. showLength: true, // Whether to display distance in the tooltip
  40. zIndexOffset: 2000 // This should be > than the highest z-index any map layers
  41. },
  42. // @method initialize(): void
  43. initialize: function (map, options) {
  44. // if touch, switch to touch icon
  45. if (L.Browser.touch) {
  46. this.options.icon = this.options.touchIcon;
  47. }
  48. // Need to set this here to ensure the correct message is used.
  49. this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error;
  50. // Merge default drawError options with custom options
  51. if (options && options.drawError) {
  52. options.drawError = L.Util.extend({}, this.options.drawError, options.drawError);
  53. }
  54. // Save the type so super can fire, need to do this as cannot do this.TYPE :(
  55. this.type = L.Draw.Polyline.TYPE;
  56. L.Draw.Feature.prototype.initialize.call(this, map, options);
  57. },
  58. // @method addHooks(): void
  59. // Add listener hooks to this handler
  60. addHooks: function () {
  61. L.Draw.Feature.prototype.addHooks.call(this);
  62. if (this._map) {
  63. this._markers = [];
  64. this._markerGroup = new L.LayerGroup();
  65. this._map.addLayer(this._markerGroup);
  66. this._poly = new L.Polyline([], this.options.shapeOptions);
  67. this._tooltip.updateContent(this._getTooltipText());
  68. // Make a transparent marker that will used to catch click events. These click
  69. // events will create the vertices. We need to do this so we can ensure that
  70. // we can create vertices over other map layers (markers, vector layers). We
  71. // also do not want to trigger any click handlers of objects we are clicking on
  72. // while drawing.
  73. if (!this._mouseMarker) {
  74. this._mouseMarker = L.marker(this._map.getCenter(), {
  75. icon: L.divIcon({
  76. className: 'leaflet-mouse-marker',
  77. iconAnchor: [20, 20],
  78. iconSize: [40, 40]
  79. }),
  80. opacity: 0,
  81. zIndexOffset: this.options.zIndexOffset
  82. });
  83. }
  84. this._mouseMarker
  85. .on('mouseout', this._onMouseOut, this)
  86. .on('mousemove', this._onMouseMove, this) // Necessary to prevent 0.8 stutter
  87. .on('mousedown', this._onMouseDown, this)
  88. .on('mouseup', this._onMouseUp, this) // Necessary for 0.8 compatibility
  89. .addTo(this._map);
  90. this._map
  91. .on('mouseup', this._onMouseUp, this) // Necessary for 0.7 compatibility
  92. .on('mousemove', this._onMouseMove, this)
  93. .on('zoomlevelschange', this._onZoomEnd, this)
  94. .on('touchstart', this._onTouch, this)
  95. .on('zoomend', this._onZoomEnd, this);
  96. }
  97. },
  98. // @method removeHooks(): void
  99. // Remove listener hooks from this handler.
  100. removeHooks: function () {
  101. L.Draw.Feature.prototype.removeHooks.call(this);
  102. this._clearHideErrorTimeout();
  103. this._cleanUpShape();
  104. // remove markers from map
  105. this._map.removeLayer(this._markerGroup);
  106. delete this._markerGroup;
  107. delete this._markers;
  108. this._map.removeLayer(this._poly);
  109. delete this._poly;
  110. this._mouseMarker
  111. .off('mousedown', this._onMouseDown, this)
  112. .off('mouseout', this._onMouseOut, this)
  113. .off('mouseup', this._onMouseUp, this)
  114. .off('mousemove', this._onMouseMove, this);
  115. this._map.removeLayer(this._mouseMarker);
  116. delete this._mouseMarker;
  117. // clean up DOM
  118. this._clearGuides();
  119. this._map
  120. .off('mouseup', this._onMouseUp, this)
  121. .off('mousemove', this._onMouseMove, this)
  122. .off('zoomlevelschange', this._onZoomEnd, this)
  123. .off('zoomend', this._onZoomEnd, this)
  124. .off('touchstart', this._onTouch, this)
  125. .off('click', this._onTouch, this);
  126. },
  127. // @method deleteLastVertex(): void
  128. // Remove the last vertex from the polyline, removes polyline from map if only one point exists.
  129. deleteLastVertex: function () {
  130. if (this._markers.length <= 1) {
  131. return;
  132. }
  133. var lastMarker = this._markers.pop(),
  134. poly = this._poly,
  135. // Replaces .spliceLatLngs()
  136. latlngs = poly.getLatLngs(),
  137. latlng = latlngs.splice(-1, 1)[0];
  138. this._poly.setLatLngs(latlngs);
  139. this._markerGroup.removeLayer(lastMarker);
  140. if (poly.getLatLngs().length < 2) {
  141. this._map.removeLayer(poly);
  142. }
  143. this._vertexChanged(latlng, false);
  144. },
  145. // @method addVertex(): void
  146. // Add a vertex to the end of the polyline
  147. addVertex: function (latlng) {
  148. var markersLength = this._markers.length;
  149. // markersLength must be greater than or equal to 2 before intersections can occur
  150. if (markersLength >= 2 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) {
  151. this._showErrorTooltip();
  152. return;
  153. }
  154. else if (this._errorShown) {
  155. this._hideErrorTooltip();
  156. }
  157. this._markers.push(this._createMarker(latlng));
  158. this._poly.addLatLng(latlng);
  159. if (this._poly.getLatLngs().length === 2) {
  160. this._map.addLayer(this._poly);
  161. }
  162. this._vertexChanged(latlng, true);
  163. },
  164. // @method completeShape(): void
  165. // Closes the polyline between the first and last points
  166. completeShape: function () {
  167. if (this._markers.length <= 1) {
  168. return;
  169. }
  170. this._fireCreatedEvent();
  171. this.disable();
  172. if (this.options.repeatMode) {
  173. this.enable();
  174. }
  175. },
  176. _finishShape: function () {
  177. var latlngs = this._poly._defaultShape ? this._poly._defaultShape() : this._poly.getLatLngs();
  178. var intersects = this._poly.newLatLngIntersects(latlngs[latlngs.length - 1]);
  179. if ((!this.options.allowIntersection && intersects) || !this._shapeIsValid()) {
  180. this._showErrorTooltip();
  181. return;
  182. }
  183. this._fireCreatedEvent();
  184. this.disable();
  185. if (this.options.repeatMode) {
  186. this.enable();
  187. }
  188. },
  189. // Called to verify the shape is valid when the user tries to finish it
  190. // Return false if the shape is not valid
  191. _shapeIsValid: function () {
  192. return true;
  193. },
  194. _onZoomEnd: function () {
  195. if (this._markers !== null) {
  196. this._updateGuide();
  197. }
  198. },
  199. _onMouseMove: function (e) {
  200. var newPos = this._map.mouseEventToLayerPoint(e.originalEvent);
  201. var latlng = this._map.layerPointToLatLng(newPos);
  202. // Save latlng
  203. // should this be moved to _updateGuide() ?
  204. this._currentLatLng = latlng;
  205. this._updateTooltip(latlng);
  206. // Update the guide line
  207. this._updateGuide(newPos);
  208. // Update the mouse marker position
  209. this._mouseMarker.setLatLng(latlng);
  210. L.DomEvent.preventDefault(e.originalEvent);
  211. },
  212. _vertexChanged: function (latlng, added) {
  213. this._map.fire(L.Draw.Event.DRAWVERTEX, { layers: this._markerGroup });
  214. this._updateFinishHandler();
  215. this._updateRunningMeasure(latlng, added);
  216. this._clearGuides();
  217. this._updateTooltip();
  218. },
  219. _onMouseDown: function (e) {
  220. if (!this._clickHandled && !this._touchHandled && !this._disableMarkers) {
  221. this._onMouseMove(e);
  222. this._clickHandled = true;
  223. this._disableNewMarkers();
  224. var originalEvent = e.originalEvent;
  225. var clientX = originalEvent.clientX;
  226. var clientY = originalEvent.clientY;
  227. this._startPoint.call(this, clientX, clientY);
  228. }
  229. },
  230. _startPoint: function (clientX, clientY) {
  231. this._mouseDownOrigin = L.point(clientX, clientY);
  232. },
  233. _onMouseUp: function (e) {
  234. var originalEvent = e.originalEvent;
  235. var clientX = originalEvent.clientX;
  236. var clientY = originalEvent.clientY;
  237. this._endPoint.call(this, clientX, clientY, e);
  238. this._clickHandled = null;
  239. },
  240. _endPoint: function (clientX, clientY, e) {
  241. if (this._mouseDownOrigin) {
  242. var dragCheckDistance = L.point(clientX, clientY)
  243. .distanceTo(this._mouseDownOrigin);
  244. var lastPtDistance = this._calculateFinishDistance(e.latlng);
  245. if (lastPtDistance < 10 && L.Browser.touch) {
  246. this._finishShape();
  247. } else if (Math.abs(dragCheckDistance) < 9 * (window.devicePixelRatio || 1)) {
  248. this.addVertex(e.latlng);
  249. }
  250. this._enableNewMarkers(); // after a short pause, enable new markers
  251. }
  252. this._mouseDownOrigin = null;
  253. },
  254. // ontouch prevented by clickHandled flag because some browsers fire both click/touch events,
  255. // causing unwanted behavior
  256. _onTouch: function (e) {
  257. var originalEvent = e.originalEvent;
  258. var clientX;
  259. var clientY;
  260. if (originalEvent.touches && originalEvent.touches[0] && !this._clickHandled && !this._touchHandled && !this._disableMarkers) {
  261. clientX = originalEvent.touches[0].clientX;
  262. clientY = originalEvent.touches[0].clientY;
  263. this._disableNewMarkers();
  264. this._touchHandled = true;
  265. this._startPoint.call(this, clientX, clientY);
  266. this._endPoint.call(this, clientX, clientY, e);
  267. this._touchHandled = null;
  268. }
  269. this._clickHandled = null;
  270. },
  271. _onMouseOut: function () {
  272. if (this._tooltip) {
  273. this._tooltip._onMouseOut.call(this._tooltip);
  274. }
  275. },
  276. // calculate if we are currently within close enough distance
  277. // of the closing point (first point for shapes, last point for lines)
  278. // this is semi-ugly code but the only reliable way i found to get the job done
  279. // note: calculating point.distanceTo between mouseDownOrigin and last marker did NOT work
  280. _calculateFinishDistance: function (potentialLatLng) {
  281. var lastPtDistance
  282. if (this._markers.length > 0) {
  283. var finishMarker;
  284. if (this.type === L.Draw.Polyline.TYPE) {
  285. finishMarker = this._markers[this._markers.length - 1];
  286. } else if (this.type === L.Draw.Polygon.TYPE) {
  287. finishMarker = this._markers[0];
  288. } else {
  289. return Infinity;
  290. }
  291. var lastMarkerPoint = this._map.latLngToContainerPoint(finishMarker.getLatLng()),
  292. potentialMarker = new L.Marker(potentialLatLng, {
  293. icon: this.options.icon,
  294. zIndexOffset: this.options.zIndexOffset * 2
  295. });
  296. var potentialMarkerPint = this._map.latLngToContainerPoint(potentialMarker.getLatLng());
  297. lastPtDistance = lastMarkerPoint.distanceTo(potentialMarkerPint);
  298. } else {
  299. lastPtDistance = Infinity;
  300. }
  301. return lastPtDistance;
  302. },
  303. _updateFinishHandler: function () {
  304. var markerCount = this._markers.length;
  305. // The last marker should have a click handler to close the polyline
  306. if (markerCount > 1) {
  307. this._markers[markerCount - 1].on('click', this._finishShape, this);
  308. }
  309. // Remove the old marker click handler (as only the last point should close the polyline)
  310. if (markerCount > 2) {
  311. this._markers[markerCount - 2].off('click', this._finishShape, this);
  312. }
  313. },
  314. _createMarker: function (latlng) {
  315. var marker = new L.Marker(latlng, {
  316. icon: this.options.icon,
  317. zIndexOffset: this.options.zIndexOffset * 2
  318. });
  319. this._markerGroup.addLayer(marker);
  320. return marker;
  321. },
  322. _updateGuide: function (newPos) {
  323. var markerCount = this._markers ? this._markers.length : 0;
  324. if (markerCount > 0) {
  325. newPos = newPos || this._map.latLngToLayerPoint(this._currentLatLng);
  326. // draw the guide line
  327. this._clearGuides();
  328. this._drawGuide(
  329. this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()),
  330. newPos
  331. );
  332. }
  333. },
  334. _updateTooltip: function (latLng) {
  335. var text = this._getTooltipText();
  336. if (latLng) {
  337. this._tooltip.updatePosition(latLng);
  338. }
  339. if (!this._errorShown) {
  340. this._tooltip.updateContent(text);
  341. }
  342. },
  343. _drawGuide: function (pointA, pointB) {
  344. var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))),
  345. guidelineDistance = this.options.guidelineDistance,
  346. maxGuideLineLength = this.options.maxGuideLineLength,
  347. // Only draw a guideline with a max length
  348. i = length > maxGuideLineLength ? length - maxGuideLineLength : guidelineDistance,
  349. fraction,
  350. dashPoint,
  351. dash;
  352. //create the guides container if we haven't yet
  353. if (!this._guidesContainer) {
  354. this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane);
  355. }
  356. //draw a dash every GuildeLineDistance
  357. for (; i < length; i += this.options.guidelineDistance) {
  358. //work out fraction along line we are
  359. fraction = i / length;
  360. //calculate new x,y point
  361. dashPoint = {
  362. x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)),
  363. y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y))
  364. };
  365. //add guide dash to guide container
  366. dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer);
  367. dash.style.backgroundColor =
  368. !this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color;
  369. L.DomUtil.setPosition(dash, dashPoint);
  370. }
  371. },
  372. _updateGuideColor: function (color) {
  373. if (this._guidesContainer) {
  374. for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) {
  375. this._guidesContainer.childNodes[i].style.backgroundColor = color;
  376. }
  377. }
  378. },
  379. // removes all child elements (guide dashes) from the guides container
  380. _clearGuides: function () {
  381. if (this._guidesContainer) {
  382. while (this._guidesContainer.firstChild) {
  383. this._guidesContainer.removeChild(this._guidesContainer.firstChild);
  384. }
  385. }
  386. },
  387. _getTooltipText: function () {
  388. var showLength = this.options.showLength,
  389. labelText, distanceStr;
  390. if (L.Browser.touch) {
  391. showLength = false; // if there's a better place to put this, feel free to move it
  392. }
  393. if (this._markers.length === 0) {
  394. labelText = {
  395. text: L.drawLocal.draw.handlers.polyline.tooltip.start
  396. };
  397. } else {
  398. distanceStr = showLength ? this._getMeasurementString() : '';
  399. if (this._markers.length === 1) {
  400. labelText = {
  401. text: L.drawLocal.draw.handlers.polyline.tooltip.cont,
  402. subtext: distanceStr
  403. };
  404. } else {
  405. labelText = {
  406. text: L.drawLocal.draw.handlers.polyline.tooltip.end,
  407. subtext: distanceStr
  408. };
  409. }
  410. }
  411. return labelText;
  412. },
  413. _updateRunningMeasure: function (latlng, added) {
  414. var markersLength = this._markers.length,
  415. previousMarkerIndex, distance;
  416. if (this._markers.length === 1) {
  417. this._measurementRunningTotal = 0;
  418. } else {
  419. previousMarkerIndex = markersLength - (added ? 2 : 1);
  420. distance = latlng.distanceTo(this._markers[previousMarkerIndex].getLatLng());
  421. this._measurementRunningTotal += distance * (added ? 1 : -1);
  422. }
  423. },
  424. _getMeasurementString: function () {
  425. var currentLatLng = this._currentLatLng,
  426. previousLatLng = this._markers[this._markers.length - 1].getLatLng(),
  427. distance;
  428. // calculate the distance from the last fixed point to the mouse position
  429. distance = this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng);
  430. return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic);
  431. },
  432. _showErrorTooltip: function () {
  433. this._errorShown = true;
  434. // Update tooltip
  435. this._tooltip
  436. .showAsError()
  437. .updateContent({ text: this.options.drawError.message });
  438. // Update shape
  439. this._updateGuideColor(this.options.drawError.color);
  440. this._poly.setStyle({ color: this.options.drawError.color });
  441. // Hide the error after 2 seconds
  442. this._clearHideErrorTimeout();
  443. this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorTooltip, this), this.options.drawError.timeout);
  444. },
  445. _hideErrorTooltip: function () {
  446. this._errorShown = false;
  447. this._clearHideErrorTimeout();
  448. // Revert tooltip
  449. this._tooltip
  450. .removeError()
  451. .updateContent(this._getTooltipText());
  452. // Revert shape
  453. this._updateGuideColor(this.options.shapeOptions.color);
  454. this._poly.setStyle({ color: this.options.shapeOptions.color });
  455. },
  456. _clearHideErrorTimeout: function () {
  457. if (this._hideErrorTimeout) {
  458. clearTimeout(this._hideErrorTimeout);
  459. this._hideErrorTimeout = null;
  460. }
  461. },
  462. // disable new markers temporarily;
  463. // this is to prevent duplicated touch/click events in some browsers
  464. _disableNewMarkers: function () {
  465. this._disableMarkers = true;
  466. },
  467. // see _disableNewMarkers
  468. _enableNewMarkers: function () {
  469. setTimeout(function() {
  470. this._disableMarkers = false;
  471. }.bind(this), 50);
  472. },
  473. _cleanUpShape: function () {
  474. if (this._markers.length > 1) {
  475. this._markers[this._markers.length - 1].off('click', this._finishShape, this);
  476. }
  477. },
  478. _fireCreatedEvent: function () {
  479. var poly = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions);
  480. L.Draw.Feature.prototype._fireCreatedEvent.call(this, poly);
  481. }
  482. });