(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>} 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} */ 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} 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); this._selectedFeatureIds.delete(id); if (!options.silent) { if (deletedFeaturesToEmit.indexOf(this._features[id]) === -1) { deletedFeaturesToEmit.push(this._features[id].toGeoJSON()); } } delete this._features[id]; this.isDirty = true; }); if (deletedFeaturesToEmit.length) { this.ctx.events.fire(events$1.DELETE, {features: deletedFeaturesToEmit}); } refreshSelectedCoordinates(this, options); return this; }; /** * Returns a feature in the store matching the specified value. * @return {Object | undefined} feature */ Store.prototype.get = function(id) { return this._features[id]; }; /** * Returns all features in the store. * @return {Array} */ Store.prototype.getAll = function() { return Object.keys(this._features).map(id => this._features[id]); }; /** * Adds features to the current selection. * @param {string | Array} featureIds * @param {Object} [options] * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. * @return {Store} this */ Store.prototype.select = function(featureIds, options = {}) { toDenseArray(featureIds).forEach((id) => { if (this._selectedFeatureIds.has(id)) return; this._selectedFeatureIds.add(id); this._changedFeatureIds.add(id); if (!options.silent) { this._emitSelectionChange = true; } }); return this; }; /** * Deletes features from the current selection. * @param {string | Array} featureIds * @param {Object} [options] * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. * @return {Store} this */ Store.prototype.deselect = function(featureIds, options = {}) { toDenseArray(featureIds).forEach((id) => { if (!this._selectedFeatureIds.has(id)) return; this._selectedFeatureIds.delete(id); this._changedFeatureIds.add(id); if (!options.silent) { this._emitSelectionChange = true; } }); refreshSelectedCoordinates(this, options); return this; }; /** * Clears the current selection. * @param {Object} [options] * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. * @return {Store} this */ Store.prototype.clearSelected = function(options = {}) { this.deselect(this._selectedFeatureIds.values(), { silent: options.silent }); return this; }; /** * Sets the store's selection, clearing any prior values. * If no feature ids are passed, the store is just cleared. * @param {string | Array | undefined} featureIds * @param {Object} [options] * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. * @return {Store} this */ Store.prototype.setSelected = function(featureIds, options = {}) { featureIds = toDenseArray(featureIds); // Deselect any features not in the new selection this.deselect(this._selectedFeatureIds.values().filter(id => featureIds.indexOf(id) === -1), { silent: options.silent }); // Select any features in the new selection that were not already selected this.select(featureIds.filter(id => !this._selectedFeatureIds.has(id)), { silent: options.silent }); return this; }; /** * Sets the store's coordinates selection, clearing any prior values. * @param {Array>} coordinates * @return {Store} this */ Store.prototype.setSelectedCoordinates = function(coordinates) { this._selectedCoordinates = coordinates; this._emitSelectionChange = true; return this; }; /** * Clears the current coordinates selection. * @param {Object} [options] * @return {Store} this */ Store.prototype.clearSelectedCoordinates = function() { this._selectedCoordinates = []; this._emitSelectionChange = true; return this; }; /** * Returns the ids of features in the current selection. * @return {Array} Selected feature ids. */ Store.prototype.getSelectedIds = function() { return this._selectedFeatureIds.values(); }; /** * Returns features in the current selection. * @return {Array} Selected features. */ Store.prototype.getSelected = function() { return this.getSelectedIds().map(id => this.get(id)); }; /** * Returns selected coordinates in the currently selected feature. * @return {Array} Selected coordinates. */ Store.prototype.getSelectedCoordinates = function() { const selected = this._selectedCoordinates.map((coordinate) => { const feature = this.get(coordinate.feature_id); return { coordinates: feature.getCoordinate(coordinate.coord_path) }; }); return selected; }; /** * Indicates whether a feature is selected. * @param {string} featureId * @return {boolean} `true` if the feature is selected, `false` if not. */ Store.prototype.isSelected = function(featureId) { return this._selectedFeatureIds.has(featureId); }; /** * Sets a property on the given feature * @param {string} featureId * @param {string} property property * @param {string} property value * @param {Object} [options] * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. */ Store.prototype.setFeatureProperty = function(featureId, property, value, options = {}) { this.get(featureId).setProperty(property, value); this.featureChanged(featureId, { silent: options.silent, action: updateActions.CHANGE_PROPERTIES }); }; function refreshSelectedCoordinates(store, options = {}) { const newSelectedCoordinates = store._selectedCoordinates.filter(point => store._selectedFeatureIds.has(point.feature_id)); if (store._selectedCoordinates.length !== newSelectedCoordinates.length && !options.silent) { store._emitSelectionChange = true; } store._selectedCoordinates = newSelectedCoordinates; } /** * Stores the initial config for a map, so that we can set it again after we're done. */ Store.prototype.storeMapConfig = function() { interactions.forEach((interaction) => { const interactionSet = this.ctx.map[interaction]; if (interactionSet) { this._mapInitialConfig[interaction] = this.ctx.map[interaction].isEnabled(); } }); }; /** * Restores the initial config for a map, ensuring all is well. */ Store.prototype.restoreMapConfig = function() { Object.keys(this._mapInitialConfig).forEach((key) => { const value = this._mapInitialConfig[key]; if (value) { this.ctx.map[key].enable(); } else { this.ctx.map[key].disable(); } }); }; /** * Returns the initial state of an interaction setting. * @param {string} interaction * @return {boolean} `true` if the interaction is enabled, `false` if not. * Defaults to `true`. (Todo: include defaults.) */ Store.prototype.getInitialConfigValue = function(interaction) { if (this._mapInitialConfig[interaction] !== undefined) { return this._mapInitialConfig[interaction]; } else { // This needs to be set to whatever the default is for that interaction // It seems to be true for all cases currently, so let's send back `true`. return true; } }; const classTypes = ['mode', 'feature', 'mouse']; function ui(ctx) { const buttonElements = {}; let activeButton = null; let currentMapClasses = { mode: null, // e.g. mode-direct_select feature: null, // e.g. feature-vertex mouse: null // e.g. mouse-move }; let nextMapClasses = { mode: null, feature: null, mouse: null }; function clearMapClasses() { queueMapClasses({mode:null, feature:null, mouse:null}); updateMapClasses(); } function queueMapClasses(options) { nextMapClasses = Object.assign(nextMapClasses, options); } function updateMapClasses() { if (!ctx.container) return; const classesToRemove = []; const classesToAdd = []; classTypes.forEach((type) => { if (nextMapClasses[type] === currentMapClasses[type]) return; classesToRemove.push(`${type}-${currentMapClasses[type]}`); if (nextMapClasses[type] !== null) { classesToAdd.push(`${type}-${nextMapClasses[type]}`); } }); if (classesToRemove.length > 0) { ctx.container.classList.remove(...classesToRemove); } if (classesToAdd.length > 0) { ctx.container.classList.add(...classesToAdd); } currentMapClasses = Object.assign(currentMapClasses, nextMapClasses); } function createControlButton(id, options = {}) { const button = document.createElement('button'); button.className = `${classes.CONTROL_BUTTON} ${options.className}`; button.setAttribute('title', options.title); options.container.appendChild(button); button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const clickedButton = e.target; if (clickedButton === activeButton) { deactivateButtons(); options.onDeactivate(); return; } setActiveButton(id); options.onActivate(); }, true); return button; } function deactivateButtons() { if (!activeButton) return; activeButton.classList.remove(classes.ACTIVE_BUTTON); activeButton = null; } function setActiveButton(id) { deactivateButtons(); const button = buttonElements[id]; if (!button) return; if (button && id !== 'trash') { button.classList.add(classes.ACTIVE_BUTTON); activeButton = button; } } function addButtons() { const controls = ctx.options.controls; const controlGroup = document.createElement('div'); controlGroup.className = `${classes.CONTROL_GROUP} ${classes.CONTROL_BASE}`; if (!controls) return controlGroup; if (controls[types.LINE]) { buttonElements[types.LINE] = createControlButton(types.LINE, { container: controlGroup, className: classes.CONTROL_BUTTON_LINE, title: `LineString tool ${ctx.options.keybindings ? '(l)' : ''}`, onActivate: () => ctx.events.changeMode(modes$1.DRAW_LINE_STRING), onDeactivate: () => ctx.events.trash() }); } if (controls[types.POLYGON]) { buttonElements[types.POLYGON] = createControlButton(types.POLYGON, { container: controlGroup, className: classes.CONTROL_BUTTON_POLYGON, title: `Polygon tool ${ctx.options.keybindings ? '(p)' : ''}`, onActivate: () => ctx.events.changeMode(modes$1.DRAW_POLYGON), onDeactivate: () => ctx.events.trash() }); } if (controls[types.POINT]) { buttonElements[types.POINT] = createControlButton(types.POINT, { container: controlGroup, className: classes.CONTROL_BUTTON_POINT, title: `Marker tool ${ctx.options.keybindings ? '(m)' : ''}`, onActivate: () => ctx.events.changeMode(modes$1.DRAW_POINT), onDeactivate: () => ctx.events.trash() }); } if (controls.trash) { buttonElements.trash = createControlButton('trash', { container: controlGroup, className: classes.CONTROL_BUTTON_TRASH, title: 'Delete', onActivate: () => { ctx.events.trash(); } }); } if (controls.combine_features) { buttonElements.combine_features = createControlButton('combineFeatures', { container: controlGroup, className: classes.CONTROL_BUTTON_COMBINE_FEATURES, title: 'Combine', onActivate: () => { ctx.events.combineFeatures(); } }); } if (controls.uncombine_features) { buttonElements.uncombine_features = createControlButton('uncombineFeatures', { container: controlGroup, className: classes.CONTROL_BUTTON_UNCOMBINE_FEATURES, title: 'Uncombine', onActivate: () => { ctx.events.uncombineFeatures(); } }); } return controlGroup; } function removeButtons() { Object.keys(buttonElements).forEach((buttonId) => { const button = buttonElements[buttonId]; if (button.parentNode) { button.parentNode.removeChild(button); } delete buttonElements[buttonId]; }); } return { setActiveButton, queueMapClasses, updateMapClasses, clearMapClasses, addButtons, removeButtons }; } function runSetup(ctx) { let controlContainer = null; let mapLoadedInterval = null; const setup = { onRemove() { // Stop connect attempt in the event that control is removed before map is loaded ctx.map.off('load', setup.connect); clearInterval(mapLoadedInterval); setup.removeLayers(); ctx.store.restoreMapConfig(); ctx.ui.removeButtons(); ctx.events.removeEventListeners(); ctx.ui.clearMapClasses(); if (ctx.boxZoomInitial) ctx.map.boxZoom.enable(); ctx.map = null; ctx.container = null; ctx.store = null; if (controlContainer && controlContainer.parentNode) controlContainer.parentNode.removeChild(controlContainer); controlContainer = null; return this; }, connect() { ctx.map.off('load', setup.connect); clearInterval(mapLoadedInterval); setup.addLayers(); ctx.store.storeMapConfig(); ctx.events.addEventListeners(); }, onAdd(map) { ctx.map = map; ctx.events = events(ctx); ctx.ui = ui(ctx); ctx.container = map.getContainer(); ctx.store = new Store(ctx); controlContainer = ctx.ui.addButtons(); if (ctx.options.boxSelect) { ctx.boxZoomInitial = map.boxZoom.isEnabled(); map.boxZoom.disable(); const dragPanIsEnabled = map.dragPan.isEnabled(); // Need to toggle dragPan on and off or else first // dragPan disable attempt in simple_select doesn't work map.dragPan.disable(); map.dragPan.enable(); if (!dragPanIsEnabled) { map.dragPan.disable(); } } if (map.loaded()) { setup.connect(); } else { map.on('load', setup.connect); mapLoadedInterval = setInterval(() => { if (map.loaded()) setup.connect(); }, 16); } ctx.events.start(); return controlContainer; }, addLayers() { // drawn features style ctx.map.addSource(sources.COLD, { data: { type: geojsonTypes.FEATURE_COLLECTION, features: [] }, type: 'geojson' }); // hot features style ctx.map.addSource(sources.HOT, { data: { type: geojsonTypes.FEATURE_COLLECTION, features: [] }, type: 'geojson' }); ctx.options.styles.forEach((style) => { ctx.map.addLayer(style); }); ctx.store.setDirty(true); ctx.store.render(); }, // Check for layers and sources before attempting to remove // If user adds draw control and removes it before the map is loaded, layers and sources will be missing removeLayers() { ctx.options.styles.forEach((style) => { if (ctx.map.getLayer(style.id)) { ctx.map.removeLayer(style.id); } }); if (ctx.map.getSource(sources.COLD)) { ctx.map.removeSource(sources.COLD); } if (ctx.map.getSource(sources.HOT)) { ctx.map.removeSource(sources.HOT); } } }; ctx.setup = setup; return setup; } /* eslint comma-dangle: ["error", "always-multiline"] */ const blue = '#3bb2d0'; const orange = '#fbb03b'; const white = '#fff'; var styles = [ // Polygons // Solid fill // Active state defines color { 'id': 'gl-draw-polygon-fill', 'type': 'fill', 'filter': [ 'all', ['==', '$type', 'Polygon'], ], 'paint': { 'fill-color': [ 'case', ['==', ['get', 'active'], 'true'], orange, blue, ], 'fill-opacity': 0.1, }, }, // Lines // Polygon // Matches Lines AND Polygons // Active state defines color { 'id': 'gl-draw-lines', 'type': 'line', 'filter': [ 'any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon'], ], 'layout': { 'line-cap': 'round', 'line-join': 'round', }, 'paint': { 'line-color': [ 'case', ['==', ['get', 'active'], 'true'], orange, blue, ], 'line-dasharray': [ 'case', ['==', ['get', 'active'], 'true'], [0.2, 2], [2, 0], ], 'line-width': 2, }, }, // Points // Circle with an outline // Active state defines size and color { 'id': 'gl-draw-point-outer', 'type': 'circle', 'filter': [ 'all', ['==', '$type', 'Point'], ['==', 'meta', 'feature'], ], 'paint': { 'circle-radius': [ 'case', ['==', ['get', 'active'], 'true'], 7, 5, ], 'circle-color': white, }, }, { 'id': 'gl-draw-point-inner', 'type': 'circle', 'filter': [ 'all', ['==', '$type', 'Point'], ['==', 'meta', 'feature'], ], 'paint': { 'circle-radius': [ 'case', ['==', ['get', 'active'], 'true'], 5, 3, ], 'circle-color': [ 'case', ['==', ['get', 'active'], 'true'], orange, blue, ], }, }, // Vertex // Visible when editing polygons and lines // Similar behaviour to Points // Active state defines size { 'id': 'gl-draw-vertex-outer', 'type': 'circle', 'filter': [ 'all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['!=', 'mode', 'simple_select'], ], 'paint': { 'circle-radius': [ 'case', ['==', ['get', 'active'], 'true'], 7, 5, ], 'circle-color': white, }, }, { 'id': 'gl-draw-vertex-inner', 'type': 'circle', 'filter': [ 'all', ['==', '$type', 'Point'], ['==', 'meta', 'vertex'], ['!=', 'mode', 'simple_select'], ], 'paint': { 'circle-radius': [ 'case', ['==', ['get', 'active'], 'true'], 5, 3, ], 'circle-color': orange, }, }, // Midpoint // Visible when editing polygons and lines // Tapping or dragging them adds a new vertex to the feature { 'id': 'gl-draw-midpoint', 'type': 'circle', 'filter': [ 'all', ['==', 'meta', 'midpoint'], ], 'paint': { 'circle-radius': 3, 'circle-color': orange, }, }, ]; function isOfMetaType(type) { return function(e) { const featureTarget = e.featureTarget; if (!featureTarget) return false; if (!featureTarget.properties) return false; return featureTarget.properties.meta === type; }; } function isShiftMousedown(e) { if (!e.originalEvent) return false; if (!e.originalEvent.shiftKey) return false; return e.originalEvent.button === 0; } function isActiveFeature(e) { if (!e.featureTarget) return false; if (!e.featureTarget.properties) return false; return e.featureTarget.properties.active === activeStates.ACTIVE && e.featureTarget.properties.meta === meta.FEATURE; } function isInactiveFeature(e) { if (!e.featureTarget) return false; if (!e.featureTarget.properties) return false; return e.featureTarget.properties.active === activeStates.INACTIVE && e.featureTarget.properties.meta === meta.FEATURE; } function noTarget(e) { return e.featureTarget === undefined; } function isFeature(e) { if (!e.featureTarget) return false; if (!e.featureTarget.properties) return false; return e.featureTarget.properties.meta === meta.FEATURE; } function isVertex$1(e) { const featureTarget = e.featureTarget; if (!featureTarget) return false; if (!featureTarget.properties) return false; return featureTarget.properties.meta === meta.VERTEX; } function isShiftDown(e) { if (!e.originalEvent) return false; return e.originalEvent.shiftKey === true; } function isEscapeKey(e) { return e.keyCode === 27; } function isEnterKey(e) { return e.keyCode === 13; } function isTrue() { return true; } var common_selectors = /*#__PURE__*/Object.freeze({ __proto__: null, isActiveFeature: isActiveFeature, isEnterKey: isEnterKey, isEscapeKey: isEscapeKey, isFeature: isFeature, isInactiveFeature: isInactiveFeature, isOfMetaType: isOfMetaType, isShiftDown: isShiftDown, isShiftMousedown: isShiftMousedown, isTrue: isTrue, isVertex: isVertex$1, noTarget: noTarget }); /** * A standalone point geometry with useful accessor, comparison, and * modification methods. * * @class * @param {number} x the x-coordinate. This could be longitude or screen pixels, or any other sort of unit. * @param {number} y the y-coordinate. This could be latitude or screen pixels, or any other sort of unit. * * @example * const point = new Point(-77, 38); */ function Point(x, y) { this.x = x; this.y = y; } Point.prototype = { /** * Clone this point, returning a new point that can be modified * without affecting the old one. * @return {Point} the clone */ clone() { return new Point(this.x, this.y); }, /** * Add this point's x & y coordinates to another point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ add(p) { return this.clone()._add(p); }, /** * Subtract this point's x & y coordinates to from point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ sub(p) { return this.clone()._sub(p); }, /** * Multiply this point's x & y coordinates by point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ multByPoint(p) { return this.clone()._multByPoint(p); }, /** * Divide this point's x & y coordinates by point, * yielding a new point. * @param {Point} p the other point * @return {Point} output point */ divByPoint(p) { return this.clone()._divByPoint(p); }, /** * Multiply this point's x & y coordinates by a factor, * yielding a new point. * @param {number} k factor * @return {Point} output point */ mult(k) { return this.clone()._mult(k); }, /** * Divide this point's x & y coordinates by a factor, * yielding a new point. * @param {number} k factor * @return {Point} output point */ div(k) { return this.clone()._div(k); }, /** * Rotate this point around the 0, 0 origin by an angle a, * given in radians * @param {number} a angle to rotate around, in radians * @return {Point} output point */ rotate(a) { return this.clone()._rotate(a); }, /** * Rotate this point around p point by an angle a, * given in radians * @param {number} a angle to rotate around, in radians * @param {Point} p Point to rotate around * @return {Point} output point */ rotateAround(a, p) { return this.clone()._rotateAround(a, p); }, /** * Multiply this point by a 4x1 transformation matrix * @param {[number, number, number, number]} m transformation matrix * @return {Point} output point */ matMult(m) { return this.clone()._matMult(m); }, /** * Calculate this point but as a unit vector from 0, 0, meaning * that the distance from the resulting point to the 0, 0 * coordinate will be equal to 1 and the angle from the resulting * point to the 0, 0 coordinate will be the same as before. * @return {Point} unit vector point */ unit() { return this.clone()._unit(); }, /** * Compute a perpendicular point, where the new y coordinate * is the old x coordinate and the new x coordinate is the old y * coordinate multiplied by -1 * @return {Point} perpendicular point */ perp() { return this.clone()._perp(); }, /** * Return a version of this point with the x & y coordinates * rounded to integers. * @return {Point} rounded point */ round() { return this.clone()._round(); }, /** * Return the magnitude of this point: this is the Euclidean * distance from the 0, 0 coordinate to this point's x and y * coordinates. * @return {number} magnitude */ mag() { return Math.sqrt(this.x * this.x + this.y * this.y); }, /** * Judge whether this point is equal to another point, returning * true or false. * @param {Point} other the other point * @return {boolean} whether the points are equal */ equals(other) { return this.x === other.x && this.y === other.y; }, /** * Calculate the distance from this point to another point * @param {Point} p the other point * @return {number} distance */ dist(p) { return Math.sqrt(this.distSqr(p)); }, /** * Calculate the distance from this point to another point, * without the square root step. Useful if you're comparing * relative distances. * @param {Point} p the other point * @return {number} distance */ distSqr(p) { const dx = p.x - this.x, dy = p.y - this.y; return dx * dx + dy * dy; }, /** * Get the angle from the 0, 0 coordinate to this point, in radians * coordinates. * @return {number} angle */ angle() { return Math.atan2(this.y, this.x); }, /** * Get the angle from this point to another point, in radians * @param {Point} b the other point * @return {number} angle */ angleTo(b) { return Math.atan2(this.y - b.y, this.x - b.x); }, /** * Get the angle between this point and another point, in radians * @param {Point} b the other point * @return {number} angle */ angleWith(b) { return this.angleWithSep(b.x, b.y); }, /** * Find the angle of the two vectors, solving the formula for * the cross product a x b = |a||b|sin(θ) for θ. * @param {number} x the x-coordinate * @param {number} y the y-coordinate * @return {number} the angle in radians */ angleWithSep(x, y) { return Math.atan2( this.x * y - this.y * x, this.x * x + this.y * y); }, /** @param {[number, number, number, number]} m */ _matMult(m) { const x = m[0] * this.x + m[1] * this.y, y = m[2] * this.x + m[3] * this.y; this.x = x; this.y = y; return this; }, /** @param {Point} p */ _add(p) { this.x += p.x; this.y += p.y; return this; }, /** @param {Point} p */ _sub(p) { this.x -= p.x; this.y -= p.y; return this; }, /** @param {number} k */ _mult(k) { this.x *= k; this.y *= k; return this; }, /** @param {number} k */ _div(k) { this.x /= k; this.y /= k; return this; }, /** @param {Point} p */ _multByPoint(p) { this.x *= p.x; this.y *= p.y; return this; }, /** @param {Point} p */ _divByPoint(p) { this.x /= p.x; this.y /= p.y; return this; }, _unit() { this._div(this.mag()); return this; }, _perp() { const y = this.y; this.y = this.x; this.x = -y; return this; }, /** @param {number} angle */ _rotate(angle) { const cos = Math.cos(angle), sin = Math.sin(angle), x = cos * this.x - sin * this.y, y = sin * this.x + cos * this.y; this.x = x; this.y = y; return this; }, /** * @param {number} angle * @param {Point} p */ _rotateAround(angle, p) { const cos = Math.cos(angle), sin = Math.sin(angle), x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y), y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y); this.x = x; this.y = y; return this; }, _round() { this.x = Math.round(this.x); this.y = Math.round(this.y); return this; }, constructor: Point }; /** * Construct a point from an array if necessary, otherwise if the input * is already a Point, return it unchanged. * @param {Point | [number, number] | {x: number, y: number}} p input value * @return {Point} constructed point. * @example * // this * var point = Point.convert([0, 1]); * // is equivalent to * var point = new Point(0, 1); */ Point.convert = function (p) { if (p instanceof Point) { return /** @type {Point} */ (p); } if (Array.isArray(p)) { return new Point(+p[0], +p[1]); } if (p.x !== undefined && p.y !== undefined) { return new Point(+p.x, +p.y); } throw new Error('Expected [x, y] or {x, y} point format'); }; /** * Returns a Point representing a mouse event's position * relative to a containing element. * * @param {MouseEvent} mouseEvent * @param {Node} container * @returns {Point} */ function mouseEventPoint(mouseEvent, container) { const rect = container.getBoundingClientRect(); return new Point( mouseEvent.clientX - rect.left - (container.clientLeft || 0), mouseEvent.clientY - rect.top - (container.clientTop || 0) ); } /** * Returns GeoJSON for a Point representing the * vertex of another feature. * * @param {string} parentId * @param {Array} coordinates * @param {string} path - Dot-separated numbers indicating exactly * where the point exists within its parent feature's coordinates. * @param {boolean} selected * @return {GeoJSON} Point */ function createVertex(parentId, coordinates, path, selected) { return { type: geojsonTypes.FEATURE, properties: { meta: meta.VERTEX, parent: parentId, coord_path: path, active: (selected) ? activeStates.ACTIVE : activeStates.INACTIVE }, geometry: { type: geojsonTypes.POINT, coordinates } }; } function createMidpoint(parent, startVertex, endVertex) { const startCoord = startVertex.geometry.coordinates; const endCoord = endVertex.geometry.coordinates; // If a coordinate exceeds the projection, we can't calculate a midpoint, // so run away if (startCoord[1] > LAT_RENDERED_MAX$1 || startCoord[1] < LAT_RENDERED_MIN$1 || endCoord[1] > LAT_RENDERED_MAX$1 || endCoord[1] < LAT_RENDERED_MIN$1) { return null; } const mid = { lng: (startCoord[0] + endCoord[0]) / 2, lat: (startCoord[1] + endCoord[1]) / 2 }; return { type: geojsonTypes.FEATURE, properties: { meta: meta.MIDPOINT, parent, lng: mid.lng, lat: mid.lat, coord_path: endVertex.properties.coord_path }, geometry: { type: geojsonTypes.POINT, coordinates: [mid.lng, mid.lat] } }; } function createSupplementaryPoints(geojson, options = {}, basePath = null) { const { type, coordinates } = geojson.geometry; const featureId = geojson.properties && geojson.properties.id; let supplementaryPoints = []; if (type === geojsonTypes.POINT) { // For points, just create a vertex supplementaryPoints.push(createVertex(featureId, coordinates, basePath, isSelectedPath(basePath))); } else if (type === geojsonTypes.POLYGON) { // Cycle through a Polygon's rings and // process each line coordinates.forEach((line, lineIndex) => { processLine(line, (basePath !== null) ? `${basePath}.${lineIndex}` : String(lineIndex)); }); } else if (type === geojsonTypes.LINE_STRING) { processLine(coordinates, basePath); } else if (type.indexOf(geojsonTypes.MULTI_PREFIX) === 0) { processMultiGeometry(); } function processLine(line, lineBasePath) { let firstPointString = ''; let lastVertex = null; line.forEach((point, pointIndex) => { const pointPath = (lineBasePath !== undefined && lineBasePath !== null) ? `${lineBasePath}.${pointIndex}` : String(pointIndex); const vertex = createVertex(featureId, point, pointPath, isSelectedPath(pointPath)); // If we're creating midpoints, check if there was a // vertex before this one. If so, add a midpoint // between that vertex and this one. if (options.midpoints && lastVertex) { const midpoint = createMidpoint(featureId, lastVertex, vertex); if (midpoint) { supplementaryPoints.push(midpoint); } } lastVertex = vertex; // A Polygon line's last point is the same as the first point. If we're on the last // point, we want to draw a midpoint before it but not another vertex on it // (since we already a vertex there, from the first point). const stringifiedPoint = JSON.stringify(point); if (firstPointString !== stringifiedPoint) { supplementaryPoints.push(vertex); } if (pointIndex === 0) { firstPointString = stringifiedPoint; } }); } function isSelectedPath(path) { if (!options.selectedPaths) return false; return options.selectedPaths.indexOf(path) !== -1; } // Split a multi-geometry into constituent // geometries, and accumulate the supplementary points // for each of those constituents function processMultiGeometry() { const subType = type.replace(geojsonTypes.MULTI_PREFIX, ''); coordinates.forEach((subCoordinates, index) => { const subFeature = { type: geojsonTypes.FEATURE, properties: geojson.properties, geometry: { type: subType, coordinates: subCoordinates } }; supplementaryPoints = supplementaryPoints.concat(createSupplementaryPoints(subFeature, options, index)); }); } return supplementaryPoints; } var doubleClickZoom = { enable(ctx) { setTimeout(() => { // First check we've got a map and some context. if (!ctx.map || !ctx.map.doubleClickZoom || !ctx._ctx || !ctx._ctx.store || !ctx._ctx.store.getInitialConfigValue) return; // Now check initial state wasn't false (we leave it disabled if so) if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return; ctx.map.doubleClickZoom.enable(); }, 0); }, disable(ctx) { setTimeout(() => { if (!ctx.map || !ctx.map.doubleClickZoom) return; // Always disable here, as it's necessary in some cases. ctx.map.doubleClickZoom.disable(); }, 0); } }; const { LAT_MIN, LAT_MAX, LAT_RENDERED_MIN, LAT_RENDERED_MAX, LNG_MIN, LNG_MAX, } = Constants; function extent(feature) { const depth = { Point: 0, LineString: 1, Polygon: 2, MultiPoint: 1, MultiLineString: 2, MultiPolygon: 3, }[feature.geometry.type]; const coords = [feature.geometry.coordinates].flat(depth); const lngs = coords.map(coord => coord[0]); const lats = coords.map(coord => coord[1]); const min = vals => Math.min.apply(null, vals); const max = vals => Math.max.apply(null, vals); return [min(lngs), min(lats), max(lngs), max(lats)]; } // Ensure that we do not drag north-south far enough for // - any part of any feature to exceed the poles // - any feature to be completely lost in the space between the projection's // edge and the poles, such that it couldn't be re-selected and moved back function constrainFeatureMovement(geojsonFeatures, delta) { // "inner edge" = a feature's latitude closest to the equator let northInnerEdge = LAT_MIN; let southInnerEdge = LAT_MAX; // "outer edge" = a feature's latitude furthest from the equator let northOuterEdge = LAT_MIN; let southOuterEdge = LAT_MAX; let westEdge = LNG_MAX; let eastEdge = LNG_MIN; geojsonFeatures.forEach((feature) => { const bounds = extent(feature); const featureSouthEdge = bounds[1]; const featureNorthEdge = bounds[3]; const featureWestEdge = bounds[0]; const featureEastEdge = bounds[2]; if (featureSouthEdge > northInnerEdge) northInnerEdge = featureSouthEdge; if (featureNorthEdge < southInnerEdge) southInnerEdge = featureNorthEdge; if (featureNorthEdge > northOuterEdge) northOuterEdge = featureNorthEdge; if (featureSouthEdge < southOuterEdge) southOuterEdge = featureSouthEdge; if (featureWestEdge < westEdge) westEdge = featureWestEdge; if (featureEastEdge > eastEdge) eastEdge = featureEastEdge; }); // These changes are not mutually exclusive: we might hit the inner // edge but also have hit the outer edge and therefore need // another readjustment const constrainedDelta = delta; if (northInnerEdge + constrainedDelta.lat > LAT_RENDERED_MAX) { constrainedDelta.lat = LAT_RENDERED_MAX - northInnerEdge; } if (northOuterEdge + constrainedDelta.lat > LAT_MAX) { constrainedDelta.lat = LAT_MAX - northOuterEdge; } if (southInnerEdge + constrainedDelta.lat < LAT_RENDERED_MIN) { constrainedDelta.lat = LAT_RENDERED_MIN - southInnerEdge; } if (southOuterEdge + constrainedDelta.lat < LAT_MIN) { constrainedDelta.lat = LAT_MIN - southOuterEdge; } if (westEdge + constrainedDelta.lng <= LNG_MIN) { constrainedDelta.lng += Math.ceil(Math.abs(constrainedDelta.lng) / 360) * 360; } if (eastEdge + constrainedDelta.lng >= LNG_MAX) { constrainedDelta.lng -= Math.ceil(Math.abs(constrainedDelta.lng) / 360) * 360; } return constrainedDelta; } function moveFeatures(features, delta) { const constrainedDelta = constrainFeatureMovement(features.map(feature => feature.toGeoJSON()), delta); features.forEach((feature) => { const currentCoordinates = feature.getCoordinates(); const moveCoordinate = (coord) => { const point = { lng: coord[0] + constrainedDelta.lng, lat: coord[1] + constrainedDelta.lat }; return [point.lng, point.lat]; }; const moveRing = ring => ring.map(coord => moveCoordinate(coord)); const moveMultiPolygon = multi => multi.map(ring => moveRing(ring)); let nextCoordinates; if (feature.type === geojsonTypes.POINT) { nextCoordinates = moveCoordinate(currentCoordinates); } else if (feature.type === geojsonTypes.LINE_STRING || feature.type === geojsonTypes.MULTI_POINT) { nextCoordinates = currentCoordinates.map(moveCoordinate); } else if (feature.type === geojsonTypes.POLYGON || feature.type === geojsonTypes.MULTI_LINE_STRING) { nextCoordinates = currentCoordinates.map(moveRing); } else if (feature.type === geojsonTypes.MULTI_POLYGON) { nextCoordinates = currentCoordinates.map(moveMultiPolygon); } feature.incomingCoords(nextCoordinates); }); } const SimpleSelect = {}; SimpleSelect.onSetup = function(opts) { // turn the opts into state. const state = { dragMoveLocation: null, boxSelectStartLocation: null, boxSelectElement: undefined, boxSelecting: false, canBoxSelect: false, dragMoving: false, canDragMove: false, initialDragPanState: this.map.dragPan.isEnabled(), initiallySelectedFeatureIds: opts.featureIds || [] }; this.setSelected(state.initiallySelectedFeatureIds.filter(id => this.getFeature(id) !== undefined)); this.fireActionable(); this.setActionableState({ combineFeatures: true, uncombineFeatures: true, trash: true }); return state; }; SimpleSelect.fireUpdate = function() { this.fire(events$1.UPDATE, { action: updateActions.MOVE, features: this.getSelected().map(f => f.toGeoJSON()) }); }; SimpleSelect.fireActionable = function() { const selectedFeatures = this.getSelected(); const multiFeatures = selectedFeatures.filter( feature => this.isInstanceOf('MultiFeature', feature) ); let combineFeatures = false; if (selectedFeatures.length > 1) { combineFeatures = true; const featureType = selectedFeatures[0].type.replace('Multi', ''); selectedFeatures.forEach((feature) => { if (feature.type.replace('Multi', '') !== featureType) { combineFeatures = false; } }); } const uncombineFeatures = multiFeatures.length > 0; const trash = selectedFeatures.length > 0; this.setActionableState({ combineFeatures, uncombineFeatures, trash }); }; SimpleSelect.getUniqueIds = function(allFeatures) { if (!allFeatures.length) return []; const ids = allFeatures.map(s => s.properties.id) .filter(id => id !== undefined) .reduce((memo, id) => { memo.add(id); return memo; }, new StringSet()); return ids.values(); }; SimpleSelect.stopExtendedInteractions = function(state) { if (state.boxSelectElement) { if (state.boxSelectElement.parentNode) state.boxSelectElement.parentNode.removeChild(state.boxSelectElement); state.boxSelectElement = null; } if ((state.canDragMove || state.canBoxSelect) && state.initialDragPanState === true) { this.map.dragPan.enable(); } state.boxSelecting = false; state.canBoxSelect = false; state.dragMoving = false; state.canDragMove = false; }; SimpleSelect.onStop = function() { doubleClickZoom.enable(this); }; SimpleSelect.onMouseMove = function(state, e) { const isFeature$1 = isFeature(e); if (isFeature$1 && state.dragMoving) this.fireUpdate(); // On mousemove that is not a drag, stop extended interactions. // This is useful if you drag off the canvas, release the button, // then move the mouse back over the canvas --- we don't allow the // interaction to continue then, but we do let it continue if you held // the mouse button that whole time this.stopExtendedInteractions(state); // Skip render return true; }; SimpleSelect.onMouseOut = function(state) { // As soon as you mouse leaves the canvas, update the feature if (state.dragMoving) return this.fireUpdate(); // Skip render return true; }; SimpleSelect.onTap = SimpleSelect.onClick = function(state, e) { // Click (with or without shift) on no feature if (noTarget(e)) return this.clickAnywhere(state, e); // also tap if (isOfMetaType(meta.VERTEX)(e)) return this.clickOnVertex(state, e); //tap if (isFeature(e)) return this.clickOnFeature(state, e); }; SimpleSelect.clickAnywhere = function (state) { // Clear the re-render selection const wasSelected = this.getSelectedIds(); if (wasSelected.length) { this.clearSelectedFeatures(); wasSelected.forEach(id => this.doRender(id)); } doubleClickZoom.enable(this); this.stopExtendedInteractions(state); }; SimpleSelect.clickOnVertex = function(state, e) { // Enter direct select mode this.changeMode(modes$1.DIRECT_SELECT, { featureId: e.featureTarget.properties.parent, coordPath: e.featureTarget.properties.coord_path, startPos: e.lngLat }); this.updateUIClasses({ mouse: cursors.MOVE }); }; SimpleSelect.startOnActiveFeature = function(state, e) { // Stop any already-underway extended interactions this.stopExtendedInteractions(state); // Disable map.dragPan immediately so it can't start this.map.dragPan.disable(); // Re-render it and enable drag move this.doRender(e.featureTarget.properties.id); // Set up the state for drag moving state.canDragMove = true; state.dragMoveLocation = e.lngLat; }; SimpleSelect.clickOnFeature = function(state, e) { // Stop everything doubleClickZoom.disable(this); this.stopExtendedInteractions(state); const isShiftClick = isShiftDown(e); const selectedFeatureIds = this.getSelectedIds(); const featureId = e.featureTarget.properties.id; const isFeatureSelected = this.isSelected(featureId); // Click (without shift) on any selected feature but a point if (!isShiftClick && isFeatureSelected && this.getFeature(featureId).type !== geojsonTypes.POINT) { // Enter direct select mode return this.changeMode(modes$1.DIRECT_SELECT, { featureId }); } // Shift-click on a selected feature if (isFeatureSelected && isShiftClick) { // Deselect it this.deselect(featureId); this.updateUIClasses({ mouse: cursors.POINTER }); if (selectedFeatureIds.length === 1) { doubleClickZoom.enable(this); } // Shift-click on an unselected feature } else if (!isFeatureSelected && isShiftClick) { // Add it to the selection this.select(featureId); this.updateUIClasses({ mouse: cursors.MOVE }); // Click (without shift) on an unselected feature } else if (!isFeatureSelected && !isShiftClick) { // Make it the only selected feature selectedFeatureIds.forEach(id => this.doRender(id)); this.setSelected(featureId); this.updateUIClasses({ mouse: cursors.MOVE }); } // No matter what, re-render the clicked feature this.doRender(featureId); }; SimpleSelect.onMouseDown = function(state, e) { state.initialDragPanState = this.map.dragPan.isEnabled(); if (isActiveFeature(e)) return this.startOnActiveFeature(state, e); if (this.drawConfig.boxSelect && isShiftMousedown(e)) return this.startBoxSelect(state, e); }; SimpleSelect.startBoxSelect = function(state, e) { this.stopExtendedInteractions(state); this.map.dragPan.disable(); // Enable box select state.boxSelectStartLocation = mouseEventPoint(e.originalEvent, this.map.getContainer()); state.canBoxSelect = true; }; SimpleSelect.onTouchStart = function(state, e) { if (isActiveFeature(e)) return this.startOnActiveFeature(state, e); }; SimpleSelect.onDrag = function(state, e) { if (state.canDragMove) return this.dragMove(state, e); if (this.drawConfig.boxSelect && state.canBoxSelect) return this.whileBoxSelect(state, e); }; SimpleSelect.whileBoxSelect = function(state, e) { state.boxSelecting = true; this.updateUIClasses({ mouse: cursors.ADD }); // Create the box node if it doesn't exist if (!state.boxSelectElement) { state.boxSelectElement = document.createElement('div'); state.boxSelectElement.classList.add(classes.BOX_SELECT); this.map.getContainer().appendChild(state.boxSelectElement); } // Adjust the box node's width and xy position const current = mouseEventPoint(e.originalEvent, this.map.getContainer()); const minX = Math.min(state.boxSelectStartLocation.x, current.x); const maxX = Math.max(state.boxSelectStartLocation.x, current.x); const minY = Math.min(state.boxSelectStartLocation.y, current.y); const maxY = Math.max(state.boxSelectStartLocation.y, current.y); const translateValue = `translate(${minX}px, ${minY}px)`; state.boxSelectElement.style.transform = translateValue; state.boxSelectElement.style.WebkitTransform = translateValue; state.boxSelectElement.style.width = `${maxX - minX}px`; state.boxSelectElement.style.height = `${maxY - minY}px`; }; SimpleSelect.dragMove = function(state, e) { // Dragging when drag move is enabled state.dragMoving = true; e.originalEvent.stopPropagation(); const delta = { lng: e.lngLat.lng - state.dragMoveLocation.lng, lat: e.lngLat.lat - state.dragMoveLocation.lat }; moveFeatures(this.getSelected(), delta); state.dragMoveLocation = e.lngLat; }; SimpleSelect.onTouchEnd = SimpleSelect.onMouseUp = function(state, e) { // End any extended interactions if (state.dragMoving) { this.fireUpdate(); } else if (state.boxSelecting) { const bbox = [ state.boxSelectStartLocation, mouseEventPoint(e.originalEvent, this.map.getContainer()) ]; const featuresInBox = this.featuresAt(null, bbox, 'click'); const idsToSelect = this.getUniqueIds(featuresInBox) .filter(id => !this.isSelected(id)); if (idsToSelect.length) { this.select(idsToSelect); idsToSelect.forEach(id => this.doRender(id)); this.updateUIClasses({ mouse: cursors.MOVE }); } } this.stopExtendedInteractions(state); }; SimpleSelect.toDisplayFeatures = function(state, geojson, display) { geojson.properties.active = (this.isSelected(geojson.properties.id)) ? activeStates.ACTIVE : activeStates.INACTIVE; display(geojson); this.fireActionable(); if (geojson.properties.active !== activeStates.ACTIVE || geojson.geometry.type === geojsonTypes.POINT) return; createSupplementaryPoints(geojson).forEach(display); }; SimpleSelect.onTrash = function() { this.deleteFeature(this.getSelectedIds()); this.fireActionable(); }; SimpleSelect.onCombineFeatures = function() { const selectedFeatures = this.getSelected(); if (selectedFeatures.length === 0 || selectedFeatures.length < 2) return; const coordinates = [], featuresCombined = []; const featureType = selectedFeatures[0].type.replace('Multi', ''); for (let i = 0; i < selectedFeatures.length; i++) { const feature = selectedFeatures[i]; if (feature.type.replace('Multi', '') !== featureType) { return; } if (feature.type.includes('Multi')) { feature.getCoordinates().forEach((subcoords) => { coordinates.push(subcoords); }); } else { coordinates.push(feature.getCoordinates()); } featuresCombined.push(feature.toGeoJSON()); } if (featuresCombined.length > 1) { const multiFeature = this.newFeature({ type: geojsonTypes.FEATURE, properties: featuresCombined[0].properties, geometry: { type: `Multi${featureType}`, coordinates } }); this.addFeature(multiFeature); this.deleteFeature(this.getSelectedIds(), { silent: true }); this.setSelected([multiFeature.id]); this.fire(events$1.COMBINE_FEATURES, { createdFeatures: [multiFeature.toGeoJSON()], deletedFeatures: featuresCombined }); } this.fireActionable(); }; SimpleSelect.onUncombineFeatures = function() { const selectedFeatures = this.getSelected(); if (selectedFeatures.length === 0) return; const createdFeatures = []; const featuresUncombined = []; for (let i = 0; i < selectedFeatures.length; i++) { const feature = selectedFeatures[i]; if (this.isInstanceOf('MultiFeature', feature)) { feature.getFeatures().forEach((subFeature) => { this.addFeature(subFeature); subFeature.properties = feature.properties; createdFeatures.push(subFeature.toGeoJSON()); this.select([subFeature.id]); }); this.deleteFeature(feature.id, { silent: true }); featuresUncombined.push(feature.toGeoJSON()); } } if (createdFeatures.length > 1) { this.fire(events$1.UNCOMBINE_FEATURES, { createdFeatures, deletedFeatures: featuresUncombined }); } this.fireActionable(); }; const isVertex = isOfMetaType(meta.VERTEX); const isMidpoint = isOfMetaType(meta.MIDPOINT); const DirectSelect = {}; // INTERNAL FUCNTIONS DirectSelect.fireUpdate = function() { this.fire(events$1.UPDATE, { action: updateActions.CHANGE_COORDINATES, features: this.getSelected().map(f => f.toGeoJSON()) }); }; DirectSelect.fireActionable = function(state) { this.setActionableState({ combineFeatures: false, uncombineFeatures: false, trash: state.selectedCoordPaths.length > 0 }); }; DirectSelect.startDragging = function(state, e) { state.initialDragPanState = this.map.dragPan.isEnabled(); this.map.dragPan.disable(); state.canDragMove = true; state.dragMoveLocation = e.lngLat; }; DirectSelect.stopDragging = function(state) { if (state.canDragMove && state.initialDragPanState === true) { this.map.dragPan.enable(); } state.dragMoving = false; state.canDragMove = false; state.dragMoveLocation = null; }; DirectSelect.onVertex = function (state, e) { this.startDragging(state, e); const about = e.featureTarget.properties; const selectedIndex = state.selectedCoordPaths.indexOf(about.coord_path); if (!isShiftDown(e) && selectedIndex === -1) { state.selectedCoordPaths = [about.coord_path]; } else if (isShiftDown(e) && selectedIndex === -1) { state.selectedCoordPaths.push(about.coord_path); } const selectedCoordinates = this.pathsToCoordinates(state.featureId, state.selectedCoordPaths); this.setSelectedCoordinates(selectedCoordinates); }; DirectSelect.onMidpoint = function(state, e) { this.startDragging(state, e); const about = e.featureTarget.properties; state.feature.addCoordinate(about.coord_path, about.lng, about.lat); this.fireUpdate(); state.selectedCoordPaths = [about.coord_path]; }; DirectSelect.pathsToCoordinates = function(featureId, paths) { return paths.map(coord_path => ({ feature_id: featureId, coord_path })); }; DirectSelect.onFeature = function(state, e) { if (state.selectedCoordPaths.length === 0) this.startDragging(state, e); else this.stopDragging(state); }; DirectSelect.dragFeature = function(state, e, delta) { moveFeatures(this.getSelected(), delta); state.dragMoveLocation = e.lngLat; }; DirectSelect.dragVertex = function(state, e, delta) { const selectedCoords = state.selectedCoordPaths.map(coord_path => state.feature.getCoordinate(coord_path)); const selectedCoordPoints = selectedCoords.map(coords => ({ type: geojsonTypes.FEATURE, properties: {}, geometry: { type: geojsonTypes.POINT, coordinates: coords } })); const constrainedDelta = constrainFeatureMovement(selectedCoordPoints, delta); for (let i = 0; i < selectedCoords.length; i++) { const coord = selectedCoords[i]; state.feature.updateCoordinate(state.selectedCoordPaths[i], coord[0] + constrainedDelta.lng, coord[1] + constrainedDelta.lat); } }; DirectSelect.clickNoTarget = function () { this.changeMode(modes$1.SIMPLE_SELECT); }; DirectSelect.clickInactive = function () { this.changeMode(modes$1.SIMPLE_SELECT); }; DirectSelect.clickActiveFeature = function (state) { state.selectedCoordPaths = []; this.clearSelectedCoordinates(); state.feature.changed(); }; // EXTERNAL FUNCTIONS DirectSelect.onSetup = function(opts) { const featureId = opts.featureId; const feature = this.getFeature(featureId); if (!feature) { throw new Error('You must provide a featureId to enter direct_select mode'); } if (feature.type === geojsonTypes.POINT) { throw new TypeError('direct_select mode doesn\'t handle point features'); } const state = { featureId, feature, dragMoveLocation: opts.startPos || null, dragMoving: false, canDragMove: false, selectedCoordPaths: opts.coordPath ? [opts.coordPath] : [], }; this.setSelectedCoordinates(this.pathsToCoordinates(featureId, state.selectedCoordPaths)); this.setSelected(featureId); doubleClickZoom.disable(this); this.setActionableState({ trash: true }); return state; }; DirectSelect.onStop = function() { doubleClickZoom.enable(this); this.clearSelectedCoordinates(); }; DirectSelect.toDisplayFeatures = function(state, geojson, push) { if (state.featureId === geojson.properties.id) { geojson.properties.active = activeStates.ACTIVE; push(geojson); createSupplementaryPoints(geojson, { map: this.map, midpoints: true, selectedPaths: state.selectedCoordPaths }).forEach(push); } else { geojson.properties.active = activeStates.INACTIVE; push(geojson); } this.fireActionable(state); }; DirectSelect.onTrash = function(state) { // Uses number-aware sorting to make sure '9' < '10'. Comparison is reversed because we want them // in reverse order so that we can remove by index safely. state.selectedCoordPaths .sort((a, b) => b.localeCompare(a, 'en', { numeric: true })) .forEach(id => state.feature.removeCoordinate(id)); this.fireUpdate(); state.selectedCoordPaths = []; this.clearSelectedCoordinates(); this.fireActionable(state); if (state.feature.isValid() === false) { this.deleteFeature([state.featureId]); this.changeMode(modes$1.SIMPLE_SELECT, {}); } }; DirectSelect.onMouseMove = function(state, e) { // On mousemove that is not a drag, stop vertex movement. const isFeature = isActiveFeature(e); const onVertex = isVertex(e); const isMidPoint = isMidpoint(e); const noCoords = state.selectedCoordPaths.length === 0; if (isFeature && noCoords) this.updateUIClasses({ mouse: cursors.MOVE }); else if (onVertex && !noCoords) this.updateUIClasses({ mouse: cursors.MOVE }); else this.updateUIClasses({ mouse: cursors.NONE }); const isDraggableItem = onVertex || isFeature || isMidPoint; if (isDraggableItem && state.dragMoving) this.fireUpdate(); this.stopDragging(state); // Skip render return true; }; DirectSelect.onMouseOut = function(state) { // As soon as you mouse leaves the canvas, update the feature if (state.dragMoving) this.fireUpdate(); // Skip render return true; }; DirectSelect.onTouchStart = DirectSelect.onMouseDown = function(state, e) { if (isVertex(e)) return this.onVertex(state, e); if (isActiveFeature(e)) return this.onFeature(state, e); if (isMidpoint(e)) return this.onMidpoint(state, e); }; DirectSelect.onDrag = function(state, e) { if (state.canDragMove !== true) return; state.dragMoving = true; e.originalEvent.stopPropagation(); const delta = { lng: e.lngLat.lng - state.dragMoveLocation.lng, lat: e.lngLat.lat - state.dragMoveLocation.lat }; if (state.selectedCoordPaths.length > 0) this.dragVertex(state, e, delta); else this.dragFeature(state, e, delta); state.dragMoveLocation = e.lngLat; }; DirectSelect.onClick = function(state, e) { if (noTarget(e)) return this.clickNoTarget(state, e); if (isActiveFeature(e)) return this.clickActiveFeature(state, e); if (isInactiveFeature(e)) return this.clickInactive(state, e); this.stopDragging(state); }; DirectSelect.onTap = function(state, e) { if (noTarget(e)) return this.clickNoTarget(state, e); if (isActiveFeature(e)) return this.clickActiveFeature(state, e); if (isInactiveFeature(e)) return this.clickInactive(state, e); }; DirectSelect.onTouchEnd = DirectSelect.onMouseUp = function(state) { if (state.dragMoving) { this.fireUpdate(); } this.stopDragging(state); }; const DrawPoint = {}; DrawPoint.onSetup = function() { const point = this.newFeature({ type: geojsonTypes.FEATURE, properties: {}, geometry: { type: geojsonTypes.POINT, coordinates: [] } }); this.addFeature(point); this.clearSelectedFeatures(); this.updateUIClasses({ mouse: cursors.ADD }); this.activateUIButton(types.POINT); this.setActionableState({ trash: true }); return { point }; }; DrawPoint.stopDrawingAndRemove = function(state) { this.deleteFeature([state.point.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT); }; DrawPoint.onTap = DrawPoint.onClick = function(state, e) { this.updateUIClasses({ mouse: cursors.MOVE }); state.point.updateCoordinate('', e.lngLat.lng, e.lngLat.lat); this.fire(events$1.CREATE, { features: [state.point.toGeoJSON()] }); this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.point.id] }); }; DrawPoint.onStop = function(state) { this.activateUIButton(); if (!state.point.getCoordinate().length) { this.deleteFeature([state.point.id], { silent: true }); } }; DrawPoint.toDisplayFeatures = function(state, geojson, display) { // Never render the point we're drawing const isActivePoint = geojson.properties.id === state.point.id; geojson.properties.active = (isActivePoint) ? activeStates.ACTIVE : activeStates.INACTIVE; if (!isActivePoint) return display(geojson); }; DrawPoint.onTrash = DrawPoint.stopDrawingAndRemove; DrawPoint.onKeyUp = function(state, e) { if (isEscapeKey(e) || isEnterKey(e)) { return this.stopDrawingAndRemove(state, e); } }; function isEventAtCoordinates(event, coordinates) { if (!event.lngLat) return false; return event.lngLat.lng === coordinates[0] && event.lngLat.lat === coordinates[1]; } const DrawPolygon = {}; DrawPolygon.onSetup = function() { const polygon = this.newFeature({ type: geojsonTypes.FEATURE, properties: {}, geometry: { type: geojsonTypes.POLYGON, coordinates: [[]] } }); this.addFeature(polygon); this.clearSelectedFeatures(); doubleClickZoom.disable(this); this.updateUIClasses({ mouse: cursors.ADD }); this.activateUIButton(types.POLYGON); this.setActionableState({ trash: true }); return { polygon, currentVertexPosition: 0 }; }; DrawPolygon.clickAnywhere = function(state, e) { if (state.currentVertexPosition > 0 && isEventAtCoordinates(e, state.polygon.coordinates[0][state.currentVertexPosition - 1])) { return this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.polygon.id] }); } this.updateUIClasses({ mouse: cursors.ADD }); state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, e.lngLat.lng, e.lngLat.lat); state.currentVertexPosition++; state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, e.lngLat.lng, e.lngLat.lat); }; DrawPolygon.clickOnVertex = function(state) { return this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.polygon.id] }); }; DrawPolygon.onMouseMove = function(state, e) { state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, e.lngLat.lng, e.lngLat.lat); if (isVertex$1(e)) { this.updateUIClasses({ mouse: cursors.POINTER }); } }; DrawPolygon.onTap = DrawPolygon.onClick = function(state, e) { if (isVertex$1(e)) return this.clickOnVertex(state, e); return this.clickAnywhere(state, e); }; DrawPolygon.onKeyUp = function(state, e) { if (isEscapeKey(e)) { this.deleteFeature([state.polygon.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT); } else if (isEnterKey(e)) { this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.polygon.id] }); } }; DrawPolygon.onStop = function(state) { this.updateUIClasses({ mouse: cursors.NONE }); doubleClickZoom.enable(this); this.activateUIButton(); // check to see if we've deleted this feature if (this.getFeature(state.polygon.id) === undefined) return; //remove last added coordinate state.polygon.removeCoordinate(`0.${state.currentVertexPosition}`); if (state.polygon.isValid()) { this.fire(events$1.CREATE, { features: [state.polygon.toGeoJSON()] }); } else { this.deleteFeature([state.polygon.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT, {}, { silent: true }); } }; DrawPolygon.toDisplayFeatures = function(state, geojson, display) { const isActivePolygon = geojson.properties.id === state.polygon.id; geojson.properties.active = (isActivePolygon) ? activeStates.ACTIVE : activeStates.INACTIVE; if (!isActivePolygon) return display(geojson); // Don't render a polygon until it has two positions // (and a 3rd which is just the first repeated) if (geojson.geometry.coordinates.length === 0) return; const coordinateCount = geojson.geometry.coordinates[0].length; // 2 coordinates after selecting a draw type // 3 after creating the first point if (coordinateCount < 3) { return; } geojson.properties.meta = meta.FEATURE; display(createVertex(state.polygon.id, geojson.geometry.coordinates[0][0], '0.0', false)); if (coordinateCount > 3) { // Add a start position marker to the map, clicking on this will finish the feature // This should only be shown when we're in a valid spot const endPos = geojson.geometry.coordinates[0].length - 3; display(createVertex(state.polygon.id, geojson.geometry.coordinates[0][endPos], `0.${endPos}`, false)); } if (coordinateCount <= 4) { // If we've only drawn two positions (plus the closer), // make a LineString instead of a Polygon const lineCoordinates = [ [geojson.geometry.coordinates[0][0][0], geojson.geometry.coordinates[0][0][1]], [geojson.geometry.coordinates[0][1][0], geojson.geometry.coordinates[0][1][1]] ]; // create an initial vertex so that we can track the first point on mobile devices display({ type: geojsonTypes.FEATURE, properties: geojson.properties, geometry: { coordinates: lineCoordinates, type: geojsonTypes.LINE_STRING } }); if (coordinateCount === 3) { return; } } // render the Polygon return display(geojson); }; DrawPolygon.onTrash = function(state) { this.deleteFeature([state.polygon.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT); }; const DrawLineString = {}; DrawLineString.onSetup = function(opts) { opts = opts || {}; const featureId = opts.featureId; let line, currentVertexPosition; let direction = 'forward'; if (featureId) { line = this.getFeature(featureId); if (!line) { throw new Error('Could not find a feature with the provided featureId'); } let from = opts.from; if (from && from.type === 'Feature' && from.geometry && from.geometry.type === 'Point') { from = from.geometry; } if (from && from.type === 'Point' && from.coordinates && from.coordinates.length === 2) { from = from.coordinates; } if (!from || !Array.isArray(from)) { throw new Error('Please use the `from` property to indicate which point to continue the line from'); } const lastCoord = line.coordinates.length - 1; if (line.coordinates[lastCoord][0] === from[0] && line.coordinates[lastCoord][1] === from[1]) { currentVertexPosition = lastCoord + 1; // add one new coordinate to continue from line.addCoordinate(currentVertexPosition, ...line.coordinates[lastCoord]); } else if (line.coordinates[0][0] === from[0] && line.coordinates[0][1] === from[1]) { direction = 'backwards'; currentVertexPosition = 0; // add one new coordinate to continue from line.addCoordinate(currentVertexPosition, ...line.coordinates[0]); } else { throw new Error('`from` should match the point at either the start or the end of the provided LineString'); } } else { line = this.newFeature({ type: geojsonTypes.FEATURE, properties: {}, geometry: { type: geojsonTypes.LINE_STRING, coordinates: [] } }); currentVertexPosition = 0; this.addFeature(line); } this.clearSelectedFeatures(); doubleClickZoom.disable(this); this.updateUIClasses({ mouse: cursors.ADD }); this.activateUIButton(types.LINE); this.setActionableState({ trash: true }); return { line, currentVertexPosition, direction }; }; DrawLineString.clickAnywhere = function(state, e) { if (state.currentVertexPosition > 0 && isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition - 1]) || state.direction === 'backwards' && isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition + 1])) { return this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.line.id] }); } this.updateUIClasses({ mouse: cursors.ADD }); state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); if (state.direction === 'forward') { state.currentVertexPosition++; state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); } else { state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); } }; DrawLineString.clickOnVertex = function(state) { return this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.line.id] }); }; DrawLineString.onMouseMove = function(state, e) { state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); if (isVertex$1(e)) { this.updateUIClasses({ mouse: cursors.POINTER }); } }; DrawLineString.onTap = DrawLineString.onClick = function(state, e) { if (isVertex$1(e)) return this.clickOnVertex(state, e); this.clickAnywhere(state, e); }; DrawLineString.onKeyUp = function(state, e) { if (isEnterKey(e)) { this.changeMode(modes$1.SIMPLE_SELECT, { featureIds: [state.line.id] }); } else if (isEscapeKey(e)) { this.deleteFeature([state.line.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT); } }; DrawLineString.onStop = function(state) { doubleClickZoom.enable(this); this.activateUIButton(); // check to see if we've deleted this feature if (this.getFeature(state.line.id) === undefined) return; //remove last added coordinate state.line.removeCoordinate(`${state.currentVertexPosition}`); if (state.line.isValid()) { this.fire(events$1.CREATE, { features: [state.line.toGeoJSON()] }); } else { this.deleteFeature([state.line.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT, {}, { silent: true }); } }; DrawLineString.onTrash = function(state) { this.deleteFeature([state.line.id], { silent: true }); this.changeMode(modes$1.SIMPLE_SELECT); }; DrawLineString.toDisplayFeatures = function(state, geojson, display) { const isActiveLine = geojson.properties.id === state.line.id; geojson.properties.active = (isActiveLine) ? activeStates.ACTIVE : activeStates.INACTIVE; if (!isActiveLine) return display(geojson); // Only render the line if it has at least one real coordinate if (geojson.geometry.coordinates.length < 2) return; geojson.properties.meta = meta.FEATURE; display(createVertex( state.line.id, geojson.geometry.coordinates[state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1], `${state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1}`, false )); display(geojson); }; var modes = { simple_select: SimpleSelect, direct_select: DirectSelect, draw_point: DrawPoint, draw_polygon: DrawPolygon, draw_line_string: DrawLineString, }; const defaultOptions = { defaultMode: modes$1.SIMPLE_SELECT, keybindings: true, touchEnabled: true, clickBuffer: 2, touchBuffer: 25, boxSelect: true, displayControlsDefault: true, styles, modes, controls: {}, userProperties: false, suppressAPIEvents: true }; const showControls = { point: true, line_string: true, polygon: true, trash: true, combine_features: true, uncombine_features: true }; const hideControls = { point: false, line_string: false, polygon: false, trash: false, combine_features: false, uncombine_features: false }; function addSources(styles, sourceBucket) { return styles.map((style) => { if (style.source) return style; return Object.assign({}, style, { id: `${style.id}.${sourceBucket}`, source: (sourceBucket === 'hot') ? sources.HOT : sources.COLD }); }); } function setupOptions(options = {}) { let withDefaults = Object.assign({}, options); if (!options.controls) { withDefaults.controls = {}; } if (options.displayControlsDefault === false) { withDefaults.controls = Object.assign({}, hideControls, options.controls); } else { withDefaults.controls = Object.assign({}, showControls, options.controls); } withDefaults = Object.assign({}, defaultOptions, withDefaults); // Layers with a shared source should be adjacent for performance reasons withDefaults.styles = addSources(withDefaults.styles, 'cold').concat(addSources(withDefaults.styles, 'hot')); return withDefaults; } var fastDeepEqual; var hasRequiredFastDeepEqual; function requireFastDeepEqual () { if (hasRequiredFastDeepEqual) return fastDeepEqual; hasRequiredFastDeepEqual = 1; // do not edit .js files directly - edit src/index.jst fastDeepEqual = function equal(a, b) { if (a === b) return true; if (a && b && typeof a == 'object' && typeof b == 'object') { if (a.constructor !== b.constructor) return false; var length, i, keys; if (Array.isArray(a)) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false; return true; } if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); keys = Object.keys(a); length = keys.length; if (length !== Object.keys(b).length) return false; for (i = length; i-- !== 0;) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; for (i = length; i-- !== 0;) { var key = keys[i]; if (!equal(a[key], b[key])) return false; } return true; } // true if both NaN, false otherwise return a!==a && b!==b; }; return fastDeepEqual; } var fastDeepEqualExports = requireFastDeepEqual(); var isEqual = /*@__PURE__*/getDefaultExportFromCjs(fastDeepEqualExports); var geojsonNormalize; var hasRequiredGeojsonNormalize; function requireGeojsonNormalize () { if (hasRequiredGeojsonNormalize) return geojsonNormalize; hasRequiredGeojsonNormalize = 1; geojsonNormalize = normalize; var types = { Point: 'geometry', MultiPoint: 'geometry', LineString: 'geometry', MultiLineString: 'geometry', Polygon: 'geometry', MultiPolygon: 'geometry', GeometryCollection: 'geometry', Feature: 'feature', FeatureCollection: 'featurecollection' }; /** * Normalize a GeoJSON feature into a FeatureCollection. * * @param {object} gj geojson data * @returns {object} normalized geojson data */ function normalize(gj) { if (!gj || !gj.type) return null; var type = types[gj.type]; if (!type) return null; if (type === 'geometry') { return { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: gj }] }; } else if (type === 'feature') { return { type: 'FeatureCollection', features: [gj] }; } else if (type === 'featurecollection') { return gj; } } return geojsonNormalize; } var geojsonNormalizeExports = requireGeojsonNormalize(); var normalize = /*@__PURE__*/getDefaultExportFromCjs(geojsonNormalizeExports); function stringSetsAreEqual(a, b) { if (a.length !== b.length) return false; return JSON.stringify(a.map(id => id).sort()) === JSON.stringify(b.map(id => id).sort()); } const featureTypes = { Polygon, LineString, Point: Point$1, MultiPolygon: MultiFeature, MultiLineString: MultiFeature, MultiPoint: MultiFeature }; function setupAPI(ctx, api) { api.modes = modes$1; // API doesn't emit events by default const silent = ctx.options.suppressAPIEvents !== undefined ? !!ctx.options.suppressAPIEvents : true; api.getFeatureIdsAt = function(point) { const features = featuresAt.click({ point }, null, ctx); return features.map(feature => feature.properties.id); }; api.getSelectedIds = function() { return ctx.store.getSelectedIds(); }; api.getSelected = function() { return { type: geojsonTypes.FEATURE_COLLECTION, features: ctx.store.getSelectedIds().map(id => ctx.store.get(id)).map(feature => feature.toGeoJSON()) }; }; api.getSelectedPoints = function() { return { type: geojsonTypes.FEATURE_COLLECTION, features: ctx.store.getSelectedCoordinates().map(coordinate => ({ type: geojsonTypes.FEATURE, properties: {}, geometry: { type: geojsonTypes.POINT, coordinates: coordinate.coordinates } })) }; }; api.set = function(featureCollection) { if (featureCollection.type === undefined || featureCollection.type !== geojsonTypes.FEATURE_COLLECTION || !Array.isArray(featureCollection.features)) { throw new Error('Invalid FeatureCollection'); } const renderBatch = ctx.store.createRenderBatch(); let toDelete = ctx.store.getAllIds().slice(); const newIds = api.add(featureCollection); const newIdsLookup = new StringSet(newIds); toDelete = toDelete.filter(id => !newIdsLookup.has(id)); if (toDelete.length) { api.delete(toDelete); } renderBatch(); return newIds; }; api.add = function(geojson) { const featureCollection = JSON.parse(JSON.stringify(normalize(geojson))); const ids = featureCollection.features.map((feature) => { feature.id = feature.id || generateID(); if (feature.geometry === null) { throw new Error('Invalid geometry: null'); } if (ctx.store.get(feature.id) === undefined || ctx.store.get(feature.id).type !== feature.geometry.type) { // If the feature has not yet been created ... const Model = featureTypes[feature.geometry.type]; if (Model === undefined) { throw new Error(`Invalid geometry type: ${feature.geometry.type}.`); } const internalFeature = new Model(ctx, feature); ctx.store.add(internalFeature, { silent }); } else { // If a feature of that id has already been created, and we are swapping it out ... const internalFeature = ctx.store.get(feature.id); const originalProperties = internalFeature.properties; internalFeature.properties = feature.properties; if (!isEqual(originalProperties, feature.properties)) { ctx.store.featureChanged(internalFeature.id, { silent }); } if (!isEqual(internalFeature.getCoordinates(), feature.geometry.coordinates)) { internalFeature.incomingCoords(feature.geometry.coordinates); } } return feature.id; }); ctx.store.render(); return ids; }; api.get = function(id) { const feature = ctx.store.get(id); if (feature) { return feature.toGeoJSON(); } }; api.getAll = function() { return { type: geojsonTypes.FEATURE_COLLECTION, features: ctx.store.getAll().map(feature => feature.toGeoJSON()) }; }; api.delete = function(featureIds) { ctx.store.delete(featureIds, { silent }); // If we were in direct select mode and our selected feature no longer exists // (because it was deleted), we need to get out of that mode. if (api.getMode() === modes$1.DIRECT_SELECT && !ctx.store.getSelectedIds().length) { ctx.events.changeMode(modes$1.SIMPLE_SELECT, undefined, { silent }); } else { ctx.store.render(); } return api; }; api.deleteAll = function() { ctx.store.delete(ctx.store.getAllIds(), { silent }); // If we were in direct select mode, now our selected feature no longer exists, // so escape that mode. if (api.getMode() === modes$1.DIRECT_SELECT) { ctx.events.changeMode(modes$1.SIMPLE_SELECT, undefined, { silent }); } else { ctx.store.render(); } return api; }; api.changeMode = function(mode, modeOptions = {}) { // Avoid changing modes just to re-select what's already selected if (mode === modes$1.SIMPLE_SELECT && api.getMode() === modes$1.SIMPLE_SELECT) { if (stringSetsAreEqual((modeOptions.featureIds || []), ctx.store.getSelectedIds())) return api; // And if we are changing the selection within simple_select mode, just change the selection, // instead of stopping and re-starting the mode ctx.store.setSelected(modeOptions.featureIds, { silent }); ctx.store.render(); return api; } if (mode === modes$1.DIRECT_SELECT && api.getMode() === modes$1.DIRECT_SELECT && modeOptions.featureId === ctx.store.getSelectedIds()[0]) { return api; } ctx.events.changeMode(mode, modeOptions, { silent }); return api; }; api.getMode = function() { return ctx.events.getMode(); }; api.trash = function() { ctx.events.trash({ silent }); return api; }; api.combineFeatures = function() { ctx.events.combineFeatures({ silent }); return api; }; api.uncombineFeatures = function() { ctx.events.uncombineFeatures({ silent }); return api; }; api.setFeatureProperty = function(featureId, property, value) { ctx.store.setFeatureProperty(featureId, property, value, { silent }); return api; }; return api; } var lib = /*#__PURE__*/Object.freeze({ __proto__: null, CommonSelectors: common_selectors, ModeHandler: ModeHandler, StringSet: StringSet, constrainFeatureMovement: constrainFeatureMovement, createMidPoint: createMidpoint, createSupplementaryPoints: createSupplementaryPoints, createVertex: createVertex, doubleClickZoom: doubleClickZoom, euclideanDistance: euclideanDistance, featuresAt: featuresAt, getFeatureAtAndSetCursors: getFeatureAtAndSetCursors, isClick: isClick, isEventAtCoordinates: isEventAtCoordinates, isTap: isTap, mapEventToBoundingBox: mapEventToBoundingBox, moveFeatures: moveFeatures, sortFeatures: sortFeatures, stringSetsAreEqual: stringSetsAreEqual, theme: styles, toDenseArray: toDenseArray }); const setupDraw = function(options, api) { options = setupOptions(options); const ctx = { options }; api = setupAPI(ctx, api); ctx.api = api; const setup = runSetup(ctx); api.onAdd = setup.onAdd; api.onRemove = setup.onRemove; api.types = types; api.options = options; return api; }; function MapboxDraw(options) { setupDraw(options, this); } MapboxDraw.modes = modes; MapboxDraw.constants = Constants; MapboxDraw.lib = lib; return MapboxDraw; })); //# sourceMappingURL=mapbox-gl-draw-unminified.js.map