Skip to content

Commit 33e2792

Browse files
CopilotAlexLipp
andauthored
Merge main (PR#45: downstream impact popup) into Scottish Water branch
- Bring in DownstreamImpactPopup component and AddDownstreamImpactCommand - Merge infoUrl field into ArcGISWaterCompanyConfig type - Update AddDownstreamImpactCommand to skip Scottish Water (no infoUrl) using isArcGISWaterCompanyConfig type guard Co-authored-by: AlexLipp <10188895+AlexLipp@users.noreply.github.com>
2 parents 1444ea3 + fc3d677 commit 33e2792

11 files changed

Lines changed: 459 additions & 10 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
# production
1616
/build
17+
/dist
1718

1819
# misc
1920
.DS_Store

package-lock.json

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@testing-library/jest-dom": "^6.6.3",
6161
"@testing-library/react": "^16.3.0",
6262
"@testing-library/user-event": "^14.6.1",
63-
"@types/node": "^22.19.17",
63+
"@types/node": "^25.5.0",
6464
"@types/react": "19.1.2",
6565
"@types/react-dom": "19.1.2",
6666
"@vitejs/plugin-react-swc": "^3.9.0",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import styled from '@emotion/styled';
2+
import { Box, ScrollArea, Text } from '@radix-ui/themes';
3+
import Wave from 'react-wavify';
4+
5+
import { usePrefersReducedMotion } from '../../lib/hooks/usePrefersReducedMotion';
6+
import Tabs from '../common/Tabs/Tabs';
7+
import { Em } from '../common/Text';
8+
import { DownstreamImpactProperties } from './types';
9+
10+
const BodyWrapper = styled.div`
11+
position: relative;
12+
overflow: hidden;
13+
`;
14+
15+
const BackgroundWave = styled(Wave)`
16+
position: absolute;
17+
max-width: unset;
18+
min-width: 100%;
19+
height: 400px;
20+
top: -24px;
21+
`;
22+
23+
const ContentWrapper = styled(Box)`
24+
width: 100%;
25+
padding: 0px 4px;
26+
position: relative;
27+
overflow: hidden;
28+
`;
29+
30+
const DataCardWrapper = styled.div`
31+
--shadow-color: var(--river-blue);
32+
--shadow-elevation-medium:
33+
0.2px 0.3px 0.4px hsl(var(--shadow-color) / 0.1),
34+
0.5px 1px 1.2px -0.8px hsl(var(--shadow-color) / 0.09),
35+
1.2px 2.5px 3.1px -1.7px hsl(var(--shadow-color) / 0.09),
36+
3px 6px 7.5px -2.5px hsl(var(--shadow-color) / 0.09);
37+
margin-bottom: 8px;
38+
padding: 8px;
39+
border-radius: 8px;
40+
box-shadow: var(--shadow-elevation-medium);
41+
border: var(--river-blue) solid 1px;
42+
display: flex;
43+
flex-direction: column;
44+
justify-content: center;
45+
background-color: var(--color-panel);
46+
position: relative;
47+
min-height: 50px;
48+
`;
49+
50+
const StatRow = styled.div`
51+
display: flex;
52+
justify-content: space-between;
53+
align-items: center;
54+
padding: 2px 0;
55+
gap: 8px;
56+
`;
57+
58+
type DownstreamImpactPopupBodyProps = DownstreamImpactProperties;
59+
60+
export function DownstreamImpactPopupBody({
61+
number_upstream_CSOs,
62+
number_CSOs_per_km2,
63+
CSOs,
64+
}: DownstreamImpactPopupBodyProps) {
65+
const prefersReducedMotion = usePrefersReducedMotion();
66+
67+
return (
68+
<BodyWrapper>
69+
<BackgroundWave
70+
fill="var(--wave-brown)"
71+
paused={prefersReducedMotion}
72+
options={{
73+
height: 30,
74+
amplitude: 20,
75+
speed: 0.1,
76+
points: 6,
77+
}}
78+
/>
79+
<BackgroundWave
80+
fill="var(--wave-blue)"
81+
paused={prefersReducedMotion}
82+
options={{
83+
height: 25,
84+
amplitude: 15,
85+
speed: 0.175,
86+
points: 5,
87+
}}
88+
/>
89+
<ContentWrapper>
90+
<Tabs.Root defaultValue="summary" aria-label="Choose which information to see:">
91+
<Tabs.List>
92+
<Tabs.Trigger value="summary">Summary</Tabs.Trigger>
93+
<Tabs.Trigger value="spills">Upstream Spills</Tabs.Trigger>
94+
</Tabs.List>
95+
<Box pt={'3'}>
96+
<Tabs.Content value="summary">
97+
<DataCardWrapper>
98+
<StatRow>
99+
<Text size="2">
100+
<Em>Number of upstream spills:</Em>
101+
</Text>
102+
<Text size="3" weight="bold">
103+
{number_upstream_CSOs}
104+
</Text>
105+
</StatRow>
106+
<StatRow>
107+
<Text size="2">
108+
<Em>Number of upstream spills per km²:</Em>
109+
</Text>
110+
<Text size="3" weight="bold">
111+
{parseFloat(number_CSOs_per_km2.toPrecision(3)).toString()}
112+
</Text>
113+
</StatRow>
114+
<Text size="1" color="gray" mt="2" as="p">
115+
Values include all spills that have occurred in the previous 48 hours
116+
</Text>
117+
</DataCardWrapper>
118+
</Tabs.Content>
119+
<Tabs.Content value="spills">
120+
<DataCardWrapper>
121+
<ScrollArea type="auto" scrollbars="vertical" style={{ maxHeight: 160 }}>
122+
{CSOs.length > 0 ? (
123+
<Text size="2" as="p">
124+
{CSOs.join(', ')}
125+
</Text>
126+
) : (
127+
<Text size="2" color="gray" as="p">
128+
No upstream spills recorded.
129+
</Text>
130+
)}
131+
</ScrollArea>
132+
<Text size="1" color="gray" mt="2" as="p">
133+
These are the names/IDs of CSOs upstream of this point that have spilled in the
134+
last 48 hours.
135+
</Text>
136+
</DataCardWrapper>
137+
</Tabs.Content>
138+
</Box>
139+
</Tabs.Root>
140+
</ContentWrapper>
141+
</BodyWrapper>
142+
);
143+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Flex, Heading, Text } from '@radix-ui/themes';
2+
3+
import { Em } from '../common/Text';
4+
5+
type DownstreamImpactPopupHeaderProps = {
6+
company: string;
7+
longitude: number;
8+
latitude: number;
9+
};
10+
11+
export function DownstreamImpactPopupHeader({
12+
company,
13+
longitude,
14+
latitude,
15+
}: DownstreamImpactPopupHeaderProps) {
16+
const lon = longitude.toFixed(4);
17+
const lat = latitude.toFixed(4);
18+
19+
return (
20+
<Flex gap="2" align="center">
21+
<div>
22+
<Heading size={'4'} as="h3">
23+
{company} Downstream Impact
24+
</Heading>
25+
<Text size={'2'}>
26+
<Em>
27+
Downstream impact at ({lat}, {lon})
28+
</Em>
29+
</Text>
30+
</div>
31+
</Flex>
32+
);
33+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import '@radix-ui/themes/styles.css';
2+
3+
import Point from '@arcgis/core/geometry/Point';
4+
import PopupTemplate from '@arcgis/core/PopupTemplate';
5+
import React from 'react';
6+
import { flushSync } from 'react-dom';
7+
import { createRoot } from 'react-dom/client';
8+
9+
import { AppTheme } from '../Theme/AppTheme';
10+
import { AppThemeProvider } from '../Theme/ThemeProvider';
11+
import { DownstreamImpactPopupBody } from './DownstreamImpactPopupBody';
12+
import { DownstreamImpactPopupHeader } from './DownstreamImpactPopupHeader';
13+
import { DownstreamImpactProperties } from './types';
14+
15+
function getPropertiesFromGraphic(graphic: __esri.Graphic): DownstreamImpactProperties {
16+
const attrs = graphic.attributes as Record<string, unknown>;
17+
18+
let CSOs: string[] = [];
19+
const rawCSOs = attrs['CSOs'];
20+
if (Array.isArray(rawCSOs)) {
21+
CSOs = rawCSOs as string[];
22+
} else if (typeof rawCSOs === 'string' && rawCSOs.trim().length > 0) {
23+
try {
24+
const parsed = JSON.parse(rawCSOs) as unknown;
25+
if (Array.isArray(parsed)) {
26+
CSOs = parsed as string[];
27+
}
28+
} catch {
29+
// ArcGIS may serialise Python-style lists with single quotes; normalise and retry
30+
try {
31+
const normalised = rawCSOs.replace(/'/g, '"');
32+
const parsed = JSON.parse(normalised) as unknown;
33+
if (Array.isArray(parsed)) {
34+
CSOs = parsed as string[];
35+
}
36+
} catch {
37+
// Fall back to treating the whole string as a comma-separated list
38+
CSOs = rawCSOs
39+
.replace(/^\[|\]$/g, '')
40+
.split(',')
41+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
42+
.filter(Boolean);
43+
}
44+
}
45+
}
46+
47+
return {
48+
CSOs,
49+
number_upstream_CSOs: Number(attrs['number_upstream_CSOs'] ?? 0),
50+
number_CSOs_per_km2: Number(attrs['number_CSOs_per_km2'] ?? 0),
51+
};
52+
}
53+
54+
function getCoordinatesFromGraphic(graphic: __esri.Graphic): {
55+
longitude: number;
56+
latitude: number;
57+
} {
58+
const point = graphic.geometry as Point | null;
59+
return {
60+
longitude: point?.longitude ?? 0,
61+
latitude: point?.latitude ?? 0,
62+
};
63+
}
64+
65+
function createHTMLContentFn() {
66+
return function ({ graphic }: { graphic: __esri.Graphic }) {
67+
const container = document.createElement('div');
68+
const root = createRoot(container);
69+
const properties = getPropertiesFromGraphic(graphic);
70+
71+
root.render(
72+
<React.StrictMode>
73+
<AppThemeProvider
74+
theme={{
75+
accentColor: 'blue',
76+
grayColor: 'gray',
77+
panelBackground: 'solid',
78+
}}
79+
isChild={true}
80+
>
81+
<AppTheme>
82+
<DownstreamImpactPopupBody
83+
CSOs={properties.CSOs}
84+
number_upstream_CSOs={properties.number_upstream_CSOs}
85+
number_CSOs_per_km2={properties.number_CSOs_per_km2}
86+
/>
87+
</AppTheme>
88+
</AppThemeProvider>
89+
</React.StrictMode>,
90+
);
91+
return container;
92+
};
93+
}
94+
95+
function createTitleFn(companyName: string) {
96+
return function ({ graphic }: { graphic: __esri.Graphic }) {
97+
const container = document.createElement('div');
98+
const root = createRoot(container);
99+
const { longitude, latitude } = getCoordinatesFromGraphic(graphic);
100+
101+
flushSync(() => {
102+
root.render(
103+
<React.StrictMode>
104+
<AppThemeProvider
105+
theme={{
106+
accentColor: 'blue',
107+
grayColor: 'gray',
108+
panelBackground: 'solid',
109+
}}
110+
isChild={true}
111+
>
112+
<AppTheme>
113+
<DownstreamImpactPopupHeader
114+
company={companyName}
115+
longitude={longitude}
116+
latitude={latitude}
117+
/>
118+
</AppTheme>
119+
</AppThemeProvider>
120+
</React.StrictMode>,
121+
);
122+
});
123+
124+
return container.innerHTML;
125+
};
126+
}
127+
128+
export function createDownstreamImpactPopupTemplate(companyName: string): PopupTemplate {
129+
return new PopupTemplate({
130+
title: createTitleFn(companyName),
131+
returnGeometry: true,
132+
content: createHTMLContentFn(),
133+
});
134+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type DownstreamImpactProperties = {
2+
CSOs: string[];
3+
number_CSOs_per_km2: number;
4+
number_upstream_CSOs: number;
5+
};

0 commit comments

Comments
 (0)