@mapbox/mapbox-gl-draw
Version:
A drawing component for Mapbox GL JS
201 lines (168 loc) • 6.49 kB
JavaScript
import isEqual from 'fast-deep-equal';
import normalize from '@mapbox/geojson-normalize';
import {generateID} from './lib/id.js';
import featuresAt from './lib/features_at.js';
import stringSetsAreEqual from './lib/string_sets_are_equal.js';
import * as Constants from './constants.js';
import StringSet from './lib/string_set.js';
import Polygon from './feature_types/polygon.js';
import LineString from './feature_types/line_string.js';
import Point from './feature_types/point.js';
import MultiFeature from './feature_types/multi_feature.js';
const featureTypes = {
Polygon,
LineString,
Point,
MultiPolygon: MultiFeature,
MultiLineString: MultiFeature,
MultiPoint: MultiFeature
};
export default function(ctx, api) {
api.modes = Constants.modes;
// 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: Constants.geojsonTypes.FEATURE_COLLECTION,
features: ctx.store.getSelectedIds().map(id => ctx.store.get(id)).map(feature => feature.toGeoJSON())
};
};
api.getSelectedPoints = function() {
return {
type: Constants.geojsonTypes.FEATURE_COLLECTION,
features: ctx.store.getSelectedCoordinates().map(coordinate => ({
type: Constants.geojsonTypes.FEATURE,
properties: {},
geometry: {
type: Constants.geojsonTypes.POINT,
coordinates: coordinate.coordinates
}
}))
};
};
api.set = function(featureCollection) {
if (featureCollection.type === undefined || featureCollection.type !== Constants.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: Constants.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() === Constants.modes.DIRECT_SELECT && !ctx.store.getSelectedIds().length) {
ctx.events.changeMode(Constants.modes.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() === Constants.modes.DIRECT_SELECT) {
ctx.events.changeMode(Constants.modes.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 === Constants.modes.SIMPLE_SELECT && api.getMode() === Constants.modes.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 === Constants.modes.DIRECT_SELECT && api.getMode() === Constants.modes.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;
}