thanthamky's picture
Upload 116 files
f655b1d verified
/**
* @class L.Draw.Polyline
* @aka Draw.Polyline
* @inherits L.Draw.Feature
*/
L.Draw.Polyline = L.Draw.Feature.extend({
statics: {
TYPE: 'polyline'
},
Poly: L.Polyline,
options: {
allowIntersection: true,
repeatMode: false,
drawError: {
color: '#b00b00',
timeout: 2500
},
icon: new L.DivIcon({
iconSize: new L.Point(8, 8),
className: 'leaflet-div-icon leaflet-editing-icon'
}),
touchIcon: new L.DivIcon({
iconSize: new L.Point(20, 20),
className: 'leaflet-div-icon leaflet-editing-icon leaflet-touch-icon'
}),
guidelineDistance: 20,
maxGuideLineLength: 4000,
shapeOptions: {
stroke: true,
color: '#3388ff',
weight: 4,
opacity: 0.5,
fill: false,
clickable: true
},
metric: true, // Whether to use the metric measurement system or imperial
feet: true, // When not metric, to use feet instead of yards for display.
nautic: false, // When not metric, not feet use nautic mile for display
showLength: true, // Whether to display distance in the tooltip
zIndexOffset: 2000 // This should be > than the highest z-index any map layers
},
// @method initialize(): void
initialize: function (map, options) {
// if touch, switch to touch icon
if (L.Browser.touch) {
this.options.icon = this.options.touchIcon;
}
// Need to set this here to ensure the correct message is used.
this.options.drawError.message = L.drawLocal.draw.handlers.polyline.error;
// Merge default drawError options with custom options
if (options && options.drawError) {
options.drawError = L.Util.extend({}, this.options.drawError, options.drawError);
}
// Save the type so super can fire, need to do this as cannot do this.TYPE :(
this.type = L.Draw.Polyline.TYPE;
L.Draw.Feature.prototype.initialize.call(this, map, options);
},
// @method addHooks(): void
// Add listener hooks to this handler
addHooks: function () {
L.Draw.Feature.prototype.addHooks.call(this);
if (this._map) {
this._markers = [];
this._markerGroup = new L.LayerGroup();
this._map.addLayer(this._markerGroup);
this._poly = new L.Polyline([], this.options.shapeOptions);
this._tooltip.updateContent(this._getTooltipText());
// Make a transparent marker that will used to catch click events. These click
// events will create the vertices. We need to do this so we can ensure that
// we can create vertices over other map layers (markers, vector layers). We
// also do not want to trigger any click handlers of objects we are clicking on
// while drawing.
if (!this._mouseMarker) {
this._mouseMarker = L.marker(this._map.getCenter(), {
icon: L.divIcon({
className: 'leaflet-mouse-marker',
iconAnchor: [20, 20],
iconSize: [40, 40]
}),
opacity: 0,
zIndexOffset: this.options.zIndexOffset
});
}
this._mouseMarker
.on('mouseout', this._onMouseOut, this)
.on('mousemove', this._onMouseMove, this) // Necessary to prevent 0.8 stutter
.on('mousedown', this._onMouseDown, this)
.on('mouseup', this._onMouseUp, this) // Necessary for 0.8 compatibility
.addTo(this._map);
this._map
.on('mouseup', this._onMouseUp, this) // Necessary for 0.7 compatibility
.on('mousemove', this._onMouseMove, this)
.on('zoomlevelschange', this._onZoomEnd, this)
.on('touchstart', this._onTouch, this)
.on('zoomend', this._onZoomEnd, this);
}
},
// @method removeHooks(): void
// Remove listener hooks from this handler.
removeHooks: function () {
L.Draw.Feature.prototype.removeHooks.call(this);
this._clearHideErrorTimeout();
this._cleanUpShape();
// remove markers from map
this._map.removeLayer(this._markerGroup);
delete this._markerGroup;
delete this._markers;
this._map.removeLayer(this._poly);
delete this._poly;
this._mouseMarker
.off('mousedown', this._onMouseDown, this)
.off('mouseout', this._onMouseOut, this)
.off('mouseup', this._onMouseUp, this)
.off('mousemove', this._onMouseMove, this);
this._map.removeLayer(this._mouseMarker);
delete this._mouseMarker;
// clean up DOM
this._clearGuides();
this._map
.off('mouseup', this._onMouseUp, this)
.off('mousemove', this._onMouseMove, this)
.off('zoomlevelschange', this._onZoomEnd, this)
.off('zoomend', this._onZoomEnd, this)
.off('touchstart', this._onTouch, this)
.off('click', this._onTouch, this);
},
// @method deleteLastVertex(): void
// Remove the last vertex from the polyline, removes polyline from map if only one point exists.
deleteLastVertex: function () {
if (this._markers.length <= 1) {
return;
}
var lastMarker = this._markers.pop(),
poly = this._poly,
// Replaces .spliceLatLngs()
latlngs = poly.getLatLngs(),
latlng = latlngs.splice(-1, 1)[0];
this._poly.setLatLngs(latlngs);
this._markerGroup.removeLayer(lastMarker);
if (poly.getLatLngs().length < 2) {
this._map.removeLayer(poly);
}
this._vertexChanged(latlng, false);
},
// @method addVertex(): void
// Add a vertex to the end of the polyline
addVertex: function (latlng) {
var markersLength = this._markers.length;
// markersLength must be greater than or equal to 2 before intersections can occur
if (markersLength >= 2 && !this.options.allowIntersection && this._poly.newLatLngIntersects(latlng)) {
this._showErrorTooltip();
return;
}
else if (this._errorShown) {
this._hideErrorTooltip();
}
this._markers.push(this._createMarker(latlng));
this._poly.addLatLng(latlng);
if (this._poly.getLatLngs().length === 2) {
this._map.addLayer(this._poly);
}
this._vertexChanged(latlng, true);
},
// @method completeShape(): void
// Closes the polyline between the first and last points
completeShape: function () {
if (this._markers.length <= 1) {
return;
}
this._fireCreatedEvent();
this.disable();
if (this.options.repeatMode) {
this.enable();
}
},
_finishShape: function () {
var latlngs = this._poly._defaultShape ? this._poly._defaultShape() : this._poly.getLatLngs();
var intersects = this._poly.newLatLngIntersects(latlngs[latlngs.length - 1]);
if ((!this.options.allowIntersection && intersects) || !this._shapeIsValid()) {
this._showErrorTooltip();
return;
}
this._fireCreatedEvent();
this.disable();
if (this.options.repeatMode) {
this.enable();
}
},
// Called to verify the shape is valid when the user tries to finish it
// Return false if the shape is not valid
_shapeIsValid: function () {
return true;
},
_onZoomEnd: function () {
if (this._markers !== null) {
this._updateGuide();
}
},
_onMouseMove: function (e) {
var newPos = this._map.mouseEventToLayerPoint(e.originalEvent);
var latlng = this._map.layerPointToLatLng(newPos);
// Save latlng
// should this be moved to _updateGuide() ?
this._currentLatLng = latlng;
this._updateTooltip(latlng);
// Update the guide line
this._updateGuide(newPos);
// Update the mouse marker position
this._mouseMarker.setLatLng(latlng);
L.DomEvent.preventDefault(e.originalEvent);
},
_vertexChanged: function (latlng, added) {
this._map.fire(L.Draw.Event.DRAWVERTEX, { layers: this._markerGroup });
this._updateFinishHandler();
this._updateRunningMeasure(latlng, added);
this._clearGuides();
this._updateTooltip();
},
_onMouseDown: function (e) {
if (!this._clickHandled && !this._touchHandled && !this._disableMarkers) {
this._onMouseMove(e);
this._clickHandled = true;
this._disableNewMarkers();
var originalEvent = e.originalEvent;
var clientX = originalEvent.clientX;
var clientY = originalEvent.clientY;
this._startPoint.call(this, clientX, clientY);
}
},
_startPoint: function (clientX, clientY) {
this._mouseDownOrigin = L.point(clientX, clientY);
},
_onMouseUp: function (e) {
var originalEvent = e.originalEvent;
var clientX = originalEvent.clientX;
var clientY = originalEvent.clientY;
this._endPoint.call(this, clientX, clientY, e);
this._clickHandled = null;
},
_endPoint: function (clientX, clientY, e) {
if (this._mouseDownOrigin) {
var dragCheckDistance = L.point(clientX, clientY)
.distanceTo(this._mouseDownOrigin);
var lastPtDistance = this._calculateFinishDistance(e.latlng);
if (lastPtDistance < 10 && L.Browser.touch) {
this._finishShape();
} else if (Math.abs(dragCheckDistance) < 9 * (window.devicePixelRatio || 1)) {
this.addVertex(e.latlng);
}
this._enableNewMarkers(); // after a short pause, enable new markers
}
this._mouseDownOrigin = null;
},
// ontouch prevented by clickHandled flag because some browsers fire both click/touch events,
// causing unwanted behavior
_onTouch: function (e) {
var originalEvent = e.originalEvent;
var clientX;
var clientY;
if (originalEvent.touches && originalEvent.touches[0] && !this._clickHandled && !this._touchHandled && !this._disableMarkers) {
clientX = originalEvent.touches[0].clientX;
clientY = originalEvent.touches[0].clientY;
this._disableNewMarkers();
this._touchHandled = true;
this._startPoint.call(this, clientX, clientY);
this._endPoint.call(this, clientX, clientY, e);
this._touchHandled = null;
}
this._clickHandled = null;
},
_onMouseOut: function () {
if (this._tooltip) {
this._tooltip._onMouseOut.call(this._tooltip);
}
},
// calculate if we are currently within close enough distance
// of the closing point (first point for shapes, last point for lines)
// this is semi-ugly code but the only reliable way i found to get the job done
// note: calculating point.distanceTo between mouseDownOrigin and last marker did NOT work
_calculateFinishDistance: function (potentialLatLng) {
var lastPtDistance
if (this._markers.length > 0) {
var finishMarker;
if (this.type === L.Draw.Polyline.TYPE) {
finishMarker = this._markers[this._markers.length - 1];
} else if (this.type === L.Draw.Polygon.TYPE) {
finishMarker = this._markers[0];
} else {
return Infinity;
}
var lastMarkerPoint = this._map.latLngToContainerPoint(finishMarker.getLatLng()),
potentialMarker = new L.Marker(potentialLatLng, {
icon: this.options.icon,
zIndexOffset: this.options.zIndexOffset * 2
});
var potentialMarkerPint = this._map.latLngToContainerPoint(potentialMarker.getLatLng());
lastPtDistance = lastMarkerPoint.distanceTo(potentialMarkerPint);
} else {
lastPtDistance = Infinity;
}
return lastPtDistance;
},
_updateFinishHandler: function () {
var markerCount = this._markers.length;
// The last marker should have a click handler to close the polyline
if (markerCount > 1) {
this._markers[markerCount - 1].on('click', this._finishShape, this);
}
// Remove the old marker click handler (as only the last point should close the polyline)
if (markerCount > 2) {
this._markers[markerCount - 2].off('click', this._finishShape, this);
}
},
_createMarker: function (latlng) {
var marker = new L.Marker(latlng, {
icon: this.options.icon,
zIndexOffset: this.options.zIndexOffset * 2
});
this._markerGroup.addLayer(marker);
return marker;
},
_updateGuide: function (newPos) {
var markerCount = this._markers ? this._markers.length : 0;
if (markerCount > 0) {
newPos = newPos || this._map.latLngToLayerPoint(this._currentLatLng);
// draw the guide line
this._clearGuides();
this._drawGuide(
this._map.latLngToLayerPoint(this._markers[markerCount - 1].getLatLng()),
newPos
);
}
},
_updateTooltip: function (latLng) {
var text = this._getTooltipText();
if (latLng) {
this._tooltip.updatePosition(latLng);
}
if (!this._errorShown) {
this._tooltip.updateContent(text);
}
},
_drawGuide: function (pointA, pointB) {
var length = Math.floor(Math.sqrt(Math.pow((pointB.x - pointA.x), 2) + Math.pow((pointB.y - pointA.y), 2))),
guidelineDistance = this.options.guidelineDistance,
maxGuideLineLength = this.options.maxGuideLineLength,
// Only draw a guideline with a max length
i = length > maxGuideLineLength ? length - maxGuideLineLength : guidelineDistance,
fraction,
dashPoint,
dash;
//create the guides container if we haven't yet
if (!this._guidesContainer) {
this._guidesContainer = L.DomUtil.create('div', 'leaflet-draw-guides', this._overlayPane);
}
//draw a dash every GuildeLineDistance
for (; i < length; i += this.options.guidelineDistance) {
//work out fraction along line we are
fraction = i / length;
//calculate new x,y point
dashPoint = {
x: Math.floor((pointA.x * (1 - fraction)) + (fraction * pointB.x)),
y: Math.floor((pointA.y * (1 - fraction)) + (fraction * pointB.y))
};
//add guide dash to guide container
dash = L.DomUtil.create('div', 'leaflet-draw-guide-dash', this._guidesContainer);
dash.style.backgroundColor =
!this._errorShown ? this.options.shapeOptions.color : this.options.drawError.color;
L.DomUtil.setPosition(dash, dashPoint);
}
},
_updateGuideColor: function (color) {
if (this._guidesContainer) {
for (var i = 0, l = this._guidesContainer.childNodes.length; i < l; i++) {
this._guidesContainer.childNodes[i].style.backgroundColor = color;
}
}
},
// removes all child elements (guide dashes) from the guides container
_clearGuides: function () {
if (this._guidesContainer) {
while (this._guidesContainer.firstChild) {
this._guidesContainer.removeChild(this._guidesContainer.firstChild);
}
}
},
_getTooltipText: function () {
var showLength = this.options.showLength,
labelText, distanceStr;
if (L.Browser.touch) {
showLength = false; // if there's a better place to put this, feel free to move it
}
if (this._markers.length === 0) {
labelText = {
text: L.drawLocal.draw.handlers.polyline.tooltip.start
};
} else {
distanceStr = showLength ? this._getMeasurementString() : '';
if (this._markers.length === 1) {
labelText = {
text: L.drawLocal.draw.handlers.polyline.tooltip.cont,
subtext: distanceStr
};
} else {
labelText = {
text: L.drawLocal.draw.handlers.polyline.tooltip.end,
subtext: distanceStr
};
}
}
return labelText;
},
_updateRunningMeasure: function (latlng, added) {
var markersLength = this._markers.length,
previousMarkerIndex, distance;
if (this._markers.length === 1) {
this._measurementRunningTotal = 0;
} else {
previousMarkerIndex = markersLength - (added ? 2 : 1);
distance = latlng.distanceTo(this._markers[previousMarkerIndex].getLatLng());
this._measurementRunningTotal += distance * (added ? 1 : -1);
}
},
_getMeasurementString: function () {
var currentLatLng = this._currentLatLng,
previousLatLng = this._markers[this._markers.length - 1].getLatLng(),
distance;
// calculate the distance from the last fixed point to the mouse position
distance = this._measurementRunningTotal + currentLatLng.distanceTo(previousLatLng);
return L.GeometryUtil.readableDistance(distance, this.options.metric, this.options.feet, this.options.nautic);
},
_showErrorTooltip: function () {
this._errorShown = true;
// Update tooltip
this._tooltip
.showAsError()
.updateContent({ text: this.options.drawError.message });
// Update shape
this._updateGuideColor(this.options.drawError.color);
this._poly.setStyle({ color: this.options.drawError.color });
// Hide the error after 2 seconds
this._clearHideErrorTimeout();
this._hideErrorTimeout = setTimeout(L.Util.bind(this._hideErrorTooltip, this), this.options.drawError.timeout);
},
_hideErrorTooltip: function () {
this._errorShown = false;
this._clearHideErrorTimeout();
// Revert tooltip
this._tooltip
.removeError()
.updateContent(this._getTooltipText());
// Revert shape
this._updateGuideColor(this.options.shapeOptions.color);
this._poly.setStyle({ color: this.options.shapeOptions.color });
},
_clearHideErrorTimeout: function () {
if (this._hideErrorTimeout) {
clearTimeout(this._hideErrorTimeout);
this._hideErrorTimeout = null;
}
},
// disable new markers temporarily;
// this is to prevent duplicated touch/click events in some browsers
_disableNewMarkers: function () {
this._disableMarkers = true;
},
// see _disableNewMarkers
_enableNewMarkers: function () {
setTimeout(function() {
this._disableMarkers = false;
}.bind(this), 50);
},
_cleanUpShape: function () {
if (this._markers.length > 1) {
this._markers[this._markers.length - 1].off('click', this._finishShape, this);
}
},
_fireCreatedEvent: function () {
var poly = new this.Poly(this._poly.getLatLngs(), this.options.shapeOptions);
L.Draw.Feature.prototype._fireCreatedEvent.call(this, poly);
}
});