UNPKG

leaflet.freedraw

Version:

Zoopla inspired freehand polygon creation using Leaflet.js.

1,727 lines (1,253 loc) 62.6 kB
(function($window, L, d3, ClipperLib) { "use strict"; /** * @method freeDraw * @param options {Object} * @return {window.L.FreeDraw} */ L.freeDraw = function freeDraw(options) { return new L.FreeDraw(options); }; L.FreeDraw = L.FeatureGroup.extend({ /** * @property map * @type {L.Map|null} */ map: null, /** * @property state * @type {Array} */ state: [], /** * @property defaultPreferences * @type {Object} */ defaultPreferences: {}, /** * @property svg * @type {Object} */ svg: {}, /** * @property element * @type {Object} */ element: {}, /** * Determines whether the user is currently creating a polygon. * * @property creating * @type {Boolean} */ creating: false, /** * Responsible for holding the line function that is required by D3 to draw the line based * on the user's cursor position. * * @property lineFunction * @type {Function} */ lineFunction: function() {}, /** * Responsible for holding an array of latitudinal and longitudinal points for generating * the polygon. * * @property latLngs * @type {Array} */ latLngs: [], /** * @property options * @type {Object} */ options: {}, /** * @property lastNotification * @type {String} */ lastNotification: '', /** * @property markers * @type {L.LayerGroup|null} */ markerLayer: L.layerGroup(), /** * @property hull * @type {Object} */ hull: {}, /** * @property memory * @type {Object} */ memory: {}, /** * @property polygons * @type {Array} */ polygons: [], /** * @property edges * @type {Array} */ edges: [], /** * @property mode * @type {Number} */ mode: 1, /** * @property polygonCount * @type {Number} */ polygonCount: 0, /** * Responsible for holding the coordinates of the user's last cursor position for drawing * the D3 polygon tracing the user's cursor. * * @property fromPoint * @type {Object} */ fromPoint: { x: 0, y: 0 }, /** * @property movingEdge * @type {L.polygon|null} */ movingEdge: null, /** * Responsible for knowing whether a boundary update should be propagated once the user exits * the editing mode. * * @property boundaryUpdateRequired * @type {Boolean} */ boundaryUpdateRequired: false, /** * @property silenced * @type {Boolean} */ silenced: false, /** * @constant RECOUNT_TIMEOUT * @type {Number} */ RECOUNT_TIMEOUT: 1, /** * @method initialize * @param options {Object} * @return {void} */ initialize: function initialize(options) { if (typeof d3 === 'undefined') { // Ensure D3 has been included. L.FreeDraw.Throw('D3 is a required library', 'http://d3js.org/'); } if (typeof ClipperLib === 'undefined') { // Ensure JSClipper has been included. L.FreeDraw.Throw('JSClipper is a required library', 'http://sourceforge.net/p/jsclipper/wiki/Home%206/'); } // Reset all of the properties. this.fromPoint = { x: 0, y: 0 }; this.polygons = []; this.edges = []; this.hull = {}; this._latLngs = []; options = options || {}; this.memory = new L.FreeDraw.Memory(); this.options = new L.FreeDraw.Options(); this.hull = new L.FreeDraw.Hull(); this.element = options.element || null; this.setMode(options.mode || this.mode); this.options.setPathClipperPadding(100); L.FreeDraw.Polygon = L.Polygon.extend({ options: { className: "leaflet-freedraw-polygon" } }); }, /** * @method recreateEdges * @param polygon {Object} * @return {Number|Boolean} */ recreateEdges: function recreateEdges(polygon) { // Remove all of the current edges associated with the polygon. this.edges = this.edges.filter(function filter(edge) { if (edge._freedraw.polygon !== polygon) { return true; } // Physically remove the edge from the DOM. this.map.removeLayer(edge); }.bind(this)); // We can then re-attach the edges based on the current zoom level. return this.createEdges(polygon); }, /** * @method resurrectOrphans * @return {void} */ resurrectOrphans: function resurrectOrphans() { /** * @method recreate * @param polygon {Object} * @return {void} */ var recreate = function recreate(polygon) { setTimeout(function() { this.silently(function silently() { // Reattach the polygon's edges. this.recreateEdges(polygon); }.bind(this)); }.bind(this)); }; var polygons = this.getPolygons(true); polygons.forEach(function forEach(polygon) { if (polygon && polygon._parts[0]) { // If the polygon is currently visible then we'll re-attach its edges for the current // zoom level. recreate.call(this, polygon); } }.bind(this)); setTimeout(function setTimeout() { // Notify everybody of the update if we're using the edges to read the lat/longs. this.notifyBoundaries(); }.bind(this)); }, /** * @method onAdd * @param map {L.Map} * @return {void} */ onAdd: function onAdd(map) { map.on('zoomend', function onZoomEnd() { setTimeout(this.resurrectOrphans.bind(this)); }.bind(this)); // Lazily hook up the options and hull objects. this.map = map; this.mode = this.mode || L.FreeDraw.MODES.VIEW; // Memorise the preferences so we know how to revert. this.defaultPreferences = { dragging: map.dragging._enabled, touchZoom: map.touchZoom._enabled, doubleClickZoom: map.doubleClickZoom._enabled, scrollWheelZoom: map.scrollWheelZoom._enabled }; if (!this.element) { // Define the element D3 will bind to if the user hasn't specified a custom node. this.element = map._container; } // Define the line function for drawing the polygon from the user's mouse pointer. this.lineFunction = d3.svg.line() .x(function pointX(d) { return d.x; }) .y(function pointY(d) { return d.y; }) .interpolate('linear'); // Create a new instance of the D3 free-hand tracer. this.createD3(); // Attach all of the events. this.map.on('mousedown touchstart', this.bindEvents().mouseDown); this.map.on('mousemove touchmove', this.bindEvents().mouseMove); this.map.on('mousedown touchstart', this.bindEvents().mouseUpLeave); // Set the default mode. this.setMode(this.mode); }, /** * @method onRemove * @return {void} */ onRemove: function onRemove() { this._clearPolygons(); this.map.off('mousedown touchstart', this.bindEvents().mouseDown); this.map.off('mousemove touchmove', this.bindEvents().mouseMove); this.map.off('mousedown touchstart', this.bindEvents().mouseUpLeave); }, /** * Responsible for polygon mutation without emitting the markers event. * * @method silently * @param callbackFn {Function} * @return {void} */ silently: function silently(callbackFn) { var silentBefore = this.silenced; this.silenced = true; callbackFn.apply(this); if (!silentBefore) { // Only remove the silence if it wasn't silent before, which prevents against // nesting the `silently` methods inside one another. this.silenced = false; } }, /** * @method cancelAction * @return {void} */ cancelAction: function cancelAction() { this.creating = false; this.movingEdge = null; // Begin to create a brand-new polygon. this.destroyD3().createD3(); }, /** * Update the permissions for what the user can do on the map. * * @method setMapPermissions * @param method {String} * @return {void} */ setMapPermissions: function setMapPermissions(method) { this.map.dragging[method](); this.map.touchZoom[method](); this.map.doubleClickZoom[method](); this.map.scrollWheelZoom[method](); if (method === 'enable') { // Inherit the preferences assigned to the map instance by the developer. if (!this.defaultPreferences.dragging) { this.map.dragging.disable(); } if (!this.defaultPreferences.touchZoom) { this.map.touchZoom.disable(); } if (!this.defaultPreferences.doubleClickZoom) { this.map.doubleClickZoom.disable(); } if (!this.defaultPreferences.scrollWheelZoom) { this.map.scrollWheelZoom.disable(); } } }, /** * @method setMode * @param mode {Number} * @return {void} */ setMode: function setMode(mode) { // Prevent the mode from ever being defined as zero. mode = (mode === 0) ? L.FreeDraw.MODES.VIEW : mode; // Set the current mode and emit the event. this.mode = mode; this.fire('mode', { mode: mode }); if (!this.map) { return; } // Enable or disable dragging according to the current mode. var isCreate = !!(mode & L.FreeDraw.MODES.CREATE), method = !isCreate ? 'enable' : 'disable'; this.map.dragging[method](); if (this.boundaryUpdateRequired && !(this.mode & L.FreeDraw.MODES.EDIT)) { // Share the boundaries if there's an update available and the user is changing the mode // to anything else but the edit mode again. this.notifyBoundaries(); this.boundaryUpdateRequired = false; if (!this.options.memoriseEachEdge) { this.memory.save(this.getPolygons(true)); } } /** * Responsible for applying the necessary classes to the map based on the * current active modes. * * @method defineClasses * @return {void} */ (function defineClasses(modes, map, addClass, removeClass) { removeClass(map, 'mode-create'); removeClass(map, 'mode-edit'); removeClass(map, 'mode-delete'); removeClass(map, 'mode-view'); removeClass(map, 'mode-append'); if (mode & modes.CREATE) { addClass(map, 'mode-create'); } if (mode & modes.EDIT) { addClass(map, 'mode-edit'); } if (mode & modes.DELETE) { addClass(map, 'mode-delete'); } if (mode & modes.VIEW) { addClass(map, 'mode-view'); } if (mode & modes.APPEND) { addClass(map, 'mode-append'); } }(L.FreeDraw.MODES, this.map._container, L.DomUtil.addClass, L.DomUtil.removeClass)); }, /** * @method unsetMode * @param mode {Number} * @return {void} */ unsetMode: function unsetMode(mode) { this.setMode(this.mode ^ mode); }, /** * @method createD3 * @return {void} */ createD3: function createD3() { this.svg = d3.select(this.options.element || this.element).append('svg') .attr('class', this.options.svgClassName) .attr('width', 200).attr('height', 200); }, /** * @method destroyD3 * @return {L.FreeDraw} * @chainable */ destroyD3: function destroyD3() { this.svg.remove(); this.svg = {}; return this; }, /** * @method latLngsToClipperPoints * @param latLngs {L.LatLng[]} * @return {Object} */ latLngsToClipperPoints: function latLngsToClipperPoints(latLngs) { return latLngs.map(function forEach(latLng) { var point = this.map.latLngToLayerPoint(latLng); return { X: point.x, Y: point.y }; }.bind(this)); }, /** * @method clipperPolygonsToLatLngs * @param polygons {Array} * @return {Array} */ clipperPolygonsToLatLngs: function clipperPolygonsToLatLngs(polygons) { var latLngs = []; polygons.forEach(function forEach(polygon) { polygon.forEach(function polygons(point) { point = L.point(point.X, point.Y); var latLng = this.map.layerPointToLatLng(point); latLngs.push(latLng); }.bind(this)); }.bind(this)); return latLngs; }, /** * @method uniqueLatLngs * @param latLngs {L.LatLng[]} * @return {L.LatLng[]} */ uniqueLatLngs: function uniqueLatLngs(latLngs) { var previousLatLngs = [], uniqueValues = []; latLngs.forEach(function forEach(latLng) { var model = JSON.stringify(latLng); if (previousLatLngs.indexOf(model) !== -1) { return; } previousLatLngs.push(model); uniqueValues.push(latLng); }); return uniqueValues; }, /** * @method handlePolygonClick * @param polygon {L.Polygon} * @param event {Object} * @return {void} */ handlePolygonClick: function handlePolygonClick(polygon, event) { var latLngs = [], newPoint = this.map.mouseEventToContainerPoint(event.originalEvent), lowestDistance = Infinity, startPoint = new L.Point(), endPoint = new L.Point(), parts = []; polygon._latlngs.forEach(function forEach(latLng) { // Push each part into the array, because relying on the polygon's "_parts" array // isn't safe since they are removed when parts of the polygon aren't visible. parts.push(this.map.latLngToContainerPoint(latLng)); }.bind(this)); parts.forEach(function forEach(point, index) { var firstPoint = point, secondPoint = parts[index + 1] || parts[0], distance = L.LineUtil.pointToSegmentDistance(newPoint, firstPoint, secondPoint); if (distance < lowestDistance) { // We discovered a distance that possibly should contain the new point! lowestDistance = distance; startPoint = firstPoint; endPoint = secondPoint; } }.bind(this)); parts.forEach(function forEach(point, index) { var nextPoint = parts[index + 1] || parts[0]; if (point === startPoint && nextPoint === endPoint) { latLngs.push(this.map.containerPointToLatLng(point)); latLngs.push(this.map.containerPointToLatLng(newPoint)); return; } latLngs.push(this.map.containerPointToLatLng(point)); }.bind(this)); /** * @constant INNER_DISTANCE * @type {Number} */ var INNER_DISTANCE = this.options.elbowDistance; /** * @method updatePolygon * @return {void} */ var updatePolygon = function updatePolygon() { if (!(this.mode & L.FreeDraw.MODES.APPEND)) { // User hasn't enabled the append mode. return; } // Redraw the polygon based on the newly added lat/long boundaries. polygon.setLatLngs(latLngs); // Recreate the edges for the polygon. this.destroyEdges(polygon); this.createEdges(polygon); }.bind(this); // If the user hasn't enabled delete mode but has the append mode active, then we'll // assume they're always wanting to add an edge. if (this.mode & L.FreeDraw.MODES.APPEND && !(this.mode & L.FreeDraw.MODES.DELETE)) { // Mode has been set to only add new elbows when the user clicks the polygon close // to the boundaries as defined by the `setMaximumDistanceForElbow` method. if (this.options.onlyInDistance && lowestDistance > INNER_DISTANCE) { return; } updatePolygon(); return; } // If the inverse of the aforementioned is true then we'll always delete the polygon. if (this.mode & L.FreeDraw.MODES.DELETE && !(this.mode & L.FreeDraw.MODES.APPEND)) { this.destroyPolygon(polygon); return; } // Otherwise we'll use some logic to detect whether we should delete or add a new elbow. if (lowestDistance > INNER_DISTANCE && this.mode & L.FreeDraw.MODES.DELETE) { // Delete the polygon! this.destroyPolygon(polygon); return; } // Otherwise create a new elbow. updatePolygon(); }, /** * @method createPolygon * @param latLngs {L.LatLng[]} * @param [forceCreation=false] {Boolean} * @return {L.Polygon|Boolean} */ createPolygon: function createPolygon(latLngs, forceCreation) { if (!this.options.multiplePolygons && this.getPolygons(true).length >= 1) { if (this.options.destroyPrevious) { // Destroy the current polygon and then draw the current polygon. this.silently(this.clearPolygons); } else { // Otherwise delete the line because polygon creation has been disallowed, since there's // already one polygon on the map. this.destroyD3().createD3(); return false; } } // Begin to create a brand-new polygon. this.destroyD3().createD3(); if (this.options.simplifyPolygon) { latLngs = function simplifyPolygons() { var points = ClipperLib.Clipper.CleanPolygon(this.latLngsToClipperPoints(latLngs), 1.1), polygons = ClipperLib.Clipper.SimplifyPolygon(points, ClipperLib.PolyFillType.pftNonZero); return this.clipperPolygonsToLatLngs(polygons); }.apply(this); } if (latLngs.length <= this.options.invalidLength) { if (!forceCreation) { return false; } } var className = this.options.polygonClassName, polygon = new L.FreeDraw.Polygon(latLngs, { smoothFactor: this.options.smoothFactor, className: Array.isArray(className) ? className[this.polygons.length] : className }); // Handle the click event on a polygon. polygon.on('click', function onClick(event) { this.handlePolygonClick(polygon, event); }.bind(this)); // Add the polyline to the map, and then find the edges of the polygon. polygon.addTo(this.map); this.polygons.push(polygon); // Attach all of the edges to the polygon. this.createEdges(polygon); /** * Responsible for preventing the re-rendering of the polygon. * * @method clobberLatLngs * @return {void} */ (function clobberLatLngs() { if (this.silenced || !polygon._parts[0]) { return; } polygon._latlngs = []; polygon._parts[0].forEach(function forEach(edge) { // Iterate over all of the parts to update the latLngs to clobber the redrawing upon zooming. polygon._latlngs.push(this.map.layerPointToLatLng(edge)); }.bind(this)); }.bind(this))(); if (this.options.attemptMerge && !this.silenced) { // Merge the polygons if the developer wants to, which at the moment is very experimental! this.mergePolygons(); } if (!this.silenced) { this.notifyBoundaries(); this.memory.save(this.getPolygons(true)); } return polygon; }, /** * @method predefinedPolygon * @param latLngs {L.LatLng[]} * @return {L.Polygon|Boolean} */ predefinedPolygon: function predefinedPolyon(latLngs) { return this.createPolygon(latLngs, true); }, /** * @method undo * @return {void} */ undo: function undo() { this._modifyState('undo'); }, /** * @method redo * @return {void} */ redo: function redo() { this._modifyState('redo'); }, /** * @method _modifyState * @param method {String} * @return {void} * @private */ _modifyState: function _modifyState(method) { // Silently remove all of the polygons, and then obtain the new polygons to be inserted // into the Leaflet map. this.silently(this._clearPolygons.bind(this)); var polygons = this.memory[method](); // Iteratively create each polygon for the new state. polygons.forEach(function forEach(polygon) { this.silently(function silently() { // Create each of the polygons from the current state silently. this.createPolygon(polygon); }.bind(this)); }.bind(this)); // ...And we can finally notify everybody of our new boundaries! this.notifyBoundaries(); }, /** * @method getPolygons * @param [includingOrphans=false] {Boolean} * @return {Array} */ getPolygons: function getPolygons(includingOrphans) { var polygons = []; if (includingOrphans) { if (!this.map) { return []; } /** * Used to identify a node that is a <g> element. * * @constant GROUP_TAG * @type {String} */ var GROUP_TAG = 'G'; for (var layerIndex in this.map._layers) { if (this.map._layers.hasOwnProperty(layerIndex)) { var polygon = this.map._layers[layerIndex]; // Ensure we're dealing with a <g> node that was created by FreeDraw (...an SVG group element). if (polygon._container && polygon._container.tagName.toUpperCase() === GROUP_TAG) { if (polygon instanceof L.FreeDraw.Polygon) { polygons.push(polygon); } } } } } else { this.edges.forEach(function forEach(edge) { if (polygons.indexOf(edge._freedraw.polygon) === -1) { if (edge._freedraw.polygon instanceof L.FreeDraw.Polygon) { polygons.push(edge._freedraw.polygon); } } }.bind(this)); } return polygons; }, /** * @method mergePolygons * @return {void} */ mergePolygons: function mergePolygons() { /** * @method mergePass * @return {void} */ var mergePass = function mergePass() { var allPolygons = this.getPolygons(), allPoints = []; allPolygons.forEach(function forEach(polygon) { allPoints.push(this.latLngsToClipperPoints(polygon._latlngs)); }.bind(this)); var polygons = ClipperLib.Clipper.SimplifyPolygons(allPoints, ClipperLib.PolyFillType.pftNonZero); this.silently(function silently() { this._clearPolygons(); polygons.forEach(function forEach(polygon) { var latLngs = []; polygon.forEach(function forEach(point) { point = L.point(point.X, point.Y); latLngs.push(this.map.layerPointToLatLng(point)); }.bind(this)); // Create the polygon! this.createPolygon(latLngs, true); }.bind(this)); }); }.bind(this); // Perform two merge passes to simplify the polygons. mergePass(); mergePass(); // Trim polygon edges after being modified. this.getPolygons(true).forEach(this.trimPolygonEdges.bind(this)); }, /** * @method destroyPolygon * @param polygon {Object} * @return {void} */ destroyPolygon: function destroyPolygon(polygon) { this.map.removeLayer(polygon); // Remove from the polygons array. var index = this.polygons.indexOf(polygon); this.polygons.splice(index, 1); this.destroyEdges(polygon); if (!this.silenced) { this.notifyBoundaries(); this.memory.save(this.getPolygons(true)); } if (this.options.deleteExitMode && !this.silenced) { // Automatically exit the user from the deletion mode. this.setMode(this.mode ^ L.FreeDraw.MODES.DELETE); } }, /** * @method destroyEdges * @param polygon {Object} * @return {void} */ destroyEdges: function destroyEdges(polygon) { // ...And then remove all of its related edges to prevent memory leaks. this.edges = this.edges.filter(function filter(edge) { if (edge._freedraw.polygon !== polygon) { return true; } // Physically remove the edge from the DOM. this.map.removeLayer(edge); }.bind(this)); }, /** * @method clearPolygons * @return {void} */ clearPolygons: function clearPolygons() { this.silently(this._clearPolygons); if (!this.silenced) { this.notifyBoundaries(); this.memory.save(this.getPolygons(true)); } }, /** * @method _clearPolygons * @return {void} * @private */ _clearPolygons: function _clearPolygons() { this.getPolygons().forEach(function forEach(polygon) { // Iteratively remove each polygon in the DOM. this.destroyPolygon(polygon); }.bind(this)); if (!this.silenced) { this.notifyBoundaries(); } }, /** * @method notifyBoundaries * @return {void} */ notifyBoundaries: function notifyBoundaries() { var latLngs = []; this.getPolygons(true).forEach(function forEach(polygon) { // Ensure the polygon is visible. latLngs.push(polygon._latlngs); }.bind(this)); // Ensure the polygon is closed for the geospatial query. (function createClosedPolygon() { latLngs.forEach(function forEach(latLngGroup) { // Determine if the latitude/longitude values differ for the first and last // lat/long objects. var lastIndex = latLngGroup.length - 1; if (lastIndex && latLngGroup[0] && latLngGroup[lastIndex]) { var latDiffers = latLngGroup[0].lat !== latLngGroup[lastIndex].lat, lngDiffers = latLngGroup[0].lng !== latLngGroup[lastIndex].lng; if (latDiffers || lngDiffers) { // It's not currently a closed polygon for the query, so we'll create the closed // polygon for the geospatial query. latLngGroup.push(latLngGroup[0]); } } }); }.bind(this))(); // Update the polygon count variable. this.polygonCount = latLngs.length; // Ensure the last shared notification differs from the current. var notificationFingerprint = JSON.stringify(latLngs); if (this.lastNotification === notificationFingerprint) { return; } // Save the notification for the next time. this.lastNotification = notificationFingerprint; // Invoke the user passed method for specifying latitude/longitudes. this.fire('markers', { latLngs: latLngs }); // Perform another count at a later date to account for polygons that may have been removed // due to their polygon areas being too small. setTimeout(this.emitPolygonCount.bind(this), this.RECOUNT_TIMEOUT); }, /** * @method emitPolygonCount * @return {void} */ emitPolygonCount: function emitPolygonCount() { /** * @constant EMPTY_PATH * @type {String} */ var EMPTY_PATH = 'M0 0'; // Perform a recount on the polygon count, since some may be removed because of their // areas being too small. var polygons = this.getPolygons(true), allEmpty = polygons.every(function every(polygon) { var path = polygon._container.lastChild.getAttribute('d').trim(); return path === EMPTY_PATH; }); if (allEmpty) { this.silently(function silently() { // Silently remove all of the polygons because they are empty. this._clearPolygons(); this.fire('markers', { latLngs: [] }); this.fire('count', { count: this.polygonCount }); }.bind(this)); this.polygonCount = 0; polygons.length = 0; } if (polygons.length !== this.polygonCount) { // If the size differs then we'll assign the new length, and emit the count event. this.polygonCount = polygons.length; this.fire('count', { count: this.polygonCount }); } }, /** * @method createEdges * @param polygon {L.polygon} * @return {Number|Boolean} */ createEdges: function createEdges(polygon) { /** * Responsible for getting the parts based on the original lat/longs. * * @method originalLatLngs * @param polygon {Object} * @return {Array} */ var originalLatLngs = function originalLatLngs(polygon) { if (!polygon._parts[0]) { // We don't care for polygons that are not in the viewport. return []; } return polygon._latlngs.map(function map(latLng) { return this.map.latLngToLayerPoint(latLng); }.bind(this)); }.bind(this); var parts = this.uniqueLatLngs(originalLatLngs(polygon)), indexOf = this.polygons.indexOf(polygon), edgeCount = 0; if (!parts) { return false; } parts.forEach(function forEach(point) { // Leaflet creates elbows in the polygon, which we need to utilise to add the // points for modifying its shape. var edge = new L.DivIcon({ className: Array.isArray(this.options.iconClassName) ? this.options.iconClassName[indexOf] : this.options.iconClassName }), latLng = this.map.layerPointToLatLng(point); edge = L.marker(latLng, { icon: edge }).addTo(this.map); // Setup the freedraw object with the meta data. edge._freedraw = { polygon: polygon, polygonId: polygon['_leaflet_id'], latLng: edge._latlng }; this.edges.push(edge); edgeCount++; edge.on('mousedown touchstart', function onMouseDown(event) { event.originalEvent.preventDefault(); event.originalEvent.stopPropagation(); this.movingEdge = event.target; }.bind(this)); }.bind(this)); return edgeCount; }, /** * @method updatePolygonEdge * @param edge {Object} * @param positionX {Number} * @param positionY {Number} * @return {void} */ updatePolygonEdge: function updatePolygon(edge, positionX, positionY) { var updatedLatLng = this.map.containerPointToLatLng(new L.Point(positionX, positionY)); // Update the latitude and longitude for both the Leaflet.js model, and the FreeDraw model. edge.setLatLng(updatedLatLng); edge._freedraw.latLng = updatedLatLng; var allEdges = []; // Fetch all of the edges in the group based on the polygon. var edges = this.edges.filter(function filter(currentEdge) { allEdges.push(currentEdge); return currentEdge._freedraw.polygon === edge._freedraw.polygon; }); // Update the edge object. this.edges = allEdges; var updatedLatLngs = []; edges.forEach(function forEach(marker) { updatedLatLngs.push(marker.getLatLng()); }); // Update the latitude and longitude values. edge._freedraw.polygon.setLatLngs(updatedLatLngs); edge._freedraw.polygon.redraw(); }, /** * @method bindEvents * @return {Object} */ bindEvents: function bindEvents() { if (this.events) { return this.events; } this.events = { /** * @method mouseDown * @param {Object} event * @return {void} */ mouseDown: function onMouseDown(event) { /** * Used for determining if the user clicked with the right mouse button. * * @constant RIGHT_CLICK * @type {Number} */ var RIGHT_CLICK = 2; if (event.originalEvent.button === RIGHT_CLICK) { return; } var originalEvent = event.originalEvent; if (!this.options.disablePropagation) { originalEvent.stopPropagation(); } originalEvent.preventDefault(); this.latLngs = []; this.fromPoint = this.map.latLngToContainerPoint(event.latlng); if (this.mode & L.FreeDraw.MODES.CREATE) { // Place the user in create polygon mode. this.creating = true; this.setMapPermissions('disable'); } }.bind(this), /** * @method mouseMove * @param {Object} event * @return {void} */ mouseMove: function onMouseMove(event) { var originalEvent = event.originalEvent; if (this.movingEdge) { // User is in fact modifying the shape of the polygon. this._editMouseMove(event); return; } if (!this.creating) { // We can't do anything else if the user is not in the process of creating a brand-new // polygon. return; } this._createMouseMove(originalEvent); }.bind(this), /** * @method mouseUpLeave * @return {void} */ mouseUpLeave: function mouseUpLeave() { /** * @method completeAction * @return {void} */ var completeAction = function completeAction() { if (this.movingEdge) { if (!this.options.boundariesAfterEdit) { // Notify of a boundary update immediately after editing one edge. this.notifyBoundaries(); } else { // Change the option so that the boundaries will be invoked once the edit mode // has been exited. this.boundaryUpdateRequired = true; } // Recreate the polygon boundaries because we may have straight edges now. this.trimPolygonEdges(this.movingEdge._freedraw.polygon); this.mergePolygons(); this.movingEdge = null; if (this.options.memoriseEachEdge) { this.memory.save(this.getPolygons(true)); } setTimeout(this.emitPolygonCount.bind(this), this.RECOUNT_TIMEOUT); return; } this._createMouseUp(); }.bind(this); this.map.on('mouseup touchend', completeAction); var element = $window.document.getElementsByTagName('body')[0]; element.onmouseleave = completeAction; }.bind(this) }; return this.events; }, /** * @method _editMouseMove * @param event {Object} * @return {void} * @private */ _editMouseMove: function _editMouseMove(event) { var pointModel = this.map.latLngToContainerPoint(event.latlng); // Modify the position of the marker on the map based on the user's mouse position. var styleDeclaration = this.movingEdge._icon.style; styleDeclaration[L.DomUtil.TRANSFORM] = pointModel; // Update the polygon's shape in real-time as the user drags their cursor. this.updatePolygonEdge(this.movingEdge, pointModel.x, pointModel.y); }, /** * @method trimPolygonEdges * @param polygon {L.Polygon} * @return {void} */ trimPolygonEdges: function trimPolygonEdges(polygon) { var latLngs = []; if (!polygon || polygon._parts.length === 0 || !polygon._parts[0]) { return; } polygon._parts[0].forEach(function forEach(point) { latLngs.push(this.map.layerPointToLatLng(point)); }.bind(this)); polygon.setLatLngs(latLngs); polygon.redraw(); this.destroyEdges(polygon); this.createEdges(polygon); }, /** * @method _createMouseMove * @param event {Object} * @return {void} * @private */ _createMouseMove: function _createMouseMove(event) { // Resolve the pixel point to the latitudinal and longitudinal equivalent. var point = this.map.mouseEventToContainerPoint(event), latLng = this.map.containerPointToLatLng(point); // Line data that is fed into the D3 line function we defined earlier. var lineData = [this.fromPoint, { x: point.x, y: point.y }]; // Draw SVG line based on the last movement of the mouse's position. this.svg.append('path').classed('drawing-line', true).attr('d', this.lineFunction(lineData)) .attr('stroke', '#D7217E').attr('stroke-width', 2).attr('fill', 'none'); // Take the pointer's position from the event for the next invocation of the mouse move event, // and store the resolved latitudinal and longitudinal values. this.fromPoint.x = point.x; this.fromPoint.y = point.y; this.latLngs.push(latLng); }, /** * @method _createMouseUp * @return {void} * @private */ _createMouseUp: function _createMouseUp() { if (!this.creating) { return; } // User has finished creating their polygon! this.creating = false; if (this.latLngs.length <= 2) { // User has failed to drag their cursor enough to create a valid polygon. return; } if (this.options.hullAlgorithm) { // Use the defined hull algorithm. this.hull.setMap(this.map); var latLngs = this.hull[this.options.hullAlgorithm](this.latLngs); } // Required for joining the two ends of the free-hand drawing to create a closed polygon. this.latLngs.push(this.latLngs[0]); // Physically draw the Leaflet generated polygon. var polygon = this.createPolygon(latLngs || this.latLngs); if (!polygon) { this.setMapPermissions('enable'); return; } this.latLngs = []; if (this.options.createExitMode) { // Automatically exit the user from the creation mode. this.setMode(this.mode ^ L.FreeDraw.MODES.CREATE); this.setMapPermissions('enable'); } } }); /** * @constant MODES * @type {Object} */ L.FreeDraw.MODES = { NONE: 0, VIEW: 1, CREATE: 2, EDIT: 4, DELETE: 8, APPEND: 16, EDIT_APPEND: 4 | 16, ALL: 1 | 2 | 4 | 8 | 16 }; /** * @method Throw * @param message {String} * @param [path=''] {String} * @return {void} */ L.FreeDraw.Throw = function ThrowException(message, path) { if (path) { if (path.substr(0, 7) === 'http://' || path.substr(0, 8) === 'https://') { // Use developer supplied full URL since we've received a FQDN. $window.console.error(path); } else { // Output a link for a more informative message in the EXCEPTIONS.md. $window.console.error('See: https://github.com/Wildhoney/Leaflet.FreeDraw/blob/master/EXCEPTIONS.md#' + path); } } // ..And then output the thrown exception. throw "Leaflet.FreeDraw: " + message + "."; }; })(window, window.L, window.d3, window.ClipperLib); (function() { "use strict"; /** * @module FreeDraw * @submodule Hull * @author Adam Timberlake * @link https://github.com/Wildhoney/Leaflet.FreeDraw * @constructor */ L.FreeDraw.Hull = function FreeDrawHull() {}; /** * @property prototype * @type {Object} */ L.FreeDraw.Hull.prototype = { /** * @property map * @type {L.Map|null} */ map: null, /** * @method setMap * @param map {L.Map} * @return {void} */ setMap: function setMap(map) { this.map = map; }, /** * @link https://github.com/brian3kb/graham_scan_js * @method brian3kbGrahamScan * @param latLngs {L.LatLng[]} * @return {L.LatLng[]} */ brian3kbGrahamScan: function brian3kbGrahamScan(latLngs) { var convexHull = new ConvexHullGrahamScan(), resolvedPoints = [], points = [], hullLatLngs = []; latLngs.forEach(function forEach(latLng) { // Resolve each latitude/longitude to its respective container point. points.push(this.map.latLngToLayerPoint(latLng)); }.bind(this)); points.forEach(function forEach(point) { convexHull.addPoint(point.x, point.y); }.bind(this)); var hullPoints = convexHull.getHull(); hullPoints.forEach(function forEach(hullPoint) { resolvedPoints.push(L.point(hullPoint.x, hullPoint.y)); }.bind(this)); // Create an unbroken polygon. resolvedPoints.push(resolvedPoints[0]); resolvedPoints.forEach(function forEach(point) { hullLatLngs.push(this.map.layerPointToLatLng(point)); }.bind(this)); return hullLatLngs; }, /** * @link https://github.com/Wildhoney/ConcaveHull * @method wildhoneyConcaveHull * @param latLngs {L.LatLng[]} * @return {L.LatLng[]} */ wildhoneyConcaveHull: function wildhoneyConcaveHull(latLngs) { latLngs.push(latLngs[0]); return new ConcaveHull(latLngs).getLatLngs(); } } }()); (function() { "use strict"; /** * @module FreeDraw * @submodule