UNPKG

@mapbox/mapbox-gl-draw

Version:

A drawing component for Mapbox GL JS

1,785 lines (1,537 loc) 131 kB
(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);