tf-leaflet-freedraw
Version:
Zoopla inspired freehand polygon creation using Leaflet.js.
438 lines (354 loc) • 12.9 kB
JavaScript
import 'core-js';
import 'regenerator-runtime/runtime';
import { noConflict, FeatureGroup, Point, DomEvent } from 'leaflet';
import { select } from 'd3-selection';
import { line, curveMonotoneX } from 'd3-shape';
import Set from 'es6-set';
import WeakMap from 'es6-weak-map';
import Symbol from 'es6-symbol';
import { updateFor } from './helpers/Layer';
import { createFor, removeFor, clearFor } from './helpers/Polygon';
import { CREATE, EDIT, DELETE, APPEND, EDIT_APPEND, NONE, ALL, modeFor } from './helpers/Flags';
import simplifyPolygon from './helpers/Simplify';
// Preventing binding to the `window`.
noConflict();
/**
* @constant polygons
* @type {WeakMap}
*/
export const polygons = new WeakMap();
/**
* @constant defaultOptions
* @type {Object}
*/
export const defaultOptions = {
mode: ALL,
smoothFactor: 0.3,
elbowDistance: 10,
simplifyFactor: 1.1,
mergePolygons: true,
concavePolygon: true,
maximumPolygons: Infinity,
notifyAfterEditExit: false,
leaveModeAfterCreate: false,
strokeWidth: 2
};
/**
* @constant instanceKey
* @type {Symbol}
*/
export const instanceKey = Symbol('freedraw/instance');
/**
* @constant modesKey
* @type {Symbol}
*/
export const modesKey = Symbol('freedraw/modes');
/**
* @constant notifyDeferredKey
* @type {Symbol}
*/
export const notifyDeferredKey = Symbol('freedraw/notify-deferred');
/**
* @constant edgesKey
* @type {Symbol}
*/
export const edgesKey = Symbol('freedraw/edges');
/**
* @constant cancelKey
* @type {Symbol}
*/
const cancelKey = Symbol('freedraw/cancel');
export default class FreeDraw extends FeatureGroup {
/**
* @constructor
* @param {Object} [options = {}]
* @return {void}
*/
constructor(options = defaultOptions) {
super();
this.options = { ...defaultOptions, ...options };
this.mouseDownHandler = undefined;
}
/**
* @method onAdd
* @param {Object} map
* @return {void}
*/
onAdd(map) {
// Memorise the map instance.
this.map = map;
map._container.style['-webkit-user-select'] = 'none';
// Attach the cancel function and the instance to the map.
map[cancelKey] = () => {};
map[instanceKey] = this;
map[notifyDeferredKey] = () => {};
// Setup the dependency injection for simplifying the polygon.
map.simplifyPolygon = simplifyPolygon;
// Add the item to the map.
polygons.set(map, new Set());
// Set the initial mode.
modeFor(map, this.options.mode, this.options);
// Instantiate the SVG layer that sits on top of the map.
const svg = this.svg = select(map._container).append('svg')
.classed('free-draw', true).attr('width', '100%').attr('height', '100%')
.style('pointer-events', 'none').style('z-index', '1001').style('position', 'relative');
// Set the mouse events.
this.listenForEvents(map, svg, this.options);
}
/**
* @method onRemove
* @param {Object} map
* @return {void}
*/
onRemove(map) {
// Remove the item from the map.
polygons.delete(map);
// Remove the SVG layer.
this.svg.remove();
// Remove the appendages from the map container.
delete map[cancelKey];
delete map[instanceKey];
delete map.simplifyPolygon;
if (!!this.mouseDownHandler) {
map.off('mousedown', this.mouseDownHandler);
map.off('touchstart', this.mouseDownHandler);
}
}
/**
* @method create
* @param {LatLng[]} latLngs
* @param {Object} [options = { concavePolygon: false }]
* @return {Object}
*/
create(latLngs, options = { concavePolygon: false }) {
const created = createFor(this.map, latLngs, { ...this.options, ...options });
updateFor(this.map, 'create');
return created;
}
/**
* @method remove
* @param {Object} polygon
* @return {void}
*/
remove(polygon) {
polygon ? removeFor(this.map, polygon) : super.remove();
updateFor(this.map, 'remove');
}
/**
* @method clear
* @return {void}
*/
clear() {
clearFor(this.map);
updateFor(this.map, 'clear');
}
/**
* @method setMode
* @param {Number} [mode = null]
* @return {Number}
*/
mode(mode = null) {
// Set mode when passed `mode` is numeric, and then yield the current mode.
typeof mode === 'number' && modeFor(this.map, mode, this.options);
return this.map[modesKey];
}
/**
* @method size
* @return {Number}
*/
size() {
return polygons.get(this.map).size;
}
/**
* @method all
* @return {Array}
*/
all() {
return Array.from(polygons.get(this.map));
}
/**
* @method cancel
* @return {void}
*/
cancel() {
this.map[cancelKey]();
}
_simulateEvent(type, e) {
var simulatedEvent = document.createEvent('MouseEvents');
simulatedEvent._simulated = true;
e.target._simulatedClick = true;
simulatedEvent.initMouseEvent(
type, true, true, window, 1,
e.screenX, e.screenY,
e.clientX, e.clientY,
false, false, false, false, 0, null);
e.target.dispatchEvent(simulatedEvent);
}
/**
* @method listenForEvents
* @param {Object} map
* @param {Object} svg
* @param {Object} options
* @return {void}
*/
listenForEvents(map, svg, options) {
let latLngs = new Set();
let lineIterator;
/**
* @method mouseMove
* @param {Object} event
* @return {void}
*/
const mouseMove = (event) => {
// const x = performance.now();
// console.log('mouseMove');
// Resolve the pixel point to the latitudinal and longitudinal equivalent.
let e = event.originalEvent || event;
if (e.touches) {
e = e.touches[0];
}
const point = map.mouseEventToContainerPoint(e);
// Push each lat/lng value into the points set.
latLngs.add(map.containerPointToLatLng(point));
// Invoke the generator by passing in the starting point for the path.
const svgpoint = new Point(point.x, point.y);
lineIterator(svgpoint);
// const y = performance.now();
// console.log('mouseMoveEnd: ' + (y-x).toString());
};
/**
* @method mouseUp
* @param {Boolean} [create = true]
* @return {Function}
*/
const mouseUp = (event, create = true) => {
// Ignore pointer cancel events (touch interfaces fire these randomly)
if (event.type === 'pointercancel') {
return;
}
// const x = performance.now();
// console.log('mouseup');
// Remove the ability to invoke `cancel`.
map[cancelKey] = () => {};
// Stop listening to the events.
map.off('mouseup', mouseUp);
map.off('mousemove', mouseMove);
DomEvent.off(this.map._container, 'touchmove', mouseMove, this);
DomEvent.off(this.map._container, 'touchend', mouseUp, this);
'body' in document && document.body.removeEventListener('mouseleave', mouseUp);
// Clear the SVG canvas.
svg.selectAll('*').remove();
if (create) {
// ...And finally if we have any lat/lngs in our set then we can attempt to
// create the polygon.
latLngs.size && createFor(map, Array.from(latLngs), options);
// Finally invoke the callback for the polygon regions.
updateFor(map, 'create');
// Exit the `CREATE` mode if the options permit it.
options.leaveModeAfterCreate && this.mode(this.mode() ^ CREATE);
}
// const y = performance.now();
// console.log('mouseupEnd: ' + (y-x).toString());
};
const touchStart = (event) => {
if (!(map[modesKey] & CREATE)) {
// Polygons can only be created when the mode includes create.
return;
}
DomEvent.preventDefault(event);
DomEvent.on(this.map._container, 'touchmove', mouseMove, this);
DomEvent.on(this.map._container, 'touchend', mouseUp, this);
this._simulateEvent('mousedown', event)
}
/**
* @method mouseDown
* @param {Object} event
* @return {void}
*/
const mouseDown = (event) => {
// const x = performance.now();
// console.log('mousedown');
DomEvent.preventDefault(event);
if (!(map[modesKey] & CREATE)) {
// Polygons can only be created when the mode includes create.
return;
}
// Depending on leaflet version and plugins the touchstart event can fire a mousedown event with coords 0,0
// if that happens... just ignore it
if (event.clientX === 0 && event.clientY === 0) {
return;
}
/**
* @constant latLngs
* @type {Set}
*/
latLngs = new Set();
lineIterator = undefined;
if (!!event.latlng) {
// Create the line iterator and move it to its first `yield` point, passing in the start point
// from the mouse down event.
const point = map.latLngToContainerPoint(event.latlng);
lineIterator = this.createPath(svg, point, options.strokeWidth);
} else {
const point = map.mouseEventToContainerPoint(event);
lineIterator = this.createPath(svg, point, options.strokeWidth);
}
// if we are not a touch interface register mouse events
if (!event.touches) {
map.on('mousemove', mouseMove);
map.on('mouseup', mouseUp);
}
'body' in document && document.body.addEventListener('mouseleave', mouseUp);
// Setup the function to invoke when `cancel` has been invoked.
map[cancelKey] = () => mouseUp({}, false);
// const y = performance.now();
// console.log('mousedownEnd: ' + (y-x).toString());
};
this.mouseDownHandler = mouseDown;
DomEvent.on(map._container, 'mousedown', mouseDown, this);
DomEvent.on(map._container, 'touchstart', touchStart, this);
}
/**
* @method createPath
* @param {Object} svg
* @param {Point} fromPoint
* @param {Number} strokeWidth
* @return {void}
*/
createPath(svg, fromPoint, strokeWidth) {
let lastPoint = fromPoint;
const lineFunction = line().curve(curveMonotoneX).x(d => d.x).y(d => d.y);
return toPoint => {
const lineData = [ lastPoint, toPoint ];
lastPoint = toPoint;
// Draw SVG line based on the last movement of the mouse's position.
svg.append('path')
.classed(this.options.lineclass || 'leaflet-line', true)
.attr('d', lineFunction(lineData))
.attr('fill', this.options.fill || 'none')
.attr('fill-opacity', this.options.fillOpacity || 1)
.attr('stroke', this.options.stroke || 'black')
.attr('stroke-width', strokeWidth);
};
}
}
/**
* @method freeDraw
* @return {Object}
*/
export const freeDraw = options => {
return new FreeDraw(options);
};
export { CREATE, EDIT, DELETE, APPEND, EDIT_APPEND, NONE, ALL } from './helpers/Flags';
if (typeof window !== 'undefined') {
// Attach to the `window` as `FreeDraw` if it exists, as this would prevent `new FreeDraw.default` when
// using the web version.
window.FreeDraw = FreeDraw;
FreeDraw.CREATE = CREATE;
FreeDraw.EDIT = EDIT;
FreeDraw.DELETE = DELETE;
FreeDraw.APPEND = APPEND;
FreeDraw.EDIT_APPEND = EDIT_APPEND;
FreeDraw.NONE = NONE;
FreeDraw.ALL = ALL;
}