2 Copyright (c) 2012, Smartrak, Jacob Toye
3 Leaflet.draw is an open-source JavaScript library for drawing shapes/markers on leaflet powered maps.
4 https://github.com/jacobtoye/Leaflet.draw
6 (function (window, undefined) {
8 L.drawVersion = '0.1.4';
10 L.Util.extend(L.LineUtil, {
11 // Checks to see if two line segments intersect. Does not handle degenerate cases.
12 // http://compgeom.cs.uiuc.edu/~jeffe/teaching/373/notes/x06-sweepline.pdf
13 segmentsIntersect: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2, /*Point*/ p3) {
14 return this._checkCounterclockwise(p, p2, p3) !==
15 this._checkCounterclockwise(p1, p2, p3) &&
16 this._checkCounterclockwise(p, p1, p2) !==
17 this._checkCounterclockwise(p, p1, p3);
20 // check to see if points are in counterclockwise order
21 _checkCounterclockwise: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) {
22 return (p2.y - p.y) * (p1.x - p.x) > (p1.y - p.y) * (p2.x - p.x);
27 // Check to see if this polyline has any linesegments that intersect.
28 // NOTE: does not support detecting intersection for degenerate cases.
29 intersects: function () {
30 var points = this._originalPoints,
31 len = points ? points.length : 0,
34 if (this._tooFewPointsForIntersection()) {
38 for (i = len - 1; i >= 3; i--) {
43 if (this._lineSegmentsIntersectsRange(p, p1, i - 2)) {
51 // Check for intersection if new latlng was added to this polyline.
52 // NOTE: does not support detecting intersection for degenerate cases.
53 newLatLngIntersects: function (latlng, skipFirst) {
54 // Cannot check a polyline for intersecting lats/lngs when not added to the map
59 return this.newPointIntersects(this._map.latLngToLayerPoint(latlng), skipFirst);
62 // Check for intersection if new point was added to this polyline.
63 // newPoint must be a layer point.
64 // NOTE: does not support detecting intersection for degenerate cases.
65 newPointIntersects: function (newPoint, skipFirst) {
66 var points = this._originalPoints,
67 len = points ? points.length : 0,
68 lastPoint = points ? points[len - 1] : null,
69 // The previous previous line segment. Previous line segement doesn't need testing.
72 if (this._tooFewPointsForIntersection(1)) {
76 return this._lineSegmentsIntersectsRange(lastPoint, newPoint, maxIndex, skipFirst ? 1 : 0);
79 // Polylines with 2 sides can only intersect in cases where points are collinear (we don't support detecting these).
80 // Cannot have intersection when < 3 line segments (< 4 points)
81 _tooFewPointsForIntersection: function (extraPoints) {
82 var points = this._originalPoints,
83 len = points ? points.length : 0;
84 // Increment length by extraPoints if present
85 len += extraPoints || 0;
87 return !this._originalPoints || len <= 3;
90 // Checks a line segment intersections with any line segements before its predecessor.
91 // Don't need to check the predecessor as will never intersect.
92 _lineSegmentsIntersectsRange: function (p, p1, maxIndex, minIndex) {
93 var points = this._originalPoints,
96 minIndex = minIndex || 0;
98 // Check all previous line segments (beside the immediately previous) for intersections
99 for (var j = maxIndex; j > minIndex; j--) {
103 if (L.LineUtil.segmentsIntersect(p, p1, p2, p3)) {
113 // Checks a polygon for any intersecting line segments. Ignores holes.
114 intersects: function () {
115 var polylineIntersects,
116 points = this._originalPoints,
117 len, firstPoint, lastPoint, maxIndex;
119 if (this._tooFewPointsForIntersection()) {
123 polylineIntersects = L.Polyline.prototype.intersects.call(this);
125 // If already found an intersection don't need to check for any more.
126 if (polylineIntersects) {
131 firstPoint = points[0];
132 lastPoint = points[len - 1];
135 // Check the line segment between last and first point. Don't need to check the first line segment (minIndex = 1)
136 return this._lineSegmentsIntersectsRange(lastPoint, firstPoint, maxIndex, 1);
140 L.Handler.Draw = L.Handler.extend({
141 includes: L.Mixin.Events,
143 initialize: function (map, options) {
145 this._container = map._container;
146 this._overlayPane = map._panes.overlayPane;
147 this._popupPane = map._panes.popupPane;
149 // Merge default shapeOptions options with custom shapeOptions
150 if (options && options.shapeOptions) {
151 options.shapeOptions = L.Util.extend({}, this.options.shapeOptions, options.shapeOptions);
153 L.Util.extend(this.options, options);
156 enable: function () {
157 this.fire('activated');
158 L.Handler.prototype.enable.call(this);
161 addHooks: function () {
163 L.DomUtil.disableTextSelection();
165 this._label = L.DomUtil.create('div', 'leaflet-draw-label', this._popupPane);
166 this._singleLineLabel = false;
168 L.DomEvent.addListener(this._container, 'keyup', this._cancelDrawing, this);
172 removeHooks: function () {
174 L.DomUtil.enableTextSelection();
176 this._popupPane.removeChild(this._label);
179 L.DomEvent.removeListener(this._container, 'keyup', this._cancelDrawing);
183 _updateLabelText: function (labelText) {
184 labelText.subtext = labelText.subtext || '';
186 // update the vertical position (only if changed)
187 if (labelText.subtext.length === 0 && !this._singleLineLabel) {
188 L.DomUtil.addClass(this._label, 'leaflet-draw-label-single');
189 this._singleLineLabel = true;
191 else if (labelText.subtext.length > 0 && this._singleLineLabel) {
192 L.DomUtil.removeClass(this._label, 'leaflet-draw-label-single');
193 this._singleLineLabel = false;
196 this._label.innerHTML =
197 (labelText.subtext.length > 0 ? '<span class="leaflet-draw-label-subtext">' + labelText.subtext + '</span>' + '<br />' : '') +
198 '<span>' + labelText.text + '</span>';
201 _updateLabelPosition: function (pos) {
202 L.DomUtil.setPosition(this._label, pos);
205 // Cancel drawing when the escape key is pressed
206 _cancelDrawing: function (e) {
207 if (e.keyCode === 27) {
213 L.Polyline.Draw = L.Handler.Draw.extend({
217 allowIntersection: true,
220 message: '<strong>Error:</strong> shape edges cannot cross!',
223 icon: new L.DivIcon({
224 iconSize: new L.Point(8, 8),
225 className: 'leaflet-div-icon leaflet-editing-icon'
227 guidelineDistance: 20,
236 zIndexOffset: 2000 // This should be > than the highest z-index any map layers
239 initialize: function (map, options) {
240 // Merge default drawError options with custom options
241 if (options && options.drawError) {
242 options.drawError = L.Util.extend({}, this.options.drawError, options.drawError);
244 L.Handler.Draw.prototype.initialize.call(this, map, options);
247 addHooks: function () {
248 L.Handler.Draw.prototype.addHooks.call(this);
252 this._markerGroup = new L.LayerGroup();
253 this._map.addLayer(this._markerGroup);
255 this._poly = new L.Polyline([], this.options.shapeOptions);
257 this._updateLabelText(this._getLabelText());
259 // Make a transparent marker that will used to catch click events. These click
260 // events will create the vertices. We need to do this so we can ensure that
261 // we can create vertices over other map layers (markers, vector layers). We
262 // also do not want to trigger any click handlers of objects we are clicking on
264 if (!this._mouseMarker) {
265 this._mouseMarker = L.marker(this._map.getCenter(), {
267 className: 'leaflet-mouse-marker',
268 iconAnchor: [20, 20],
272 zIndexOffset: this.options.zIndexOffset
277 .on('click', this._onClick, this)
280 this._map.on('mousemove', this._onMouseMove, this);
284 removeHooks: function () {
285 L.Handler.Draw.prototype.removeHooks.call(this);
287 this._clearHideErrorTimeout();
289 this._cleanUpShape();
291 // remove markers from map
292 this._map.removeLayer(this._markerGroup);
293 delete this._markerGroup;
294 delete this._markers;
296 this._map.removeLayer(this._poly);
299 this._mouseMarker.off('click', this._onClick);
300 this._map.removeLayer(this._mouseMarker);
301 delete this._mouseMarker;
306 this._map.off('mousemove', this._onMouseMove);
309 _finishShape: function () {
310 if (!this.options.allowIntersection && this._poly.newLatLngIntersects(this._poly.getLatLngs()[0], true)) {
311 this._showErrorLabel();
314 if (!this._shapeIsValid()) {
315 this._showErrorLabel();
321 { poly: new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions) }
326 //Called to verify the shape is valid when the user tries to finish it
327 //Return false if the shape is not valid
328 _shapeIsValid: function () {
332 _onMouseMove: function (e) {
333 var newPos = e.layerPoint,
335 markerCount = this._markers.length;
338 this._currentLatLng = latlng;
341 this._updateLabelPosition(newPos);
343 if (markerCount > 0) {
344 this._updateLabelText(this._getLabelText());
345 // draw the guide line
348 this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()),
353 // Update the mouse marker position
354 this._mouseMarker.setLatLng(latlng);
356 L.DomEvent.preventDefault(e.originalEvent);
359 _onClick: function (e) {
360 var latlng = e.target.getLatLng(),
361 markerCount = this._markers.length;
363 if (markerCount > 0 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) {
364 this._showErrorLabel();
367 else if (this._errorShown) {
368 this._hideErrorLabel();
371 this._markers.push(this._createMarker(latlng));
373 this._poly.addLatLng(latlng);
375 if (this._poly.getLatLngs().length === 2) {
376 this._map.addLayer(this._poly);
379 this._updateMarkerHandler();
381 this._vertexAdded(latlng);
384 _updateMarkerHandler: function () {
385 // The last marker shold have a click handler to close the polyline
386 if (this._markers.length > 1) {
387 this._markers[this._markers.length - 1].on('click', this._finishShape, this);
390 // Remove the old marker click handler (as only the last point should close the polyline)
391 if (this._markers.length > 2) {
392 this._markers[this._markers.length - 2].off('click', this._finishShape);
396 _createMarker: function (latlng) {
397 var marker = new L.Marker(latlng, {
398 icon: this.options.icon,
399 zIndexOffset: this.options.zIndexOffset * 2
402 this._markerGroup.addLayer(marker);
407 _drawGuide: function (pointA, pointB) {
408 var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))),
414 //create the guides container if we haven't yet (TODO: probaly shouldn't do this every time the user starts to draw?)
415 if (!this._guidesContainer) {
416 this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane);
419 //draw a dash every GuildeLineDistance
420 for (i = this.options.guidelineDistance; i < length; i += this.options.guidelineDistance) {
421 //work out fraction along line we are
422 fraction = i / length;
424 //calculate new x,y point
426 x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)),
427 y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y))
430 //add guide dash to guide container
431 dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer);
432 dash.style.backgroundColor =
433 !this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color;
435 L.DomUtil.setPosition(dash, dashPoint);
439 _updateGuideColor: function (color) {
440 if (this._guidesContainer) {
441 for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) {
442 this._guidesContainer.childNodes[i].style.backgroundColor = color;
447 // removes all child elements (guide dashes) from the guides container
448 _clearGuides: function () {
449 if (this._guidesContainer) {
450 while (this._guidesContainer.firstChild) {
451 this._guidesContainer.removeChild(this._guidesContainer.firstChild);
456 _updateLabelText: function (labelText) {
457 if (!this._errorShown) {
458 L.Handler.Draw.prototype._updateLabelText.call(this, labelText);
462 _getLabelText: function () {
467 if (this._markers.length === 0) {
469 text: 'Click to start drawing line.'
472 // calculate the distance from the last fixed point to the mouse position
473 distance = this._measurementRunningTotal + this._currentLatLng.distanceTo(this._markers[this._markers.length - 1].getLatLng());
474 // show metres when distance is < 1km, then show km
475 distanceStr = distance > 1000 ? (distance / 1000).toFixed(2) + ' km' : Math.ceil(distance) + ' m';
477 if (this._markers.length === 1) {
479 text: 'Click to continue drawing line.',
484 text: 'Click last point to finish line.',
492 _showErrorLabel: function () {
493 this._errorShown = true;
496 L.DomUtil.addClass(this._label, 'leaflet-error-draw-label');
497 L.DomUtil.addClass(this._label, 'leaflet-flash-anim');
498 L.Handler.Draw.prototype._updateLabelText.call(this, { text: this.options.drawError.message });
501 this._updateGuideColor(this.options.drawError.color);
502 this._poly.setStyle({ color: this.options.drawError.color });
504 // Hide the error after 2 seconds
505 this._clearHideErrorTimeout();
506 this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorLabel, this), this.options.drawError.timeout);
509 _hideErrorLabel: function () {
510 this._errorShown = false;
512 this._clearHideErrorTimeout();
515 L.DomUtil.removeClass(this._label, 'leaflet-error-draw-label');
516 L.DomUtil.removeClass(this._label, 'leaflet-flash-anim');
517 this._updateLabelText(this._getLabelText());
520 this._updateGuideColor(this.options.shapeOptions.color);
521 this._poly.setStyle({ color: this.options.shapeOptions.color });
524 _clearHideErrorTimeout: function () {
525 if (this._hideErrorTimeout) {
526 clearTimeout(this._hideErrorTimeout);
527 this._hideErrorTimeout = null;
531 _vertexAdded: function (latlng) {
532 if (this._markers.length === 1) {
533 this._measurementRunningTotal = 0;
536 this._measurementRunningTotal +=
537 latlng.distanceTo(this._markers[this._markers.length - 2].getLatLng());
541 _cleanUpShape: function () {
542 if (this._markers.length > 0) {
543 this._markers[this._markers.length - 1].off('click', this._finishShape);
548 L.Polygon.Draw = L.Polyline.Draw.extend({
558 fillColor: null, //same as color by default
564 _updateMarkerHandler: function () {
565 // The first marker shold have a click handler to close the polygon
566 if (this._markers.length === 1) {
567 this._markers[0].on('click', this._finishShape, this);
571 _getLabelText: function () {
573 if (this._markers.length === 0) {
574 text = 'Click to start drawing shape.';
575 } else if (this._markers.length < 3) {
576 text = 'Click to continue drawing shape.';
578 text = 'Click first point to close this shape.';
585 _shapeIsValid: function () {
586 return this._markers.length >= 3;
589 _vertexAdded: function (latlng) {
593 _cleanUpShape: function () {
594 if (this._markers.length > 0) {
595 this._markers[0].off('click', this._finishShape);
602 L.SimpleShape.Draw = L.Handler.Draw.extend({
603 addHooks: function () {
604 L.Handler.Draw.prototype.addHooks.call(this);
606 this._map.dragging.disable();
607 //TODO refactor: move cursor to styles
608 this._container.style.cursor = 'crosshair';
610 this._updateLabelText({ text: this._initialLabelText });
613 .on('mousedown', this._onMouseDown, this)
614 .on('mousemove', this._onMouseMove, this);
619 removeHooks: function () {
620 L.Handler.Draw.prototype.removeHooks.call(this);
622 this._map.dragging.enable();
623 //TODO refactor: move cursor to styles
624 this._container.style.cursor = '';
627 .off('mousedown', this._onMouseDown, this)
628 .off('mousemove', this._onMouseMove, this);
630 L.DomEvent.off(document, 'mouseup', this._onMouseUp);
632 // If the box element doesn't exist they must not have moved the mouse, so don't need to destroy/return
634 this._map.removeLayer(this._shape);
638 this._isDrawing = false;
641 _onMouseDown: function (e) {
642 this._isDrawing = true;
643 this._startLatLng = e.latlng;
646 .on(document, 'mouseup', this._onMouseUp, this)
647 .preventDefault(e.originalEvent);
650 _onMouseMove: function (e) {
651 var layerPoint = e.layerPoint,
654 this._updateLabelPosition(layerPoint);
655 if (this._isDrawing) {
656 this._updateLabelText({ text: 'Release mouse to finish drawing.' });
657 this._drawShape(latlng);
661 _onMouseUp: function (e) {
663 this._fireCreatedEvent();
670 L.Circle.Draw = L.SimpleShape.Draw.extend({
678 fillColor: null, //same as color by default
684 _initialLabelText: 'Click and drag to draw circle.',
686 _drawShape: function (latlng) {
688 this._shape = new L.Circle(this._startLatLng, this._startLatLng.distanceTo(latlng), this.options.shapeOptions);
689 this._map.addLayer(this._shape);
691 this._shape.setRadius(this._startLatLng.distanceTo(latlng));
695 _fireCreatedEvent: function () {
697 'draw:circle-created',
698 { circ: new L.Circle(this._startLatLng, this._shape.getRadius(), this.options.shapeOptions) }
703 L.Rectangle.Draw = L.SimpleShape.Draw.extend({
711 fillColor: null, //same as color by default
717 _initialLabelText: 'Click and drag to draw rectangle.',
719 _drawShape: function (latlng) {
721 this._shape = new L.Rectangle(new L.LatLngBounds(this._startLatLng, latlng), this.options.shapeOptions);
722 this._map.addLayer(this._shape);
724 this._shape.setBounds(new L.LatLngBounds(this._startLatLng, latlng));
728 _fireCreatedEvent: function () {
730 'draw:rectangle-created',
731 { rect: new L.Rectangle(this._shape.getBounds(), this.options.shapeOptions) }
736 L.Marker.Draw = L.Handler.Draw.extend({
738 icon: new L.Icon.Default(),
739 zIndexOffset: 2000 // This should be > than the highest z-index any markers
742 addHooks: function () {
743 L.Handler.Draw.prototype.addHooks.call(this);
746 this._updateLabelText({ text: 'Click map to place marker.' });
747 this._map.on('mousemove', this._onMouseMove, this);
751 removeHooks: function () {
752 L.Handler.Draw.prototype.removeHooks.call(this);
756 this._marker.off('click', this._onClick);
758 .off('click', this._onClick)
759 .removeLayer(this._marker);
763 this._map.off('mousemove', this._onMouseMove);
767 _onMouseMove: function (e) {
768 var newPos = e.layerPoint,
771 this._updateLabelPosition(newPos);
774 this._marker = new L.Marker(latlng, {
775 icon: this.options.icon,
776 zIndexOffset: this.options.zIndexOffset
778 // Bind to both marker and map to make sure we get the click event.
779 this._marker.on('click', this._onClick, this);
781 .on('click', this._onClick, this)
782 .addLayer(this._marker);
785 this._marker.setLatLng(latlng);
789 _onClick: function (e) {
791 'draw:marker-created',
792 { marker: new L.Marker(this._marker.getLatLng(), { icon: this.options.icon }) }
802 L.Control.Draw = L.Control.extend({
807 title: 'Draw a polyline'
810 title: 'Draw a polygon'
813 title: 'Draw a rectangle'
816 title: 'Draw a circle'
819 title: 'Add a marker'
825 initialize: function (options) {
826 L.Util.extend(this.options, options);
829 onAdd: function (map) {
830 var className = 'leaflet-control-draw',
831 container = L.DomUtil.create('div', className);
833 if (this.options.polyline) {
834 this.handlers.polyline = new L.Polyline.Draw(map, this.options.polyline);
836 this.options.polyline.title,
837 className + '-polyline',
839 this.handlers.polyline.enable,
840 this.handlers.polyline
842 this.handlers.polyline.on('activated', this._disableInactiveModes, this);
845 if (this.options.polygon) {
846 this.handlers.polygon = new L.Polygon.Draw(map, this.options.polygon);
848 this.options.polygon.title,
849 className + '-polygon',
851 this.handlers.polygon.enable,
852 this.handlers.polygon
854 this.handlers.polygon.on('activated', this._disableInactiveModes, this);
857 if (this.options.rectangle) {
858 this.handlers.rectangle = new L.Rectangle.Draw(map, this.options.rectangle);
860 this.options.rectangle.title,
861 className + '-rectangle',
863 this.handlers.rectangle.enable,
864 this.handlers.rectangle
866 this.handlers.rectangle.on('activated', this._disableInactiveModes, this);
869 if (this.options.circle) {
870 this.handlers.circle = new L.Circle.Draw(map, this.options.circle);
872 this.options.circle.title,
873 className + '-circle',
875 this.handlers.circle.enable,
878 this.handlers.circle.on('activated', this._disableInactiveModes, this);
881 if (this.options.marker) {
882 this.handlers.marker = new L.Marker.Draw(map, this.options.marker);
884 this.options.marker.title,
885 className + '-marker',
887 this.handlers.marker.enable,
890 this.handlers.marker.on('activated', this._disableInactiveModes, this);
896 _createButton: function (title, className, container, fn, context) {
897 var link = L.DomUtil.create('a', className, container);
902 .on(link, 'click', L.DomEvent.stopPropagation)
903 .on(link, 'mousedown', L.DomEvent.stopPropagation)
904 .on(link, 'dblclick', L.DomEvent.stopPropagation)
905 .on(link, 'click', L.DomEvent.preventDefault)
906 .on(link, 'click', fn, context);
911 // Need to disable the drawing modes if user clicks on another without disabling the current mode
912 _disableInactiveModes: function () {
913 for (var i in this.handlers) {
914 // Check if is a property of this object and is enabled
915 if (this.handlers.hasOwnProperty(i) && this.handlers[i].enabled()) {
916 this.handlers[i].disable();
922 L.Map.addInitHook(function () {
923 if (this.options.drawControl) {
924 this.drawControl = new L.Control.Draw();
925 this.addControl(this.drawControl);