Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
b306ae4
First draft of property tree topic
ylvaselling Jan 16, 2026
00c8ce7
Make the new property tree kind of work
ylvaselling Jan 19, 2026
340d6cb
Fix the updating of properties
ylvaselling Jan 19, 2026
12a3494
Remove previous redux for properties & property owners, and switch to…
ylvaselling Jan 21, 2026
e2b330f
Fix some bugs
ylvaselling Jan 21, 2026
a4af65a
A fancy progress icon
ylvaselling Jan 21, 2026
3e507c9
Some polishing of the checkbox
ylvaselling Jan 22, 2026
3f99532
Throttle time updates
ylvaselling Jan 29, 2026
9cd5f99
First draft of more efficient property setting; minimize dispatches
ylvaselling Jan 29, 2026
4d1f18d
Remove duplicate comment
ylvaselling Jan 29, 2026
f0412d6
Make the middleware get the root explicitly
ylvaselling Jan 29, 2026
d827caf
Throttle camera updates
ylvaselling Jan 29, 2026
cf60a1a
Remove unneccessary subscriptions
ylvaselling Jan 29, 2026
8be7b4a
Rename propertyTreeTest to propertyTree and add remove uri action
ylvaselling Jan 29, 2026
d0a9910
Split the usePropertyOwnerVisibility into two hooks and add one for s…
ylvaselling Jan 30, 2026
d57c1cc
Fix progress bar icon
ylvaselling Jan 30, 2026
a2739ee
Add comments on entity adapters
ylvaselling Jan 30, 2026
7bab1cb
Fix the glitch of the icon
ylvaselling Jan 30, 2026
e562cd8
Rename propertyActive to propertyVisible
ylvaselling Jan 30, 2026
e4b30f9
Use redux visibility instead of properties to calculate visibility
ylvaselling Jan 30, 2026
5ca4d9c
Some cleanup
ylvaselling Jan 30, 2026
75397ba
Update from useProperty to usePropertyValue where possible, as we don…
ylvaselling Jan 30, 2026
f8296d9
Move helper functions from selector file to helpers
ylvaselling Jan 30, 2026
30d5851
Fix some small bugs
ylvaselling Jan 30, 2026
74a9b76
Batch time updates
ylvaselling Feb 24, 2026
2d1d154
Address PR comments from copilot
ylvaselling Feb 24, 2026
82aa4f2
Fix issues with opening a layer
ylvaselling Feb 25, 2026
10ee070
Address PR comment
ylvaselling Feb 25, 2026
9bcf088
Remove suspense as it is not helping
ylvaselling Feb 25, 2026
7b08ada
Replace selectors with usePropertyOwner hooks where possible
ylvaselling Feb 25, 2026
5410134
PR comments and lint fix
ylvaselling Feb 25, 2026
782f11b
Address PR comments
ylvaselling Feb 25, 2026
5f72eb1
Remove refreshing of group as a dispatch listener
ylvaselling Feb 25, 2026
6ad812c
Add visibility for new property owner sgns
ylvaselling Feb 25, 2026
5d655f9
Add explaining comment
ylvaselling Feb 25, 2026
c38633d
Fix issue with updating fading correctly
ylvaselling Feb 25, 2026
fff3435
Set visibility when adding new property owners
ylvaselling Feb 25, 2026
da2678c
Remove unnecessary false
ylvaselling Feb 25, 2026
adb9912
Change the fade icon as it was a bit choppy
ylvaselling Feb 25, 2026
62d1c42
Set throttling time to a better value
ylvaselling Feb 27, 2026
862d28c
Lint fix
ylvaselling Feb 27, 2026
20f85cd
Stop showing map markers when delta time is too fast
ylvaselling Feb 27, 2026
71c53ea
Fix render error with visible filter
ylvaselling Feb 27, 2026
4c56a22
Address PR comments
ylvaselling Mar 18, 2026
e1dd87c
Fix the bug with making new property updates
ylvaselling Mar 18, 2026
658e9cc
Do some proper type checks for enabled and fade
ylvaselling Mar 18, 2026
b216c33
Add visibility calculations if someone upserts many properties
ylvaselling Mar 18, 2026
9878c46
Add some behaviour to the fading action icon
ylvaselling Mar 18, 2026
e547f47
Update src/util/batcher.ts
ylvaselling Mar 19, 2026
759e1af
Update src/util/batcher.ts
ylvaselling Mar 19, 2026
eafbaa9
Address PR comments
ylvaselling Mar 19, 2026
f0f6cc7
Merge branch 'feature/property-tree-topic' of https://github.com/Open…
ylvaselling Mar 19, 2026
a2ccecf
Add metadata to property tree topic
ylvaselling Mar 19, 2026
05ca0e9
Address PR comments
ylvaselling Mar 27, 2026
8126a42
Rename propertyTree to propertytree
ylvaselling Mar 27, 2026
9d4f305
Address PR comments
ylvaselling Mar 27, 2026
eb766d3
Address PR comments
ylvaselling Mar 27, 2026
c86d3f5
Merge remote-tracking branch 'origin/master' into feature/property-tr…
ylvaselling Mar 27, 2026
fb0b317
Add comments to batcher
ylvaselling Mar 27, 2026
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: 4 additions & 0 deletions public/locales/en/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
"title": "Unavailable Map",
"description": "No map found for '{{name}}'"
},
"time-too-fast": {
"title": "Map View Unavailable",
"description": "The map is currently unavailable because the camera is moving too fast. Please slow down to view the map."
},
"aria-labels": {
"map": "Map of {{name}}",
"view-direction": "Cone showing the view direction of the camera",
Expand Down
123 changes: 38 additions & 85 deletions src/components/DynamicMap/DynamicMap.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import { PropsWithChildren, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
AspectRatio,
BackgroundImage,
Box,
Image,
LoadingOverlay,
MantineStyleProps,
Text
} from '@mantine/core';
import { Box, Image, MantineStyleProps } from '@mantine/core';

import { MapMarker } from '@/components/DynamicMap/MapMarker';
import { NightShadow } from '@/components/DynamicMap/NightShadow';
import { useSubscribeToCamera } from '@/hooks/topicSubscriptions';
import { useCameraLatLong } from '@/redux/camera/hooks';
import { useAnchorNode } from '@/util/propertyTreeHooks';

import { useMapPath } from './hooks';
import { Map } from './Map';
import { ViewCone } from './ViewCone';

// The fewer decimals we can get away with, the less the component will rerender due to
Expand Down Expand Up @@ -66,20 +56,19 @@ export function DynamicMap({
}: Props) {
const { t } = useTranslation('components', { keyPrefix: 'map' });

const refSize = useRef<HTMLDivElement>(null);

const {
latitude: currentLat,
longitude: currentLong,
viewLatitude,
viewLongitude
} = useCameraLatLong(DecimalPrecision);
const refSize = useRef<HTMLDivElement>(null);
const width = refSize?.current?.clientWidth;
const height = refSize?.current?.clientHeight;

useSubscribeToCamera();
const anchor = useAnchorNode();

const [mapPath, mapExists] = useMapPath(anchor);
const width = refSize?.current?.clientWidth;
const height = refSize?.current?.clientHeight;

const hasViewDirection = viewLatitude !== undefined && viewLongitude !== undefined;
const markerPosition = (() => {
Expand All @@ -97,77 +86,41 @@ export function DynamicMap({
// Remove jumping between 0 and -180 degrees when looking straight at surface
const cleanedAngle = Math.abs(angleDeg) === 180 ? 0 : angleDeg;

if (!anchor) {
return (
<AspectRatio ratio={2} {...styleProps} style={style}>
<BackgroundImage src={''} />
<LoadingOverlay visible={true} />
</AspectRatio>
);
}

if (!mapExists || !anchor) {
return (
<Alert variant={'light'} color={'orange'} title={t('no-map.title')}>
<Text>{t('no-map.description', { name: anchor?.name })}</Text>
</Alert>
);
}

return (
<AspectRatio
ratio={2}
mx={'auto'}
miw={300}
{...styleProps}
ref={(el) => {
if (ref && el) {
ref.current = el;
}
if (refSize && el) {
refSize.current = el;
}
}}
style={style}
>
<BackgroundImage
src={mapPath}
style={{ position: 'relative' }}
aria-label={t('aria-labels.map', { name: anchor.name })}
>
{width && height && <NightShadow width={width} height={height} />}
{children}
<MapMarker left={`${markerPosition.x * 100}%`} top={`${markerPosition.y * 100}%`}>
<Box
<Map ref={ref} refSize={refSize} style={style} {...styleProps}>
{width && height && <NightShadow width={width} height={height} />}
{children}
<MapMarker left={`${markerPosition.x * 100}%`} top={`${markerPosition.y * 100}%`}>
<Box
style={{
width: 0,
height: 0,
transform: `rotate(${cleanedAngle}deg)`
}}
>
<Image
src={iconPath}
style={{
width: 0,
height: 0,
transform: `rotate(${cleanedAngle}deg)`
width: iconSize,
height: iconSize,
position: 'absolute',
top: 0,
left: 0,
transform: 'translate(-50%, -50%)'
}}
>
<Image
src={iconPath}
style={{
width: iconSize,
height: iconSize,
position: 'absolute',
top: 0,
left: 0,
transform: 'translate(-50%, -50%)'
}}
aria-label={t('aria-labels.openspace-icon')}
/>
</Box>
</MapMarker>
{width && height && showViewDirection && hasViewDirection && (
<ViewCone
width={width}
height={height}
coneWidth={coneWidth}
coneHeight={coneHeight}
aria-label={t('aria-labels.openspace-icon')}
/>
)}
</BackgroundImage>
</AspectRatio>
</Box>
</MapMarker>
{width && height && showViewDirection && hasViewDirection && (
<ViewCone
width={width}
height={height}
coneWidth={coneWidth}
coneHeight={coneHeight}
decimalPrecision={DecimalPrecision}
/>
)}
</Map>
);
}
87 changes: 87 additions & 0 deletions src/components/DynamicMap/Map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { PropsWithChildren } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
AspectRatio,
BackgroundImage,
LoadingOverlay,
MantineStyleProps,
Text
} from '@mantine/core';

import { useSubscribeToTime } from '@/hooks/topicSubscriptions';
import { useAppSelector } from '@/redux/hooks';
import { useAnchorNode } from '@/util/propertyTreeHooks';

import { useMapPath } from './hooks';

// Settings for the OpenSpace marker and view cone
interface Props extends MantineStyleProps, PropsWithChildren {
ref?: React.RefObject<HTMLDivElement>;
refSize?: React.RefObject<HTMLDivElement | null>;
style?: React.CSSProperties;
}

export function Map({ ref, refSize, children, style, ...styleProps }: Props) {
const { t } = useTranslation('components', { keyPrefix: 'map' });
const oneDayPerSecond = 86400;

const isTimeTooFast = useAppSelector(
(state) => Math.abs(state.time.targetDeltaTime ?? 0) > oneDayPerSecond
);
const anchor = useAnchorNode();
const [mapPath, mapExists] = useMapPath(anchor);
useSubscribeToTime();

if (!anchor) {
return (
<AspectRatio ratio={2} {...styleProps} style={style}>
<BackgroundImage src={''} />
<LoadingOverlay visible={true} />
</AspectRatio>
);
}

if (!mapExists || !anchor) {
return (
<Alert variant={'light'} color={'orange'} title={t('no-map.title')}>
<Text>{t('no-map.description', { name: anchor?.name })}</Text>
</Alert>
);
}
return (
<AspectRatio
ratio={2}
mx={'auto'}
miw={300}
{...styleProps}
ref={(el) => {
if (ref && el) {
ref.current = el;
}
if (refSize && el) {
refSize.current = el;
}
}}
style={style}
>
<BackgroundImage
src={mapPath}
style={{ position: 'relative' }}
aria-label={t('aria-labels.map', { name: anchor.name })}
>
{!isTimeTooFast && children}
{isTimeTooFast && (
<Alert
variant={'filled'}
color={'orange'}
title={t('time-too-fast.title')}
style={{ position: 'absolute', top: 10, left: 10, right: 10 }}
>
{t('time-too-fast.description')}
</Alert>
)}
</BackgroundImage>
</AspectRatio>
);
}
22 changes: 16 additions & 6 deletions src/components/DynamicMap/ViewCone.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';

import { useProperty } from '@/hooks/properties';
import { usePropertyValue } from '@/hooks/properties';
import { useSubscribeToCamera } from '@/hooks/topicSubscriptions';
import { useCameraLatLong } from '@/redux/camera/hooks';
import { useAppSelector } from '@/redux/hooks';
Expand All @@ -12,20 +12,30 @@ interface Props {
height: number;
coneWidth: number;
coneHeight: number;
decimalPrecision: number;
}

// TODO: ylvse 2025-07-11 Rewrite this as a React component that uses hooks instead of D3 directly.
export function ViewCone({ width, height, coneWidth, coneHeight }: Props) {
const ref = useRef(null);
const { latitude, longitude, viewLatitude, viewLongitude } = useCameraLatLong(7);
export function ViewCone({
width,
height,
coneWidth,
coneHeight,
decimalPrecision
}: Props) {
const viewLength = useAppSelector((state) => state.camera.viewLength);
const { altitudeMeters } = useAppSelector((state) => state.camera);
const ref = useRef(null);

const anchor = useAnchorNode();
const [interactionSphere] = useProperty(
const { latitude, longitude, viewLatitude, viewLongitude } =
useCameraLatLong(decimalPrecision);
const interactionSphere = usePropertyValue(
'DoubleProperty',
`Scene.${anchor?.identifier}.EvaluatedInteractionSphere`
);
const { altitudeMeters } = useAppSelector((state) => state.camera);
useSubscribeToCamera();

const shouldShowCone =
altitudeMeters && interactionSphere && altitudeMeters < interactionSphere * 3;

Expand Down
6 changes: 4 additions & 2 deletions src/components/Property/Property.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { memo } from 'react';
import { Stack } from '@mantine/core';

import { useAppSelector } from '@/redux/hooks';
import { propertySelectors } from '@/redux/propertytree/propertySlice';
import { Uri } from '@/types/types';

import { BoolProperty } from './Types/BoolProperty';
Expand Down Expand Up @@ -79,8 +80,9 @@ interface Props {
}

export const Property = memo(({ uri }: Props) => {
const meta = useAppSelector((state) => state.properties.properties[uri]?.metaData);

const meta = useAppSelector(
(state) => propertySelectors.selectById(state, uri)?.metaData
);
if (!meta) {
return <></>;
}
Expand Down
12 changes: 9 additions & 3 deletions src/components/PropertyOwner/Custom/GlobeLayers/GlobeLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,24 @@ export function GlobeLayer({ uri }: Props) {
throw Error(`${t('error.no-property-owner-for-uri')}: ${uri}`);
}

const { isVisible } = usePropertyOwnerVisibility(uri);
const { visibility, setVisibility } = usePropertyOwnerVisibility(uri);
const visibleProperties = useVisibleProperties(propertyOwner);
const subowners = propertyOwner.subowners ?? [];

// @TODO (emmbr, 2024-12-06): We want to avoid hardcoded colors, but since changing the
// color of the text is a feature we wanted to keep I decided to do it this way for now.
const textColor = isVisible ? 'green' : undefined;
const textColor = visibility === 'Visible' ? 'green' : undefined;

return (
<Collapsable
title={<Text c={textColor}>{displayName(propertyOwner)}</Text>}
leftSection={<PropertyOwnerVisibilityCheckbox uri={uri} />}
leftSection={
<PropertyOwnerVisibilityCheckbox
uri={uri}
visibility={visibility}
setVisibility={setVisibility}
/>
}
rightSection={
<Group wrap={'nowrap'}>
<InfoBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { Collapsable } from '@/components/Collapsable/Collapsable';
import { Property } from '@/components/Property/Property';
import { usePropertyOwner } from '@/hooks/propertyOwner';
import { useAppSelector } from '@/redux/hooks';
import { propertySelectors } from '@/redux/propertytree/propertySlice';
import { Identifier, Uri } from '@/types/types';
import { displayName, isPropertyOwnerActive } from '@/util/propertyTreeHelpers';
import { checkVisibility, displayName } from '@/util/propertyTreeHelpers';
import { enabledPropertyUri, fadePropertyUri } from '@/util/uris';

import { LayerList } from './LayersList';

Expand All @@ -27,8 +29,15 @@ export function GlobeLayerGroup({ uri, globe, icon }: Props) {

const nActiveLayers = useAppSelector(
(state) =>
layers.filter((layer) => isPropertyOwnerActive(layer, state.properties.properties))
.length
layers.filter((layer) => {
const fade = propertySelectors.selectById(state, fadePropertyUri(layer))
?.value as number | undefined;
const enabled = propertySelectors.selectById(state, enabledPropertyUri(layer))
?.value as boolean | undefined;
const visibility = checkVisibility(enabled, fade);
// Count the layer as active if it is either Visible or Fading
return visibility === 'Visible' || visibility === 'Fading';
}).length
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { PropertyOwnerCollapsable } from '@/components/PropertyOwner/PropertyOwn
import { layerGroups } from '@/data/GlobeLayers';
import { usePropertyOwner } from '@/hooks/propertyOwner';
import { Uri } from '@/types/types';
import { displayName, sgnIdentifierFromSubownerUri } from '@/util/propertyTreeHelpers';
import { displayName } from '@/util/propertyTreeHelpers';
import { sgnIdentifierFromSubownerUri } from '@/util/uris';

import { GlobeLayerGroup } from './GlobeLayersGroup';

Expand Down
Loading