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
4 changes: 3 additions & 1 deletion src/renderer/components/Toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export const Toolbar = () => {
commandRegistry.command('MEASURE_DISTANCE'),
commandRegistry.command('MEASURE_AREA'),
commandRegistry.command('MEASURE_CIRCLE'),
commandRegistry.command('ELEVATION_PROFILE')
commandRegistry.command('ELEVATION_PROFILE'),
commandRegistry.command('LINE_OF_SIGHT'),
commandRegistry.command('AREA_OF_SIGHT')
]

const replicationCommands = [
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/map/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import registerGraticules from './graticules'
import measure from '../../ol/interaction/measure'
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 print from '../print'
import './Map.css'
import './ScaleLine.css'
Expand Down Expand Up @@ -83,6 +85,8 @@ export const Map = () => {
measure({ services, map })
shapeInteraction({ services, map })
elevationProfile({ services, map })
lineOfSight({ services, map })
areaOfSight({ services, map })

// Expose a function to query the current map resolution.
services.getMapResolution = () => map.getView().getResolution()
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/map/eventHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ const mapHandlers = (services, map) => {
map.once('rendercomplete', ({ target }) => sendPreview(services, target))
map.on('pointermove', throttle(75, event => osdDriver.pointermove(event)))

// Deselect everything except features and markers.
// Deselect everything except features, markers and analysis results.
map.on('click', () => {
const exclude = [ID.isFeatureId, ID.isMarkerId, ID.isMeasureId]
const exclude = [ID.isFeatureId, ID.isMarkerId, ID.isMeasureId, ID.isLosId, ID.isAosId]
const deselect = selection.selected(x => !exclude.some(p => p(x)))
if (deselect.length) selection.deselect(deselect)
})
Expand Down
51 changes: 51 additions & 0 deletions src/renderer/components/properties/AreaOfSightProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-disable react/prop-types */
import React from 'react'
import textProperty from './textProperty'
import GridCols2 from './GridCols2'
import ColSpan2 from './ColSpan2'
import { DEFAULT_RADIUS_M, MAX_RADIUS_M } from '../../ol/interaction/area-of-sight'

const formatNumber = value => (typeof value === 'number' ? String(value) : '')

const setProperty = (key, valid) => value => feature => {
const num = parseFloat(value)
if (!valid(num)) return feature
return { ...feature, properties: { ...feature.properties, [key]: num } }
}

const Radius = textProperty({
label: `Radius [m] (max ${MAX_RADIUS_M})`,
get: feature => formatNumber(feature.properties?.radius ?? DEFAULT_RADIUS_M),
set: value => feature => {
const num = parseFloat(value)
if (!Number.isFinite(num) || num < 100) return feature
return {
...feature,
properties: { ...feature.properties, radius: Math.min(num, MAX_RADIUS_M) }
}
}
})

const ObserverHeight = textProperty({
label: 'Observer height [m]',
get: feature => formatNumber(feature.properties?.observerHeight),
set: setProperty('observerHeight', num => Number.isFinite(num) && num >= 0)
})

const TargetHeight = textProperty({
label: 'Target height [m]',
get: feature => formatNumber(feature.properties?.targetHeight),
set: setProperty('targetHeight', num => Number.isFinite(num) && num >= 0)
})

const AreaOfSightProperties = (props) => (
<GridCols2>
<ObserverHeight {...props} />
<TargetHeight {...props} />
<ColSpan2>
<Radius {...props} />
</ColSpan2>
</GridCols2>
)

export default AreaOfSightProperties
58 changes: 58 additions & 0 deletions src/renderer/components/properties/LineOfSightProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* eslint-disable react/prop-types */
import React from 'react'
import { getLength } from 'ol/sphere'
import LineString from 'ol/geom/LineString'
import textProperty from './textProperty'
import GridCols2 from './GridCols2'
import ColSpan2 from './ColSpan2'

const formatHeight = h => (typeof h === 'number' ? h.toFixed(2) : '')

const setHeight = key => value => feature => {
const num = parseFloat(value)
if (!Number.isFinite(num) || num < 0) return feature
return { ...feature, properties: { ...feature.properties, [key]: num } }
}

const ObserverHeight = textProperty({
label: 'Observer height [m]',
get: feature => formatHeight(feature.properties?.observerHeight),
set: setHeight('observerHeight')
})

const TargetHeight = textProperty({
label: 'Target height [m]',
get: feature => formatHeight(feature.properties?.targetHeight),
set: setHeight('targetHeight')
})

const distanceKm = (doc) => {
const coordinates = doc?.geometry?.type === 'LineString' && doc.geometry.coordinates
if (!coordinates || coordinates.length < 2) return null
return getLength(new LineString(coordinates)) / 1000
}

const LineOfSightProperties = (props) => {
const docs = Object.values(props.features)
const single = docs.length === 1
const km = single ? distanceKm(docs[0]) : null

return (
<GridCols2>
<ObserverHeight {...props} />
<TargetHeight {...props} />
{single && km !== null && (
<ColSpan2>
<div className='form-textfield'>
<span className='form-textfield__label'>Distance</span>
<span className='form-textfield__input' style={{ paddingTop: 8 }}>
{km.toFixed(2)} km
</span>
</div>
</ColSpan2>
)}
</GridCols2>
)
}

export default LineOfSightProperties
6 changes: 5 additions & 1 deletion src/renderer/components/properties/Properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import SKKMUnitProperties from './SKKMUnitProperties'
import SKKMCommandProperties from './SKKMCommandProperties'
import ShapeProperties from './ShapeProperties'
import TextShapeProperties from './TextShapeProperties'
import LineOfSightProperties from './LineOfSightProperties'
import AreaOfSightProperties from './AreaOfSightProperties'
import './Properties.css'

const propertiesPanels = {
Expand All @@ -38,7 +40,9 @@ const propertiesPanels = {
'sse-service': props => <SSEServiceProperties {...props}/>,
'feature:SKKM/K': props => <SKKMStandardProperties {...props}/>,
'feature:SKKM/KU': props => <SKKMUnitProperties {...props}/>,
'feature:SKKM/KC': props => <SKKMCommandProperties {...props}/>
'feature:SKKM/KC': props => <SKKMCommandProperties {...props}/>,
los: props => <LineOfSightProperties {...props}/>,
aos: props => <AreaOfSightProperties {...props}/>
}

const singletons = ['tile-service', 'tile-layers', 'sse-service']
Expand Down
9 changes: 6 additions & 3 deletions src/renderer/components/sidebar/ScopeSwitcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SCOPES = {
[`@${ID.PLACE}`]: 'mdiSearchWeb',
[`@${ID.TILE_SERVICE}`]: 'mdiEarth',
[`@${ID.SSE_SERVICE}`]: 'mdiAccessPointNetwork',
[`@${ID.MEASURE}`]: 'mdiAndroidStudio',
[`@${ID.MEASURE} @${ID.LOS} @${ID.AOS}`]: 'mdiAndroidStudio',
[`@${ID.INVITED}`]: 'mdiCloudPlusOutline'
}

Expand All @@ -36,7 +36,7 @@ const TOOLTIPS = {
[`@${ID.PLACE}`]: 'Search for addresses based on OSM (online only)',
[`@${ID.TILE_SERVICE}`]: 'Manage existing tile services for maps',
[`@${ID.SSE_SERVICE}`]: 'Manage live data sources',
[`@${ID.MEASURE}`]: 'Manage existing measurements',
[`@${ID.MEASURE} @${ID.LOS} @${ID.AOS}`]: 'Manage existing measurements and sight analyses',
[`@${ID.INVITED}`]: 'Show invitations and join shared layers'
}

Expand All @@ -46,9 +46,12 @@ const TOOLTIPS = {
const ScopeSwitch = props => {
const [search, setSearch] = useMemento('ui.sidebar.search', defaultSearch)

// A switch may cover multiple scope tokens (e.g. '@measure @los @aos');
// it is active when all of its tokens are part of the current search.
const activeTokens = search.history[0].scope.split(' ')
const enabled = search.history.length > 1
? false
: search.history[0].scope.split(' ').includes(props.scope)
: props.scope.split(' ').every(token => activeTokens.includes(token))

const className = props.name
? 'a74a-named'
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const DEFAULT = 'default'
export const TAGS = 'tags'
export const STICKY = 'sticky'
export const MEASURE = 'measure'
export const LOS = 'los'
export const AOS = 'aos'
export const SHARED = 'shared'
export const INVITED = 'invited'

Expand All @@ -45,6 +47,8 @@ export const TILE_PRESET_SCOPE = TILE_PRESET + COLON
export const TILE_LAYER_SCOPE = TILE_LAYER + COLON
export const SSE_SERVICE_SCOPE = SSE_SERVICE + COLON
export const MEASURE_SCOPE = MEASURE + COLON
export const LOS_SCOPE = LOS + COLON
export const AOS_SCOPE = AOS + COLON

export const LINK_PREFIX = 'link' + PLUS
export const STYLE_PREFIX = 'style' + PLUS
Expand Down Expand Up @@ -106,6 +110,8 @@ export const isHiddenId = isId(HIDDEN_PREFIX)
export const isDefaultId = isId(DEFAULT_PREFIX)
export const isTagsId = isId(TAGS_PREFIX)
export const isMeasureId = isId(MEASURE_SCOPE)
export const isLosId = isId(LOS_SCOPE)
export const isAosId = isId(AOS_SCOPE)
export const isSharedLayerId = isId(sharedId(LAYER_SCOPE))
export const isInvitedId = isId(INVITED)
export const isRoleId = isId(ROLE_PREFIX)
Expand Down Expand Up @@ -173,6 +179,8 @@ export const tileLayerId = (tileServiceId, layerId) =>
export const markerId = () => makeId(MARKER, uuid())
export const bookmarkId = () => makeId(BOOKMARK, uuid())
export const measureId = () => makeId(MEASURE, uuid())
export const losId = () => makeId(LOS, uuid())
export const aosId = () => makeId(AOS, uuid())
export const linkId = id => LINK + PLUS + id + SLASH + uuid()
export const invitationId = () => makeId(INVITED, uuid())

Expand Down
4 changes: 4 additions & 0 deletions src/renderer/model/CommandRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import creationCommand from './commands/CreationCommands'
import measureCommands from './commands/MeasureCommands'
import shapeCommands from './commands/ShapeCommands'
import elevationProfileCommands from './commands/ElevationProfileCommands'
import lineOfSightCommands from './commands/LineOfSightCommands'
import areaOfSightCommands from './commands/AreaOfSightCommands'
import printCommands from './commands/PrintCommands'
import replicationCommands from './commands/ReplicationCommands'

Expand All @@ -23,6 +25,8 @@ export function CommandRegistry (services) {
Object.assign(this, measureCommands(services))
Object.assign(this, shapeCommands(services))
Object.assign(this, elevationProfileCommands(services))
Object.assign(this, lineOfSightCommands(services))
Object.assign(this, areaOfSightCommands(services))
Object.assign(this, printCommands(services))
Object.assign(this, replicationCommands(services))

Expand Down
Loading
Loading