@polar/plugin-draw
Version:
Draw plugin for POLAR that adds draw interactions to the map, allowing users to place various shapes and texts.
228 lines (220 loc) • 8.19 kB
text/typescript
import {
generateSimpleGetters,
generateSimpleMutations,
} from '@repositoryname/vuex-generators'
import { Feature } from 'geojson'
import { PolarModule } from '@polar/lib-custom-types'
import noop from '@repositoryname/noop'
import { Circle, LineString, Point, Polygon } from 'ol/geom'
import { DrawGetters, DrawMutations, DrawState } from '../types'
import { makeActions } from './actions'
import { inactive } from './reviseFeatures/revisionStates'
const getInitialState = (): DrawState => ({
mode: 'none',
drawMode: 'Point',
textInput: '',
selectedSize: 0,
featureCollection: {
type: 'FeatureCollection',
features: [],
},
revisedFeatureCollection: {
type: 'FeatureCollection',
features: [],
},
featureCollectionRevisionState: inactive,
selectedFeature: 1,
selectedStrokeColor: '#000000',
measureMode: 'none',
})
export const makeStoreModule = () => {
// NOTE hack to keep complex objects out of vuex
let selectedFeature = null
const { actions, drawSource } = makeActions()
const storeModule: PolarModule<DrawState, DrawGetters> = {
namespaced: true,
state: getInitialState(),
actions,
getters: {
...generateSimpleGetters(getInitialState()),
drawSource() {
return drawSource
},
selectableDrawModes(_, { configuration }) {
/* eslint-disable @typescript-eslint/naming-convention */
// NOTE: Keys are directly used as technical keys for ol/interaction/Draw and are allowed to differ from the naming scheme.
const allSelectableDrawModes = {
Circle: 'plugins.draw.drawMode.circle',
LineString: 'plugins.draw.drawMode.lineString',
Point: 'plugins.draw.drawMode.point',
Polygon: 'plugins.draw.drawMode.polygon',
Text: 'plugins.draw.drawMode.text',
}
/* eslint-enable @typescript-eslint/naming-convention */
// TODO: Do something different than eslint ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { Text, ...defaultSelectableDrawModes } = allSelectableDrawModes
return configuration.selectableDrawModes
? configuration.selectableDrawModes.reduce(
(acc, curr) => ({ ...acc, [curr]: allSelectableDrawModes[curr] }),
{}
)
: defaultSelectableDrawModes
},
selectedFeature: (state) => {
noop(state.selectedFeature)
return selectedFeature
},
selectableModes(_, { configuration }) {
const includesWrite =
configuration.selectableDrawModes?.includes('Text')
const includesMeasure = configuration.measureOptions !== undefined
let drawLabel = 'draw'
if (includesWrite && includesMeasure) {
drawLabel = 'writeAndMeasure'
} else if (includesWrite) {
drawLabel = 'write'
} else if (includesMeasure) {
drawLabel = 'measure'
}
const modes = [
['none', 'plugins.draw.mode.none'],
['draw', `plugins.draw.mode.${drawLabel}`],
['edit', 'plugins.draw.mode.edit'],
['translate', 'plugins.draw.mode.translate'],
['duplicate', 'plugins.draw.mode.duplicate'],
['cut', 'plugins.draw.mode.cut'],
['merge', 'plugins.draw.mode.merge'],
['delete', 'plugins.draw.mode.delete'],
]
if (configuration.lassos) {
modes.splice(4, 0, ['lasso', 'plugins.draw.mode.lasso'])
}
return Object.fromEntries(modes)
},
activeLassoIds: (_, { configuration }, __, rootGetters) =>
(configuration.lassos || []).reduce(
(accumulator, { id, minZoom = true }) => {
const layerConfig = rootGetters.configuration.layers?.find(
(layer) => id === layer.id
)
if (
minZoom &&
layerConfig &&
typeof layerConfig.minZoom !== 'undefined' &&
rootGetters.zoomLevel < layerConfig.minZoom
) {
return accumulator
}
accumulator.push(id)
return accumulator
},
[] as string[]
),
toastAction: (_, { configuration }) => configuration.toastAction || '',
measureOptions: (_, { configuration }) =>
configuration.measureOptions || {},
selectableMeasureModes: (_, { drawMode, measureOptions }) =>
(drawMode === 'LineString'
? Object.entries(measureOptions).filter(
([option]) => option !== 'hectares'
)
: Object.entries(measureOptions)
)
.filter((option) => option[1] === true)
.reduce(
(acc, [option]) => ({
...acc,
[option]:
`plugins.draw.measureMode.${option}` +
(drawMode === 'Polygon' && option !== 'hectares' ? 'Area' : ''),
}),
{ none: 'plugins.draw.measureMode.none' }
),
showMeasureOptions: ({ drawMode, mode }, { measureOptions }) =>
measureOptions &&
Object.values(measureOptions).some((option) => option === true) &&
mode === 'draw' &&
['LineString', 'Polygon'].includes(drawMode),
showTextInput({ drawMode, mode }, { selectedFeature }) {
return (
(drawMode === 'Text' && mode === 'draw') ||
(mode === 'edit' &&
selectedFeature &&
typeof selectedFeature.get('text') === 'string')
)
},
showDrawOptions(
{ mode },
{ configuration, showTextInput, selectedFeature }
) {
return (
configuration.enableOptions &&
!showTextInput &&
(mode === 'draw' || (mode === 'edit' && selectedFeature))
)
},
configuration(_, __, ___, rootGetters) {
return rootGetters.configuration.draw || {}
},
fontSizes(_, { configuration }) {
const { textStyle } = configuration
if (textStyle?.font) {
if (typeof textStyle.font === 'string') {
return []
}
return textStyle.font.size ? textStyle.font.size.slice(0, 5) : [10]
}
return []
},
showSizeSlider(_, { fontSizes, showTextInput }) {
return showTextInput && fontSizes.length > 1
},
textSize(_, { fontSizes, selectedSize }) {
return fontSizes[selectedSize]
},
},
mutations: {
...generateSimpleMutations(getInitialState()),
updateFeatures(state) {
const features = drawSource.getFeatures().map((feature) => {
const geometry = feature.getGeometry() as
| Circle
| LineString
| Point
| Polygon
const type = geometry.getType()
const isCircle = type === 'Circle'
const jsonFeature: Feature = {
type: 'Feature',
properties: Object.fromEntries(
Object.entries(feature.getProperties()).filter(
([property]) => property !== 'geometry'
)
),
geometry: {
// @ts-expect-error | A LinearRing can currently not be drawn
type: isCircle ? 'Point' : type,
// @ts-expect-error | The coordinates are in the correct format
coordinates: isCircle
? (geometry as Circle).getCenter()
: geometry.getCoordinates(),
},
}
// NOTE: If one is checking if properties exists (which it clearly does), TS complains
// "TS2531: Object is possibly 'null'.". This is due to the structure of the type GeoJsonProperties.
if (isCircle && jsonFeature.properties) {
jsonFeature.properties.radius = (geometry as Circle).getRadius()
}
return jsonFeature
})
state.featureCollection = { ...state.featureCollection, features }
},
setSelectedFeature: (state, payload) => {
selectedFeature = payload
state.selectedFeature = state.selectedFeature + 1
},
} as DrawMutations,
}
return storeModule
}