Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/renderer/components/Toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export const Toolbar = () => {
commandRegistry.command('MEASURE_CIRCLE'),
commandRegistry.command('ELEVATION_PROFILE'),
commandRegistry.command('LINE_OF_SIGHT'),
commandRegistry.command('AREA_OF_SIGHT')
commandRegistry.command('AREA_OF_SIGHT'),
commandRegistry.command('OBSERVER_SITING')
]

const replicationCommands = [
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/map/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import shapeInteraction from '../../ol/interaction/shape-interaction'
import elevationProfile from '../../ol/interaction/elevation-profile'
import lineOfSight from '../../ol/interaction/line-of-sight'
import areaOfSight from '../../ol/interaction/area-of-sight'
import observerSiting from '../../ol/interaction/observer-siting'
import print from '../print'
import './Map.css'
import './ScaleLine.css'
Expand Down Expand Up @@ -87,6 +88,7 @@ export const Map = () => {
elevationProfile({ services, map })
lineOfSight({ services, map })
areaOfSight({ services, map })
observerSiting({ services, map })

// Expose a function to query the current map resolution.
services.getMapResolution = () => map.getView().getResolution()
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/model/CommandRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import shapeCommands from './commands/ShapeCommands'
import elevationProfileCommands from './commands/ElevationProfileCommands'
import lineOfSightCommands from './commands/LineOfSightCommands'
import areaOfSightCommands from './commands/AreaOfSightCommands'
import observerSitingCommands from './commands/ObserverSitingCommands'
import printCommands from './commands/PrintCommands'
import replicationCommands from './commands/ReplicationCommands'

Expand All @@ -27,6 +28,7 @@ export function CommandRegistry (services) {
Object.assign(this, elevationProfileCommands(services))
Object.assign(this, lineOfSightCommands(services))
Object.assign(this, areaOfSightCommands(services))
Object.assign(this, observerSitingCommands(services))
Object.assign(this, printCommands(services))
Object.assign(this, replicationCommands(services))

Expand Down
50 changes: 50 additions & 0 deletions src/renderer/model/commands/ObserverSitingCommands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import EventEmitter from '../../../shared/emitter'
import * as ID from '../../ids'

const hasTerrainService = async (store) => {
const tuples = await store.tuples(ID.TILE_SERVICE_SCOPE)
return tuples.some(([, service]) =>
service?.capabilities?.contentType === 'terrain/mapbox-rgb' ||
service?.terrain?.length > 0
)
}

const ObserverSiting = function (services) {
this.emitter = services.emitter
this.store = services.store
this.label = 'Observer Siting'
this.path = 'mdiBinoculars'
this.isEnabled = false

hasTerrainService(this.store).then(available => {
this.isEnabled = available
this.emit('changed')
})

this.store.on('batch', ({ operations }) => {
const relevant = operations.some(({ key }) =>
ID.isTileServiceId(key) || ID.isTilePresetId(key)
)
if (!relevant) return
hasTerrainService(this.store).then(available => {
if (this.isEnabled !== available) {
this.isEnabled = available
this.emit('changed')
}
})
})
}

Object.assign(ObserverSiting.prototype, EventEmitter.prototype)

ObserverSiting.prototype.execute = function () {
this.emitter.emit('OBSERVER_SITING')
}

ObserverSiting.prototype.enabled = function () {
return this.isEnabled
}

export default services => ({
OBSERVER_SITING: new ObserverSiting(services)
})
35 changes: 35 additions & 0 deletions src/renderer/ol/interaction/observer-siting/geometry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Polygon from 'ol/geom/Polygon'
import GeometryType from '../GeometryType'

// A LineString counts as a closed ring when its endpoints are within
// this distance [projection units] — boundaries drawn around an area
// rarely snap exactly onto their starting point.
const CLOSE_TOLERANCE = 100

/**
* Interpret a feature geometry as an area for observer siting.
* Polygons are used as-is; a (nearly) closed LineString — e.g. a
* boundary drawn around an area — is converted to a polygon.
*
* @param {import('ol/geom/Geometry').default} geometry
* @returns {Polygon|null}
*/
export const polygonOf = geometry => {
if (!geometry) return null
const type = geometry.getType()

if (type === GeometryType.POLYGON) return geometry.clone()

if (type === GeometryType.LINE_STRING) {
const coords = geometry.getCoordinates()
if (coords.length < 4) return null
const [x1, y1] = coords[0]
const [x2, y2] = coords[coords.length - 1]
if (Math.hypot(x2 - x1, y2 - y1) > CLOSE_TOLERANCE) return null
const ring = [...coords]
if (x1 !== x2 || y1 !== y2) ring.push([x1, y1])
return new Polygon([ring])
}

return null
}
Loading
Loading