@mapbox/mapbox-gl-draw
Version:
A drawing component for Mapbox GL JS
1,785 lines (1,537 loc) • 131 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.MapboxDraw = factory());
})(this, (function () { 'use strict';
const ModeHandler = function(mode, DrawContext) {
const handlers = {
drag: [],
click: [],
mousemove: [],
mousedown: [],
mouseup: [],
mouseout: [],
keydown: [],
keyup: [],
touchstart: [],
touchmove: [],
touchend: [],
tap: []
};
const ctx = {
on(event, selector, fn) {
if (handlers[event] === undefined) {
throw new Error(`Invalid event type: ${event}`);
}
handlers[event].push({
selector,
fn
});
},
render(id) {
DrawContext.store.featureChanged(id);
}
};
const delegate = function (eventName, event) {
const handles = handlers[eventName];
let iHandle = handles.length;
while (iHandle--) {
const handle = handles[iHandle];
if (handle.selector(event)) {
const skipRender = handle.fn.call(ctx, event);
if (!skipRender) {
DrawContext.store.render();
}
DrawContext.ui.updateMapClasses();
// ensure an event is only handled once
// we do this to let modes have multiple overlapping selectors
// and relay on order of oppertations to filter
break;
}
}
};
mode.start.call(ctx);
return {
render: mode.render,
stop() {
if (mode.stop) mode.stop();
},
trash() {
if (mode.trash) {
mode.trash();
DrawContext.store.render();
}
},
combineFeatures() {
if (mode.combineFeatures) {
mode.combineFeatures();
}
},
uncombineFeatures() {
if (mode.uncombineFeatures) {
mode.uncombineFeatures();
}
},
drag(event) {
delegate('drag', event);
},
click(event) {
delegate('click', event);
},
mousemove(event) {
delegate('mousemove', event);
},
mousedown(event) {
delegate('mousedown', event);
},
mouseup(event) {
delegate('mouseup', event);
},
mouseout(event) {
delegate('mouseout', event);
},
keydown(event) {
delegate('keydown', event);
},
keyup(event) {
delegate('keyup', event);
},
touchstart(event) {
delegate('touchstart', event);
},
touchmove(event) {
delegate('touchmove', event);
},
touchend(event) {
delegate('touchend', event);
},
tap(event) {
delegate('tap', event);
}
};
};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var geojsonArea = {};
var wgs84 = {};
var hasRequiredWgs84;
function requireWgs84 () {
if (hasRequiredWgs84) return wgs84;
hasRequiredWgs84 = 1;
wgs84.RADIUS = 6378137;
wgs84.FLATTENING = 1/298.257223563;
wgs84.POLAR_RADIUS = 6356752.3142;
return wgs84;
}
var hasRequiredGeojsonArea;
function requireGeojsonArea () {
if (hasRequiredGeojsonArea) return geojsonArea;
hasRequiredGeojsonArea = 1;
var wgs84 = requireWgs84();
geojsonArea.geometry = geometry;
geojsonArea.ring = ringArea;
function geometry(_) {
var area = 0, i;
switch (_.type) {
case 'Polygon':
return polygonArea(_.coordinates);
case 'MultiPolygon':
for (i = 0; i < _.coordinates.length; i++) {
area += polygonArea(_.coordinates[i]);
}
return area;
case 'Point':
case 'MultiPoint':
case 'LineString':
case 'MultiLineString':
return 0;
case 'GeometryCollection':
for (i = 0; i < _.geometries.length; i++) {
area += geometry(_.geometries[i]);
}
return area;
}
}
function polygonArea(coords) {
var area = 0;
if (coords && coords.length > 0) {
area += Math.abs(ringArea(coords[0]));
for (var i = 1; i < coords.length; i++) {
area -= Math.abs(ringArea(coords[i]));
}
}
return area;
}
/**
* Calculate the approximate area of the polygon were it projected onto
* the earth. Note that this area will be positive if ring is oriented
* clockwise, otherwise it will be negative.
*
* Reference:
* Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
* Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion
* Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409
*
* Returns:
* {float} The approximate signed geodesic area of the polygon in square
* meters.
*/
function ringArea(coords) {
var p1, p2, p3, lowerIndex, middleIndex, upperIndex, i,
area = 0,
coordsLength = coords.length;
if (coordsLength > 2) {
for (i = 0; i < coordsLength; i++) {
if (i === coordsLength - 2) {// i = N-2
lowerIndex = coordsLength - 2;
middleIndex = coordsLength -1;
upperIndex = 0;
} else if (i === coordsLength - 1) {// i = N-1
lowerIndex = coordsLength - 1;
middleIndex = 0;
upperIndex = 1;
} else { // i = 0 to N-3
lowerIndex = i;
middleIndex = i+1;
upperIndex = i+2;
}
p1 = coords[lowerIndex];
p2 = coords[middleIndex];
p3 = coords[upperIndex];
area += ( rad(p3[0]) - rad(p1[0]) ) * Math.sin( rad(p2[1]));
}
area = area * wgs84.RADIUS * wgs84.RADIUS / 2;
}
return area;
}
function rad(_) {
return _ * Math.PI / 180;
}
return geojsonArea;
}
var geojsonAreaExports = requireGeojsonArea();
var area = /*@__PURE__*/getDefaultExportFromCjs(geojsonAreaExports);
const classes = {
CANVAS: 'mapboxgl-canvas',
CONTROL_BASE: 'mapboxgl-ctrl',
CONTROL_PREFIX: 'mapboxgl-ctrl-',
CONTROL_BUTTON: 'mapbox-gl-draw_ctrl-draw-btn',
CONTROL_BUTTON_LINE: 'mapbox-gl-draw_line',
CONTROL_BUTTON_POLYGON: 'mapbox-gl-draw_polygon',
CONTROL_BUTTON_POINT: 'mapbox-gl-draw_point',
CONTROL_BUTTON_TRASH: 'mapbox-gl-draw_trash',
CONTROL_BUTTON_COMBINE_FEATURES: 'mapbox-gl-draw_combine',
CONTROL_BUTTON_UNCOMBINE_FEATURES: 'mapbox-gl-draw_uncombine',
CONTROL_GROUP: 'mapboxgl-ctrl-group',
ATTRIBUTION: 'mapboxgl-ctrl-attrib',
ACTIVE_BUTTON: 'active',
BOX_SELECT: 'mapbox-gl-draw_boxselect'
};
const sources = {
HOT: 'mapbox-gl-draw-hot',
COLD: 'mapbox-gl-draw-cold'
};
const cursors = {
ADD: 'add',
MOVE: 'move',
DRAG: 'drag',
POINTER: 'pointer',
NONE: 'none'
};
const types = {
POLYGON: 'polygon',
LINE: 'line_string',
POINT: 'point'
};
const geojsonTypes = {
FEATURE: 'Feature',
POLYGON: 'Polygon',
LINE_STRING: 'LineString',
POINT: 'Point',
FEATURE_COLLECTION: 'FeatureCollection',
MULTI_PREFIX: 'Multi',
MULTI_POINT: 'MultiPoint',
MULTI_LINE_STRING: 'MultiLineString',
MULTI_POLYGON: 'MultiPolygon'
};
const modes$1 = {
DRAW_LINE_STRING: 'draw_line_string',
DRAW_POLYGON: 'draw_polygon',
DRAW_POINT: 'draw_point',
SIMPLE_SELECT: 'simple_select',
DIRECT_SELECT: 'direct_select'
};
const events$1 = {
CREATE: 'draw.create',
DELETE: 'draw.delete',
UPDATE: 'draw.update',
SELECTION_CHANGE: 'draw.selectionchange',
MODE_CHANGE: 'draw.modechange',
ACTIONABLE: 'draw.actionable',
RENDER: 'draw.render',
COMBINE_FEATURES: 'draw.combine',
UNCOMBINE_FEATURES: 'draw.uncombine'
};
const updateActions = {
MOVE: 'move',
CHANGE_PROPERTIES: 'change_properties',
CHANGE_COORDINATES: 'change_coordinates'
};
const meta = {
FEATURE: 'feature',
MIDPOINT: 'midpoint',
VERTEX: 'vertex'
};
const activeStates = {
ACTIVE: 'true',
INACTIVE: 'false'
};
const interactions = [
'scrollZoom',
'boxZoom',
'dragRotate',
'dragPan',
'keyboard',
'doubleClickZoom',
'touchZoomRotate'
];
const LAT_MIN$1 = -90;
const LAT_RENDERED_MIN$1 = -85;
const LAT_MAX$1 = 90;
const LAT_RENDERED_MAX$1 = 85;
const LNG_MIN$1 = -270;
const LNG_MAX$1 = 270;
var Constants = /*#__PURE__*/Object.freeze({
__proto__: null,
LAT_MAX: LAT_MAX$1,
LAT_MIN: LAT_MIN$1,
LAT_RENDERED_MAX: LAT_RENDERED_MAX$1,
LAT_RENDERED_MIN: LAT_RENDERED_MIN$1,
LNG_MAX: LNG_MAX$1,
LNG_MIN: LNG_MIN$1,
activeStates: activeStates,
classes: classes,
cursors: cursors,
events: events$1,
geojsonTypes: geojsonTypes,
interactions: interactions,
meta: meta,
modes: modes$1,
sources: sources,
types: types,
updateActions: updateActions
});
const FEATURE_SORT_RANKS = {
Point: 0,
LineString: 1,
MultiLineString: 1,
Polygon: 2
};
function comparator(a, b) {
const score = FEATURE_SORT_RANKS[a.geometry.type] - FEATURE_SORT_RANKS[b.geometry.type];
if (score === 0 && a.geometry.type === geojsonTypes.POLYGON) {
return a.area - b.area;
}
return score;
}
// Sort in the order above, then sort polygons by area ascending.
function sortFeatures(features) {
return features.map((feature) => {
if (feature.geometry.type === geojsonTypes.POLYGON) {
feature.area = area.geometry({
type: geojsonTypes.FEATURE,
property: {},
geometry: feature.geometry
});
}
return feature;
}).sort(comparator).map((feature) => {
delete feature.area;
return feature;
});
}
/**
* Returns a bounding box representing the event's location.
*
* @param {Event} mapEvent - Mapbox GL JS map event, with a point properties.
* @return {Array<Array<number>>} Bounding box.
*/
function mapEventToBoundingBox(mapEvent, buffer = 0) {
return [
[mapEvent.point.x - buffer, mapEvent.point.y - buffer],
[mapEvent.point.x + buffer, mapEvent.point.y + buffer]
];
}
function StringSet(items) {
this._items = {};
this._nums = {};
this._length = items ? items.length : 0;
if (!items) return;
for (let i = 0, l = items.length; i < l; i++) {
this.add(items[i]);
if (items[i] === undefined) continue;
if (typeof items[i] === 'string') this._items[items[i]] = i;
else this._nums[items[i]] = i;
}
}
StringSet.prototype.add = function(x) {
if (this.has(x)) return this;
this._length++;
if (typeof x === 'string') this._items[x] = this._length;
else this._nums[x] = this._length;
return this;
};
StringSet.prototype.delete = function(x) {
if (this.has(x) === false) return this;
this._length--;
delete this._items[x];
delete this._nums[x];
return this;
};
StringSet.prototype.has = function(x) {
if (typeof x !== 'string' && typeof x !== 'number') return false;
return this._items[x] !== undefined || this._nums[x] !== undefined;
};
StringSet.prototype.values = function() {
const values = [];
Object.keys(this._items).forEach((k) => {
values.push({ k, v: this._items[k] });
});
Object.keys(this._nums).forEach((k) => {
values.push({ k: JSON.parse(k), v: this._nums[k] });
});
return values.sort((a, b) => a.v - b.v).map(a => a.k);
};
StringSet.prototype.clear = function() {
this._length = 0;
this._items = {};
this._nums = {};
return this;
};
const META_TYPES = [
meta.FEATURE,
meta.MIDPOINT,
meta.VERTEX
];
// Requires either event or bbox
var featuresAt = {
click: featuresAtClick,
touch: featuresAtTouch
};
function featuresAtClick(event, bbox, ctx) {
return featuresAt$1(event, bbox, ctx, ctx.options.clickBuffer);
}
function featuresAtTouch(event, bbox, ctx) {
return featuresAt$1(event, bbox, ctx, ctx.options.touchBuffer);
}
function featuresAt$1(event, bbox, ctx, buffer) {
if (ctx.map === null) return [];
const box = (event) ? mapEventToBoundingBox(event, buffer) : bbox;
const queryParams = {};
if (ctx.options.styles) queryParams.layers = ctx.options.styles.map(s => s.id).filter(id => ctx.map.getLayer(id) != null);
const features = ctx.map.queryRenderedFeatures(box, queryParams)
.filter(feature => META_TYPES.indexOf(feature.properties.meta) !== -1);
const featureIds = new StringSet();
const uniqueFeatures = [];
features.forEach((feature) => {
const featureId = feature.properties.id;
if (featureIds.has(featureId)) return;
featureIds.add(featureId);
uniqueFeatures.push(feature);
});
return sortFeatures(uniqueFeatures);
}
function getFeatureAtAndSetCursors(event, ctx) {
const features = featuresAt.click(event, null, ctx);
const classes = { mouse: cursors.NONE };
if (features[0]) {
classes.mouse = (features[0].properties.active === activeStates.ACTIVE) ?
cursors.MOVE : cursors.POINTER;
classes.feature = features[0].properties.meta;
}
if (ctx.events.currentModeName().indexOf('draw') !== -1) {
classes.mouse = cursors.ADD;
}
ctx.ui.queueMapClasses(classes);
ctx.ui.updateMapClasses();
return features[0];
}
function euclideanDistance(a, b) {
const x = a.x - b.x;
const y = a.y - b.y;
return Math.sqrt((x * x) + (y * y));
}
const FINE_TOLERANCE = 4;
const GROSS_TOLERANCE = 12;
const INTERVAL = 500;
function isClick(start, end, options = {}) {
const fineTolerance = (options.fineTolerance != null) ? options.fineTolerance : FINE_TOLERANCE;
const grossTolerance = (options.grossTolerance != null) ? options.grossTolerance : GROSS_TOLERANCE;
const interval = (options.interval != null) ? options.interval : INTERVAL;
start.point = start.point || end.point;
start.time = start.time || end.time;
const moveDistance = euclideanDistance(start.point, end.point);
return moveDistance < fineTolerance ||
(moveDistance < grossTolerance && (end.time - start.time) < interval);
}
const TAP_TOLERANCE = 25;
const TAP_INTERVAL = 250;
function isTap(start, end, options = {}) {
const tolerance = (options.tolerance != null) ? options.tolerance : TAP_TOLERANCE;
const interval = (options.interval != null) ? options.interval : TAP_INTERVAL;
start.point = start.point || end.point;
start.time = start.time || end.time;
const moveDistance = euclideanDistance(start.point, end.point);
return moveDistance < tolerance && (end.time - start.time) < interval;
}
let customAlphabet = (alphabet, defaultSize = 21) => {
return (size = defaultSize) => {
let id = '';
let i = size | 0;
while (i--) {
id += alphabet[(Math.random() * alphabet.length) | 0];
}
return id
}
};
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 32);
function generateID() {
return nanoid();
}
const Feature = function(ctx, geojson) {
this.ctx = ctx;
this.properties = geojson.properties || {};
this.coordinates = geojson.geometry.coordinates;
this.id = geojson.id || generateID();
this.type = geojson.geometry.type;
};
Feature.prototype.changed = function() {
this.ctx.store.featureChanged(this.id);
};
Feature.prototype.incomingCoords = function(coords) {
this.setCoordinates(coords);
};
Feature.prototype.setCoordinates = function(coords) {
this.coordinates = coords;
this.changed();
};
Feature.prototype.getCoordinates = function() {
return JSON.parse(JSON.stringify(this.coordinates));
};
Feature.prototype.setProperty = function(property, value) {
this.properties[property] = value;
};
Feature.prototype.toGeoJSON = function() {
return JSON.parse(JSON.stringify({
id: this.id,
type: geojsonTypes.FEATURE,
properties: this.properties,
geometry: {
coordinates: this.getCoordinates(),
type: this.type
}
}));
};
Feature.prototype.internal = function(mode) {
const properties = {
id: this.id,
meta: meta.FEATURE,
'meta:type': this.type,
active: activeStates.INACTIVE,
mode
};
if (this.ctx.options.userProperties) {
for (const name in this.properties) {
properties[`user_${name}`] = this.properties[name];
}
}
return {
type: geojsonTypes.FEATURE,
properties,
geometry: {
coordinates: this.getCoordinates(),
type: this.type
}
};
};
const Point$1 = function(ctx, geojson) {
Feature.call(this, ctx, geojson);
};
Point$1.prototype = Object.create(Feature.prototype);
Point$1.prototype.isValid = function() {
return typeof this.coordinates[0] === 'number' &&
typeof this.coordinates[1] === 'number';
};
Point$1.prototype.updateCoordinate = function(pathOrLng, lngOrLat, lat) {
if (arguments.length === 3) {
this.coordinates = [lngOrLat, lat];
} else {
this.coordinates = [pathOrLng, lngOrLat];
}
this.changed();
};
Point$1.prototype.getCoordinate = function() {
return this.getCoordinates();
};
const LineString = function(ctx, geojson) {
Feature.call(this, ctx, geojson);
};
LineString.prototype = Object.create(Feature.prototype);
LineString.prototype.isValid = function() {
return this.coordinates.length > 1;
};
LineString.prototype.addCoordinate = function(path, lng, lat) {
this.changed();
const id = parseInt(path, 10);
this.coordinates.splice(id, 0, [lng, lat]);
};
LineString.prototype.getCoordinate = function(path) {
const id = parseInt(path, 10);
return JSON.parse(JSON.stringify(this.coordinates[id]));
};
LineString.prototype.removeCoordinate = function(path) {
this.changed();
this.coordinates.splice(parseInt(path, 10), 1);
};
LineString.prototype.updateCoordinate = function(path, lng, lat) {
const id = parseInt(path, 10);
this.coordinates[id] = [lng, lat];
this.changed();
};
const Polygon = function(ctx, geojson) {
Feature.call(this, ctx, geojson);
this.coordinates = this.coordinates.map(ring => ring.slice(0, -1));
};
Polygon.prototype = Object.create(Feature.prototype);
Polygon.prototype.isValid = function() {
if (this.coordinates.length === 0) return false;
return this.coordinates.every(ring => ring.length > 2);
};
// Expects valid geoJSON polygon geometry: first and last positions must be equivalent.
Polygon.prototype.incomingCoords = function(coords) {
this.coordinates = coords.map(ring => ring.slice(0, -1));
this.changed();
};
// Does NOT expect valid geoJSON polygon geometry: first and last positions should not be equivalent.
Polygon.prototype.setCoordinates = function(coords) {
this.coordinates = coords;
this.changed();
};
Polygon.prototype.addCoordinate = function(path, lng, lat) {
this.changed();
const ids = path.split('.').map(x => parseInt(x, 10));
const ring = this.coordinates[ids[0]];
ring.splice(ids[1], 0, [lng, lat]);
};
Polygon.prototype.removeCoordinate = function(path) {
this.changed();
const ids = path.split('.').map(x => parseInt(x, 10));
const ring = this.coordinates[ids[0]];
if (ring) {
ring.splice(ids[1], 1);
if (ring.length < 3) {
this.coordinates.splice(ids[0], 1);
}
}
};
Polygon.prototype.getCoordinate = function(path) {
const ids = path.split('.').map(x => parseInt(x, 10));
const ring = this.coordinates[ids[0]];
return JSON.parse(JSON.stringify(ring[ids[1]]));
};
Polygon.prototype.getCoordinates = function() {
return this.coordinates.map(coords => coords.concat([coords[0]]));
};
Polygon.prototype.updateCoordinate = function(path, lng, lat) {
this.changed();
const parts = path.split('.');
const ringId = parseInt(parts[0], 10);
const coordId = parseInt(parts[1], 10);
if (this.coordinates[ringId] === undefined) {
this.coordinates[ringId] = [];
}
this.coordinates[ringId][coordId] = [lng, lat];
};
const models = {
MultiPoint: Point$1,
MultiLineString: LineString,
MultiPolygon: Polygon
};
const takeAction = (features, action, path, lng, lat) => {
const parts = path.split('.');
const idx = parseInt(parts[0], 10);
const tail = (!parts[1]) ? null : parts.slice(1).join('.');
return features[idx][action](tail, lng, lat);
};
const MultiFeature = function(ctx, geojson) {
Feature.call(this, ctx, geojson);
delete this.coordinates;
this.model = models[geojson.geometry.type];
if (this.model === undefined) throw new TypeError(`${geojson.geometry.type} is not a valid type`);
this.features = this._coordinatesToFeatures(geojson.geometry.coordinates);
};
MultiFeature.prototype = Object.create(Feature.prototype);
MultiFeature.prototype._coordinatesToFeatures = function(coordinates) {
const Model = this.model.bind(this);
return coordinates.map(coords => new Model(this.ctx, {
id: generateID(),
type: geojsonTypes.FEATURE,
properties: {},
geometry: {
coordinates: coords,
type: this.type.replace('Multi', '')
}
}));
};
MultiFeature.prototype.isValid = function() {
return this.features.every(f => f.isValid());
};
MultiFeature.prototype.setCoordinates = function(coords) {
this.features = this._coordinatesToFeatures(coords);
this.changed();
};
MultiFeature.prototype.getCoordinate = function(path) {
return takeAction(this.features, 'getCoordinate', path);
};
MultiFeature.prototype.getCoordinates = function() {
return JSON.parse(JSON.stringify(this.features.map((f) => {
if (f.type === geojsonTypes.POLYGON) return f.getCoordinates();
return f.coordinates;
})));
};
MultiFeature.prototype.updateCoordinate = function(path, lng, lat) {
takeAction(this.features, 'updateCoordinate', path, lng, lat);
this.changed();
};
MultiFeature.prototype.addCoordinate = function(path, lng, lat) {
takeAction(this.features, 'addCoordinate', path, lng, lat);
this.changed();
};
MultiFeature.prototype.removeCoordinate = function(path) {
takeAction(this.features, 'removeCoordinate', path);
this.changed();
};
MultiFeature.prototype.getFeatures = function() {
return this.features;
};
function ModeInterface(ctx) {
this.map = ctx.map;
this.drawConfig = JSON.parse(JSON.stringify(ctx.options || {}));
this._ctx = ctx;
}
/**
* Sets Draw's interal selected state
* @name this.setSelected
* @param {DrawFeature[]} - whats selected as a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js)
*/
ModeInterface.prototype.setSelected = function(features) {
return this._ctx.store.setSelected(features);
};
/**
* Sets Draw's internal selected coordinate state
* @name this.setSelectedCoordinates
* @param {Object[]} coords - a array of {coord_path: 'string', feature_id: 'string'}
*/
ModeInterface.prototype.setSelectedCoordinates = function(coords) {
this._ctx.store.setSelectedCoordinates(coords);
coords.reduce((m, c) => {
if (m[c.feature_id] === undefined) {
m[c.feature_id] = true;
this._ctx.store.get(c.feature_id).changed();
}
return m;
}, {});
};
/**
* Get all selected features as a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js)
* @name this.getSelected
* @returns {DrawFeature[]}
*/
ModeInterface.prototype.getSelected = function() {
return this._ctx.store.getSelected();
};
/**
* Get the ids of all currently selected features
* @name this.getSelectedIds
* @returns {String[]}
*/
ModeInterface.prototype.getSelectedIds = function() {
return this._ctx.store.getSelectedIds();
};
/**
* Check if a feature is selected
* @name this.isSelected
* @param {String} id - a feature id
* @returns {Boolean}
*/
ModeInterface.prototype.isSelected = function(id) {
return this._ctx.store.isSelected(id);
};
/**
* Get a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) by its id
* @name this.getFeature
* @param {String} id - a feature id
* @returns {DrawFeature}
*/
ModeInterface.prototype.getFeature = function(id) {
return this._ctx.store.get(id);
};
/**
* Add a feature to draw's internal selected state
* @name this.select
* @param {String} id
*/
ModeInterface.prototype.select = function(id) {
return this._ctx.store.select(id);
};
/**
* Remove a feature from draw's internal selected state
* @name this.delete
* @param {String} id
*/
ModeInterface.prototype.deselect = function(id) {
return this._ctx.store.deselect(id);
};
/**
* Delete a feature from draw
* @name this.deleteFeature
* @param {String} id - a feature id
*/
ModeInterface.prototype.deleteFeature = function(id, opts = {}) {
return this._ctx.store.delete(id, opts);
};
/**
* Add a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) to draw.
* See `this.newFeature` for converting geojson into a DrawFeature
* @name this.addFeature
* @param {DrawFeature} feature - the feature to add
*/
ModeInterface.prototype.addFeature = function(feature, opts = {}) {
return this._ctx.store.add(feature, opts);
};
/**
* Clear all selected features
*/
ModeInterface.prototype.clearSelectedFeatures = function() {
return this._ctx.store.clearSelected();
};
/**
* Clear all selected coordinates
*/
ModeInterface.prototype.clearSelectedCoordinates = function() {
return this._ctx.store.clearSelectedCoordinates();
};
/**
* Indicate if the different action are currently possible with your mode
* See [draw.actionalbe](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#drawactionable) for a list of possible actions. All undefined actions are set to **false** by default
* @name this.setActionableState
* @param {Object} actions
*/
ModeInterface.prototype.setActionableState = function(actions = {}) {
const newSet = {
trash: actions.trash || false,
combineFeatures: actions.combineFeatures || false,
uncombineFeatures: actions.uncombineFeatures || false
};
return this._ctx.events.actionable(newSet);
};
/**
* Trigger a mode change
* @name this.changeMode
* @param {String} mode - the mode to transition into
* @param {Object} opts - the options object to pass to the new mode
* @param {Object} eventOpts - used to control what kind of events are emitted.
*/
ModeInterface.prototype.changeMode = function(mode, opts = {}, eventOpts = {}) {
return this._ctx.events.changeMode(mode, opts, eventOpts);
};
/**
* Fire a map event
* @name this.fire
* @param {String} eventName - the event name.
* @param {Object} eventData - the event data object.
*/
ModeInterface.prototype.fire = function(eventName, eventData) {
return this._ctx.events.fire(eventName, eventData);
};
/**
* Update the state of draw map classes
* @name this.updateUIClasses
* @param {Object} opts
*/
ModeInterface.prototype.updateUIClasses = function(opts) {
return this._ctx.ui.queueMapClasses(opts);
};
/**
* If a name is provided it makes that button active, else if makes all buttons inactive
* @name this.activateUIButton
* @param {String?} name - name of the button to make active, leave as undefined to set buttons to be inactive
*/
ModeInterface.prototype.activateUIButton = function(name) {
return this._ctx.ui.setActiveButton(name);
};
/**
* Get the features at the location of an event object or in a bbox
* @name this.featuresAt
* @param {Event||NULL} event - a mapbox-gl event object
* @param {BBOX||NULL} bbox - the area to get features from
* @param {String} bufferType - is this `click` or `tap` event, defaults to click
*/
ModeInterface.prototype.featuresAt = function(event, bbox, bufferType = 'click') {
if (bufferType !== 'click' && bufferType !== 'touch') throw new Error('invalid buffer type');
return featuresAt[bufferType](event, bbox, this._ctx);
};
/**
* Create a new [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) from geojson
* @name this.newFeature
* @param {GeoJSONFeature} geojson
* @returns {DrawFeature}
*/
ModeInterface.prototype.newFeature = function(geojson) {
const type = geojson.geometry.type;
if (type === geojsonTypes.POINT) return new Point$1(this._ctx, geojson);
if (type === geojsonTypes.LINE_STRING) return new LineString(this._ctx, geojson);
if (type === geojsonTypes.POLYGON) return new Polygon(this._ctx, geojson);
return new MultiFeature(this._ctx, geojson);
};
/**
* Check is an object is an instance of a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js)
* @name this.isInstanceOf
* @param {String} type - `Point`, `LineString`, `Polygon`, `MultiFeature`
* @param {Object} feature - the object that needs to be checked
* @returns {Boolean}
*/
ModeInterface.prototype.isInstanceOf = function(type, feature) {
if (type === geojsonTypes.POINT) return feature instanceof Point$1;
if (type === geojsonTypes.LINE_STRING) return feature instanceof LineString;
if (type === geojsonTypes.POLYGON) return feature instanceof Polygon;
if (type === 'MultiFeature') return feature instanceof MultiFeature;
throw new Error(`Unknown feature class: ${type}`);
};
/**
* Force draw to rerender the feature of the provided id
* @name this.doRender
* @param {String} id - a feature id
*/
ModeInterface.prototype.doRender = function(id) {
return this._ctx.store.featureChanged(id);
};
/**
* Triggered while a mode is being transitioned into.
* @param opts {Object} - this is the object passed via `draw.changeMode('mode', opts)`;
* @name MODE.onSetup
* @returns {Object} - this object will be passed to all other life cycle functions
*/
ModeInterface.prototype.onSetup = function() {};
/**
* Triggered when a drag event is detected on the map
* @name MODE.onDrag
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onDrag = function() {};
/**
* Triggered when the mouse is clicked
* @name MODE.onClick
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onClick = function() {};
/**
* Triggered with the mouse is moved
* @name MODE.onMouseMove
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onMouseMove = function() {};
/**
* Triggered when the mouse button is pressed down
* @name MODE.onMouseDown
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onMouseDown = function() {};
/**
* Triggered when the mouse button is released
* @name MODE.onMouseUp
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onMouseUp = function() {};
/**
* Triggered when the mouse leaves the map's container
* @name MODE.onMouseOut
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onMouseOut = function() {};
/**
* Triggered when a key up event is detected
* @name MODE.onKeyUp
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onKeyUp = function() {};
/**
* Triggered when a key down event is detected
* @name MODE.onKeyDown
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onKeyDown = function() {};
/**
* Triggered when a touch event is started
* @name MODE.onTouchStart
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onTouchStart = function() {};
/**
* Triggered when one drags thier finger on a mobile device
* @name MODE.onTouchMove
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onTouchMove = function() {};
/**
* Triggered when one removes their finger from the map
* @name MODE.onTouchEnd
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onTouchEnd = function() {};
/**
* Triggered when one quicly taps the map
* @name MODE.onTap
* @param state {Object} - a mutible state object created by onSetup
* @param e {Object} - the captured event that is triggering this life cycle event
*/
ModeInterface.prototype.onTap = function() {};
/**
* Triggered when the mode is being exited, to be used for cleaning up artifacts such as invalid features
* @name MODE.onStop
* @param state {Object} - a mutible state object created by onSetup
*/
ModeInterface.prototype.onStop = function() {};
/**
* Triggered when [draw.trash()](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#trash-draw) is called.
* @name MODE.onTrash
* @param state {Object} - a mutible state object created by onSetup
*/
ModeInterface.prototype.onTrash = function() {};
/**
* Triggered when [draw.combineFeatures()](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#combinefeatures-draw) is called.
* @name MODE.onCombineFeature
* @param state {Object} - a mutible state object created by onSetup
*/
ModeInterface.prototype.onCombineFeature = function() {};
/**
* Triggered when [draw.uncombineFeatures()](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#uncombinefeatures-draw) is called.
* @name MODE.onUncombineFeature
* @param state {Object} - a mutible state object created by onSetup
*/
ModeInterface.prototype.onUncombineFeature = function() {};
/**
* Triggered per feature on render to convert raw features into set of features for display on the map
* See [styling draw](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#styling-draw) for information about what geojson properties Draw uses as part of rendering.
* @name MODE.toDisplayFeatures
* @param state {Object} - a mutible state object created by onSetup
* @param geojson {Object} - a geojson being evaulated. To render, pass to `display`.
* @param display {Function} - all geojson objects passed to this be rendered onto the map
*/
ModeInterface.prototype.toDisplayFeatures = function() {
throw new Error('You must overwrite toDisplayFeatures');
};
const eventMapper = {
drag: 'onDrag',
click: 'onClick',
mousemove: 'onMouseMove',
mousedown: 'onMouseDown',
mouseup: 'onMouseUp',
mouseout: 'onMouseOut',
keyup: 'onKeyUp',
keydown: 'onKeyDown',
touchstart: 'onTouchStart',
touchmove: 'onTouchMove',
touchend: 'onTouchEnd',
tap: 'onTap'
};
const eventKeys = Object.keys(eventMapper);
function objectToMode(modeObject) {
const modeObjectKeys = Object.keys(modeObject);
return function(ctx, startOpts = {}) {
let state = {};
const mode = modeObjectKeys.reduce((m, k) => {
m[k] = modeObject[k];
return m;
}, new ModeInterface(ctx));
function wrapper(eh) {
return e => mode[eh](state, e);
}
return {
start() {
state = mode.onSetup(startOpts); // this should set ui buttons
// Adds event handlers for all event options
// add sets the selector to false for all
// handlers that are not present in the mode
// to reduce on render calls for functions that
// have no logic
eventKeys.forEach((key) => {
const modeHandler = eventMapper[key];
let selector = () => false;
if (modeObject[modeHandler]) {
selector = () => true;
}
this.on(key, selector, wrapper(modeHandler));
});
},
stop() {
mode.onStop(state);
},
trash() {
mode.onTrash(state);
},
combineFeatures() {
mode.onCombineFeatures(state);
},
uncombineFeatures() {
mode.onUncombineFeatures(state);
},
render(geojson, push) {
mode.toDisplayFeatures(state, geojson, push);
}
};
};
}
function events(ctx) {
const modes = Object.keys(ctx.options.modes).reduce((m, k) => {
m[k] = objectToMode(ctx.options.modes[k]);
return m;
}, {});
let mouseDownInfo = {};
let touchStartInfo = {};
const events = {};
let currentModeName = null;
let currentMode = null;
events.drag = function(event, isDrag) {
if (isDrag({
point: event.point,
time: new Date().getTime()
})) {
ctx.ui.queueMapClasses({ mouse: cursors.DRAG });
currentMode.drag(event);
} else {
event.originalEvent.stopPropagation();
}
};
events.mousedrag = function(event) {
events.drag(event, endInfo => !isClick(mouseDownInfo, endInfo));
};
events.touchdrag = function(event) {
events.drag(event, endInfo => !isTap(touchStartInfo, endInfo));
};
events.mousemove = function(event) {
const button = event.originalEvent.buttons !== undefined ? event.originalEvent.buttons : event.originalEvent.which;
if (button === 1) {
return events.mousedrag(event);
}
const target = getFeatureAtAndSetCursors(event, ctx);
event.featureTarget = target;
currentMode.mousemove(event);
};
events.mousedown = function(event) {
mouseDownInfo = {
time: new Date().getTime(),
point: event.point
};
const target = getFeatureAtAndSetCursors(event, ctx);
event.featureTarget = target;
currentMode.mousedown(event);
};
events.mouseup = function(event) {
const target = getFeatureAtAndSetCursors(event, ctx);
event.featureTarget = target;
if (isClick(mouseDownInfo, {
point: event.point,
time: new Date().getTime()
})) {
currentMode.click(event);
} else {
currentMode.mouseup(event);
}
};
events.mouseout = function(event) {
currentMode.mouseout(event);
};
events.touchstart = function(event) {
if (!ctx.options.touchEnabled) {
return;
}
touchStartInfo = {
time: new Date().getTime(),
point: event.point
};
const target = featuresAt.touch(event, null, ctx)[0];
event.featureTarget = target;
currentMode.touchstart(event);
};
events.touchmove = function(event) {
if (!ctx.options.touchEnabled) {
return;
}
currentMode.touchmove(event);
return events.touchdrag(event);
};
events.touchend = function(event) {
// Prevent emulated mouse events because we will fully handle the touch here.
// This does not stop the touch events from propogating to mapbox though.
event.originalEvent.preventDefault();
if (!ctx.options.touchEnabled) {
return;
}
const target = featuresAt.touch(event, null, ctx)[0];
event.featureTarget = target;
if (isTap(touchStartInfo, {
time: new Date().getTime(),
point: event.point
})) {
currentMode.tap(event);
} else {
currentMode.touchend(event);
}
};
// 8 - Backspace
// 46 - Delete
const isKeyModeValid = code => !(code === 8 || code === 46 || (code >= 48 && code <= 57));
events.keydown = function(event) {
const isMapElement = (event.srcElement || event.target).classList.contains(classes.CANVAS);
if (!isMapElement) return; // we only handle events on the map
if ((event.keyCode === 8 || event.keyCode === 46) && ctx.options.controls.trash) {
event.preventDefault();
currentMode.trash();
} else if (isKeyModeValid(event.keyCode)) {
currentMode.keydown(event);
} else if (event.keyCode === 49 && ctx.options.controls.point) {
changeMode(modes$1.DRAW_POINT);
} else if (event.keyCode === 50 && ctx.options.controls.line_string) {
changeMode(modes$1.DRAW_LINE_STRING);
} else if (event.keyCode === 51 && ctx.options.controls.polygon) {
changeMode(modes$1.DRAW_POLYGON);
}
};
events.keyup = function(event) {
if (isKeyModeValid(event.keyCode)) {
currentMode.keyup(event);
}
};
events.zoomend = function() {
ctx.store.changeZoom();
};
events.data = function(event) {
if (event.dataType === 'style') {
const { setup, map, options, store } = ctx;
const hasLayers = options.styles.some(style => map.getLayer(style.id));
if (!hasLayers) {
setup.addLayers();
store.setDirty();
store.render();
}
}
};
function changeMode(modename, nextModeOptions, eventOptions = {}) {
currentMode.stop();
const modebuilder = modes[modename];
if (modebuilder === undefined) {
throw new Error(`${modename} is not valid`);
}
currentModeName = modename;
const mode = modebuilder(ctx, nextModeOptions);
currentMode = ModeHandler(mode, ctx);
if (!eventOptions.silent) {
ctx.map.fire(events$1.MODE_CHANGE, { mode: modename});
}
ctx.store.setDirty();
ctx.store.render();
}
const actionState = {
trash: false,
combineFeatures: false,
uncombineFeatures: false
};
function actionable(actions) {
let changed = false;
Object.keys(actions).forEach((action) => {
if (actionState[action] === undefined) throw new Error('Invalid action type');
if (actionState[action] !== actions[action]) changed = true;
actionState[action] = actions[action];
});
if (changed) ctx.map.fire(events$1.ACTIONABLE, { actions: actionState });
}
const api = {
start() {
currentModeName = ctx.options.defaultMode;
currentMode = ModeHandler(modes[currentModeName](ctx), ctx);
},
changeMode,
actionable,
currentModeName() {
return currentModeName;
},
currentModeRender(geojson, push) {
return currentMode.render(geojson, push);
},
fire(eventName, eventData) {
if (!ctx.map) return;
ctx.map.fire(eventName, eventData);
},
addEventListeners() {
ctx.map.on('mousemove', events.mousemove);
ctx.map.on('mousedown', events.mousedown);
ctx.map.on('mouseup', events.mouseup);
ctx.map.on('data', events.data);
ctx.map.on('touchmove', events.touchmove);
ctx.map.on('touchstart', events.touchstart);
ctx.map.on('touchend', events.touchend);
ctx.container.addEventListener('mouseout', events.mouseout);
if (ctx.options.keybindings) {
ctx.container.addEventListener('keydown', events.keydown);
ctx.container.addEventListener('keyup', events.keyup);
}
},
removeEventListeners() {
ctx.map.off('mousemove', events.mousemove);
ctx.map.off('mousedown', events.mousedown);
ctx.map.off('mouseup', events.mouseup);
ctx.map.off('data', events.data);
ctx.map.off('touchmove', events.touchmove);
ctx.map.off('touchstart', events.touchstart);
ctx.map.off('touchend', events.touchend);
ctx.container.removeEventListener('mouseout', events.mouseout);
if (ctx.options.keybindings) {
ctx.container.removeEventListener('keydown', events.keydown);
ctx.container.removeEventListener('keyup', events.keyup);
}
},
trash(options) {
currentMode.trash(options);
},
combineFeatures() {
currentMode.combineFeatures();
},
uncombineFeatures() {
currentMode.uncombineFeatures();
},
getMode() {
return currentModeName;
}
};
return api;
}
/**
* Derive a dense array (no `undefined`s) from a single value or array.
*
* @param {any} x
* @return {Array<any>}
*/
function toDenseArray(x) {
return [].concat(x).filter(y => y !== undefined);
}
function render() {
// eslint-disable-next-line no-invalid-this
const store = this;
const mapExists = store.ctx.map && store.ctx.map.getSource(sources.HOT) !== undefined;
if (!mapExists) return cleanup();
const mode = store.ctx.events.currentModeName();
store.ctx.ui.queueMapClasses({ mode });
let newHotIds = [];
let newColdIds = [];
if (store.isDirty) {
newColdIds = store.getAllIds();
} else {
newHotIds = store.getChangedIds().filter(id => store.get(id) !== undefined);
newColdIds = store.sources.hot.filter(geojson => geojson.properties.id && newHotIds.indexOf(geojson.properties.id) === -1 && store.get(geojson.properties.id) !== undefined).map(geojson => geojson.properties.id);
}
store.sources.hot = [];
const lastColdCount = store.sources.cold.length;
store.sources.cold = store.isDirty ? [] : store.sources.cold.filter((geojson) => {
const id = geojson.properties.id || geojson.properties.parent;
return newHotIds.indexOf(id) === -1;
});
const coldChanged = lastColdCount !== store.sources.cold.length || newColdIds.length > 0;
newHotIds.forEach(id => renderFeature(id, 'hot'));
newColdIds.forEach(id => renderFeature(id, 'cold'));
function renderFeature(id, source) {
const feature = store.get(id);
const featureInternal = feature.internal(mode);
store.ctx.events.currentModeRender(featureInternal, (geojson) => {
geojson.properties.mode = mode;
store.sources[source].push(geojson);
});
}
if (coldChanged) {
store.ctx.map.getSource(sources.COLD).setData({
type: geojsonTypes.FEATURE_COLLECTION,
features: store.sources.cold
});
}
store.ctx.map.getSource(sources.HOT).setData({
type: geojsonTypes.FEATURE_COLLECTION,
features: store.sources.hot
});
cleanup();
function cleanup() {
store.isDirty = false;
store.clearChangedIds();
}
}
function Store(ctx) {
this._features = {};
this._featureIds = new StringSet();
this._selectedFeatureIds = new StringSet();
this._selectedCoordinates = [];
this._changedFeatureIds = new StringSet();
this._emitSelectionChange = false;
this._mapInitialConfig = {};
this.ctx = ctx;
this.sources = {
hot: [],
cold: []
};
// Deduplicate requests to render and tie them to animation frames.
let renderRequest;
this.render = () => {
if (!renderRequest) {
renderRequest = requestAnimationFrame(() => {
renderRequest = null;
render.call(this);
// Fire deduplicated selection change event
if (this._emitSelectionChange) {
this.ctx.events.fire(events$1.SELECTION_CHANGE, {
features: this.getSelected().map(feature => feature.toGeoJSON()),
points: this.getSelectedCoordinates().map(coordinate => ({
type: geojsonTypes.FEATURE,
properties: {},
geometry: {
type: geojsonTypes.POINT,
coordinates: coordinate.coordinates
}
}))
});
this._emitSelectionChange = false;
}
// Fire render event
this.ctx.events.fire(events$1.RENDER, {});
});
}
};
this.isDirty = false;
}
/**
* Delays all rendering until the returned function is invoked
* @return {Function} renderBatch
*/
Store.prototype.createRenderBatch = function() {
const holdRender = this.render;
let numRenders = 0;
this.render = function() {
numRenders++;
};
return () => {
this.render = holdRender;
if (numRenders > 0) {
this.render();
}
};
};
/**
* Sets the store's state to dirty.
* @return {Store} this
*/
Store.prototype.setDirty = function() {
this.isDirty = true;
return this;
};
/**
* Sets a feature's state to changed.
* @param {string} featureId
* @return {Store} this
*/
Store.prototype.featureCreated = function(featureId, options = {}) {
this._changedFeatureIds.add(featureId);
const silent = options.silent != null ? options.silent : this.ctx.options.suppressAPIEvents;
if (silent !== true) {
const feature = this.get(featureId);
this.ctx.events.fire(events$1.CREATE, {
features: [feature.toGeoJSON()]
});
}
return this;
};
/**
* Sets a feature's state to changed.
* @param {string} featureId
* @return {Store} this
*/
Store.prototype.featureChanged = function(featureId, options = {}) {
this._changedFeatureIds.add(featureId);
const silent = options.silent != null ? options.silent : this.ctx.options.suppressAPIEvents;
if (silent !== true) {
this.ctx.events.fire(events$1.UPDATE, {
action: options.action ? options.action : updateActions.CHANGE_COORDINATES,
features: [this.get(featureId).toGeoJSON()]
});
}
return this;
};
/**
* Gets the ids of all features currently in changed state.
* @return {Store} this
*/
Store.prototype.getChangedIds = function() {
return this._changedFeatureIds.values();
};
/**
* Sets all features to unchanged state.
* @return {Store} this
*/
Store.prototype.clearChangedIds = function() {
this._changedFeatureIds.clear();
return this;
};
/**
* Gets the ids of all features in the store.
* @return {Store} this
*/
Store.prototype.getAllIds = function() {
return this._featureIds.values();
};
/**
* Adds a feature to the store.
* @param {Object} feature
* @param {Object} [options]
* @param {Object} [options.silent] - If `true`, this invocation will not fire an event.
*
* @return {Store} this
*/
Store.prototype.add = function(feature, options = {}) {
this._features[feature.id] = feature;
this._featureIds.add(feature.id);
this.featureCreated(feature.id, {silent: options.silent});
return this;
};
/**
* Deletes a feature or array of features from the store.
* Cleans up after the deletion by deselecting the features.
* If changes were made, sets the state to the dirty
* and fires an event.
* @param {string | Array<string>} featureIds
* @param {Object} [options]
* @param {Object} [options.silent] - If `true`, this invocation will not fire an event.
* @return {Store} this
*/
Store.prototype.delete = function(featureIds, options = {}) {
const deletedFeaturesToEmit = [];
toDenseArray(featureIds).forEach((id) => {
if (!this._featureIds.has(id)) return;
this._featureIds.delete(id);