@polar/plugin-draw
Version:
Draw plugin for POLAR that adds draw interactions to the map, allowing users to place various shapes and texts.
188 lines (180 loc) • 5.48 kB
text/typescript
import { type DrawStyle, type MeasureMode } from '@polar/lib-custom-types'
import centerOfMass from '@turf/center-of-mass'
import { type Color } from 'ol/color'
import { type ColorLike } from 'ol/colorlike'
import { Feature } from 'ol'
import { LineString, Point, Polygon } from 'ol/geom'
import { type Projection } from 'ol/proj'
import { getArea, getLength } from 'ol/sphere'
import { Circle as CircleStyle, Fill, Stroke } from 'ol/style'
import Style, { type Options, StyleFunction } from 'ol/style/Style'
import Text, { type Options as TextOptions } from 'ol/style/Text'
const roundMeasurement = (measurement: number, divisor: number) =>
Math.round((measurement * 100) / divisor + Number.EPSILON) / 100
function calculatePartialDistances(
styles: Style[],
styleOptions: Options,
textOptions: TextOptions,
feature: Feature,
unit: 'm' | 'km',
projection: Projection
) {
const geometry = feature.getGeometry() as LineString | Polygon
const coordinates =
geometry instanceof Polygon
? geometry.getCoordinates()[0]
: geometry.getCoordinates()
for (let i = 1; i < coordinates.length; i++) {
const lineString = new LineString([coordinates[i - 1], coordinates[i]])
const lengthInMetres = getLength(lineString, {
projection,
})
const length = roundMeasurement(lengthInMetres, unit === 'km' ? 1000 : 1)
const text = `${length} ${unit}`
feature.set(`length-${i}`, roundMeasurement(lengthInMetres, 1))
const style = new Style({
...styleOptions,
text: new Text({
...textOptions,
text,
}),
})
style.setGeometry(lineString)
styles.push(style)
}
// This only happens once the drawing has been finished
if (
Object.keys(feature.getProperties()).filter((key) =>
key.startsWith('length-')
).length === coordinates.length
) {
feature.unset(`length-${coordinates.length}`)
}
return styles
}
function getAreaUnitAndDivisor(measureMode: Exclude<MeasureMode, 'none'>) {
let areaUnit = ''
let divisor: number
if (measureMode === 'metres') {
areaUnit = 'm²'
divisor = 1
} else if (measureMode === 'kilometres') {
areaUnit = 'km²'
divisor = 1000000
} else {
areaUnit = 'ha'
divisor = 10000
}
return { areaUnit, divisor }
}
const measureStyle: (
styleOptions: Options,
measureMode: Exclude<MeasureMode, 'none'>,
projection: Projection,
measureStyleOptions?: TextOptions
) => StyleFunction =
(styleOptions, measureMode, projection, measureStyleOptions) => (feature) => {
const geometry = feature.getGeometry()
if (geometry instanceof Polygon || geometry instanceof LineString) {
const styles = [new Style(styleOptions)]
const textOptions: TextOptions = {
font: '16px sans-serif',
placement: 'line',
fill: new Fill({ color: 'black' }),
stroke: new Stroke({ color: 'black' }),
offsetY: -5,
...measureStyleOptions,
}
if (geometry instanceof Polygon) {
const { areaUnit, divisor } = getAreaUnitAndDivisor(measureMode)
const areaInMetres = getArea(geometry, { projection })
const area = roundMeasurement(areaInMetres, divisor)
const text = `${area} ${areaUnit}`
const style = new Style({
text: new Text({
...textOptions,
placement: 'point',
text,
}),
})
// @ts-expect-error | Features in this StyleFunction are always of type Feature<Geometry>
feature.set('area', roundMeasurement(areaInMetres, 1))
style.setGeometry(
new Point(
centerOfMass({
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: geometry.getCoordinates(),
},
}).geometry.coordinates
)
)
styles.push(style)
}
return calculatePartialDistances(
styles,
styleOptions,
textOptions,
feature as Feature,
measureMode === 'metres' ? 'm' : 'km',
projection
)
}
return new Style(styleOptions)
}
export default function (
drawMode: string,
strokeColor: string,
measureMode: MeasureMode,
projection: Projection,
drawStyle?: DrawStyle
): Style | StyleFunction {
const defaultFillColor = 'rgba(255, 255, 255, 0.5)'
if (drawMode === 'Point') {
return createPointStyle(
strokeColor,
drawStyle?.circle?.fillColor
? drawStyle.circle.fillColor
: defaultFillColor,
drawStyle?.circle?.radius
)
}
const fillColor = drawStyle?.fill?.color
? drawStyle.fill.color
: defaultFillColor
const styleOptions: Options = {
image: new CircleStyle({
radius: 5,
fill: new Fill({
color: fillColor,
}),
stroke: new Stroke({ color: strokeColor }),
}),
stroke: new Stroke({
color: strokeColor,
width: drawStyle?.stroke?.width || 2,
}),
fill: new Fill({
color: fillColor,
}),
}
return measureMode === 'none'
? new Style(styleOptions)
: measureStyle(styleOptions, measureMode, projection, drawStyle?.measure)
}
function createPointStyle(
strokeColor: string,
fillColor: Color | ColorLike,
radius = 5
) {
return new Style({
image: new CircleStyle({
radius,
fill: new Fill({
color: fillColor,
}),
stroke: new Stroke({ color: strokeColor }),
}),
})
}