@mapbox/mapbox-gl-draw
Version:
A drawing component for Mapbox GL JS
406 lines (362 loc) • 11.7 kB
JavaScript
import toDenseArray from './lib/to_dense_array.js';
import StringSet from './lib/string_set.js';
import render from './render.js';
import * as Constants from './constants.js';
export default 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(Constants.events.SELECTION_CHANGE, {
features: this.getSelected().map(feature => feature.toGeoJSON()),
points: this.getSelectedCoordinates().map(coordinate => ({
type: Constants.geojsonTypes.FEATURE,
properties: {},
geometry: {
type: Constants.geojsonTypes.POINT,
coordinates: coordinate.coordinates
}
}))
});
this._emitSelectionChange = false;
}
// Fire render event
this.ctx.events.fire(Constants.events.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(Constants.events.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(Constants.events.UPDATE, {
action: options.action ? options.action : Constants.updateActions.CHANGE_COORDINATES,
features: [this.get(featureId).toGeoJSON()]
});
}
return this;
};
/**
* Gets the ids of all features currently in changed state.
* @return {Store} this
*/
Store.prototype.getChangedIds = function() {
return this._changedFeatureIds.values();
};
/**
* Sets all features to unchanged state.
* @return {Store} this
*/
Store.prototype.clearChangedIds = function() {
this._changedFeatureIds.clear();
return this;
};
/**
* Gets the ids of all features in the store.
* @return {Store} this
*/
Store.prototype.getAllIds = function() {
return this._featureIds.values();
};
/**
* Adds a feature to the store.
* @param {Object} feature
* @param {Object} [options]
* @param {Object} [options.silent] - If `true`, this invocation will not fire an event.
*
* @return {Store} this
*/
Store.prototype.add = function(feature, options = {}) {
this._features[feature.id] = feature;
this._featureIds.add(feature.id);
this.featureCreated(feature.id, {silent: options.silent});
return this;
};
/**
* Deletes a feature or array of features from the store.
* Cleans up after the deletion by deselecting the features.
* If changes were made, sets the state to the dirty
* and fires an event.
* @param {string | Array<string>} featureIds
* @param {Object} [options]
* @param {Object} [options.silent] - If `true`, this invocation will not fire an event.
* @return {Store} this
*/
Store.prototype.delete = function(featureIds, options = {}) {
const deletedFeaturesToEmit = [];
toDenseArray(featureIds).forEach((id) => {
if (!this._featureIds.has(id)) return;
this._featureIds.delete(id);
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(Constants.events.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<Object>}
*/
Store.prototype.getAll = function() {
return Object.keys(this._features).map(id => this._features[id]);
};
/**
* Adds features to the current selection.
* @param {string | Array<string>} featureIds
* @param {Object} [options]
* @param {Object} [options.silent] - If `true`, this invocation will not fire an event.
* @return {Store} this
*/
Store.prototype.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<string>} 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<string> | 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<Array<string>>} 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<string>} Selected feature ids.
*/
Store.prototype.getSelectedIds = function() {
return this._selectedFeatureIds.values();
};
/**
* Returns features in the current selection.
* @return {Array<Object>} Selected features.
*/
Store.prototype.getSelected = function() {
return this.getSelectedIds().map(id => this.get(id));
};
/**
* Returns selected coordinates in the currently selected feature.
* @return {Array<Object>} 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: Constants.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() {
Constants.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;
}
};