ebta-workflow-prod 4.0.23 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/activitypack.json +1 -1
- package/build/main.js +11 -6
- package/ebta-workflow-prod-4.0.24.tgz +0 -0
- package/eslint.config.js +12 -0
- package/package.json +54 -46
- package/src/activities/DrapGeometry.ts +3 -3
- package/src/elements/AttributeFilter.tsx +69 -75
- package/src/elements/CsColorPicker.tsx +3 -3
- package/src/elements/ItemSearcher.tsx +1 -1
- package/src/elements/ItemSearcher_v2.tsx +1 -1
- package/src/elements/Viewer360.tsx +154 -59
- package/src/elements/modules/AttributeFilterEasyComp.tsx +33 -63
- package/tsconfig.json +2 -0
- /package/{uuid.js → uuid.cjs} +0 -0
|
Binary file
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
3
|
+
import baseConfig from "@vertigis/workflow-sdk/config/eslint.config.js";
|
|
4
|
+
|
|
5
|
+
export default defineConfig([
|
|
6
|
+
globalIgnores(["**/*.js", "**/*.d.ts"]),
|
|
7
|
+
baseConfig,
|
|
8
|
+
{
|
|
9
|
+
// Add your custom rules or other eslint config overrides here.
|
|
10
|
+
rules: {}
|
|
11
|
+
}
|
|
12
|
+
]);
|
package/package.json
CHANGED
|
@@ -1,47 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
2
|
+
"name": "ebta-workflow-prod",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "4.1.0",
|
|
5
|
+
"description": "",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "vertigis-workflow-sdk build",
|
|
10
|
+
"generate": "vertigis-workflow-sdk generate",
|
|
11
|
+
"start": "vertigis-workflow-sdk start"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/react": "^19.2.18",
|
|
15
|
+
"@types/react-dom": "^19.2.4",
|
|
16
|
+
"@vertigis/workflow": "^5.52.2",
|
|
17
|
+
"@vertigis/workflow-sdk": "^6.1.1",
|
|
18
|
+
"node-fetch": "^3.3.2",
|
|
19
|
+
"typescript": "^5.4.0"
|
|
20
|
+
},
|
|
21
|
+
"browserslist": [
|
|
22
|
+
"last 2 chrome versions",
|
|
23
|
+
"last 2 edge versions",
|
|
24
|
+
"last 2 firefox versions",
|
|
25
|
+
"last 2 safari versions",
|
|
26
|
+
"ie 11"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@arcgis/core": "^5.1.19",
|
|
30
|
+
"@emotion/react": "^11.14.0",
|
|
31
|
+
"@emotion/styled": "^11.14.1",
|
|
32
|
+
"@fseehawer/react-circular-slider": "^3.2.7",
|
|
33
|
+
"@mantine/core": "^7.16.0",
|
|
34
|
+
"@mantine/hooks": "^9.5.1",
|
|
35
|
+
"@mui/icons-material": "^5.17.1",
|
|
36
|
+
"@mui/material": "^5.17.1",
|
|
37
|
+
"@mui/x-charts": "^7.6.2",
|
|
38
|
+
"@turf/turf": "^6.5.0",
|
|
39
|
+
"@types/arcgis-js-api": "^4.28.0",
|
|
40
|
+
"@vertigis/web": "^5.43.1",
|
|
41
|
+
"@xmldom/xmldom": "^0.8.10",
|
|
42
|
+
"arcgis-js-api": "^4.30.8",
|
|
43
|
+
"axios": "^1.7.7",
|
|
44
|
+
"dayjs": "^1.11.23",
|
|
45
|
+
"html2canvas": "^1.4.1",
|
|
46
|
+
"pannellum-react-update": "^1.0.0",
|
|
47
|
+
"pdf-lib": "^1.17.1",
|
|
48
|
+
"qrcode": "^1.5.4",
|
|
49
|
+
"react-color": "^2.19.3",
|
|
50
|
+
"react-color-palette": "^7.3.0",
|
|
51
|
+
"react-simple-image-slider": "^2.4.1",
|
|
52
|
+
"react-slideshow-image": "^4.3.1"
|
|
53
|
+
},
|
|
54
|
+
"type": "module"
|
|
55
|
+
}
|
|
@@ -4,8 +4,8 @@ import Multipoint from "@arcgis/core/geometry/Multipoint";
|
|
|
4
4
|
import Polyline from "@arcgis/core/geometry/Polyline";
|
|
5
5
|
import Polygon from "@arcgis/core/geometry/Polygon";
|
|
6
6
|
import ElevationLayer from "@arcgis/core/layers/ElevationLayer";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import Geometry from "@arcgis/core/geometry/Geometry";
|
|
8
|
+
import SpatialReference from "@arcgis/core/geometry/SpatialReference";
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
/** An interface that defines the inputs of the activity. */
|
|
@@ -57,7 +57,7 @@ export default class DrapGeometryActivity implements IActivityHandler {
|
|
|
57
57
|
return result.geometry as Polyline;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
let geometry3D:
|
|
60
|
+
let geometry3D:Point | Multipoint | Polyline | Polygon;
|
|
61
61
|
|
|
62
62
|
// Handle different types of geometry
|
|
63
63
|
if (inputs.inputGeometry instanceof Point) {
|
|
@@ -18,14 +18,14 @@ import Box from "@vertigis/web/ui/Box"
|
|
|
18
18
|
import Checkbox from "@vertigis/web/ui/Checkbox"
|
|
19
19
|
import { ThemeProvider } from "@emotion/react";
|
|
20
20
|
import Typography from '@vertigis/web/ui/Typography';
|
|
21
|
-
import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
|
|
21
|
+
// import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
|
|
22
22
|
import dayjs, { Dayjs } from 'dayjs';
|
|
23
23
|
import timezone from 'dayjs/plugin/timezone';
|
|
24
24
|
import utc from 'dayjs/plugin/utc';
|
|
25
25
|
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
|
26
|
-
import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
|
|
27
|
-
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
28
|
-
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
26
|
+
// import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
|
|
27
|
+
// import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
28
|
+
// import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
29
29
|
import 'dayjs/locale/en-au';
|
|
30
30
|
import Chip from '@mui/material/Chip';
|
|
31
31
|
import Button from '@vertigis/web/ui/Button';
|
|
@@ -34,8 +34,8 @@ import RadioGroup from '@vertigis/web/ui/RadioGroup';
|
|
|
34
34
|
import FormControlLabel from '@vertigis/web/ui/FormControlLabel';
|
|
35
35
|
import AttributeFilterEasyComp from './modules/AttributeFilterEasyComp';
|
|
36
36
|
import Stack from '@vertigis/web/ui/Stack';
|
|
37
|
-
import Graphic from "
|
|
38
|
-
const { Search, SearchIconWrapper, StyledInputBase, StyledBadge, LightTooltip, defaultTheme
|
|
37
|
+
import Graphic from "@arcgis/core/Graphic";
|
|
38
|
+
const { Search, SearchIconWrapper, StyledInputBase, StyledBadge, LightTooltip, defaultTheme } = AttributeFilterEasyComp;
|
|
39
39
|
dayjs.extend(utc);
|
|
40
40
|
dayjs.extend(timezone);
|
|
41
41
|
dayjs.extend(customParseFormat);
|
|
@@ -1392,80 +1392,74 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
|
|
|
1392
1392
|
{attribute.uniqValue ?
|
|
1393
1393
|
(<Collapse in={attribute.display} timeout="auto" unmountOnExit>
|
|
1394
1394
|
{attribute.type && attribute.type == 'date' && (attribute.uniqValue.length > 6) ?
|
|
1395
|
-
<Box display="flex" alignItems="center" >
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1395
|
+
// <Box display="flex" alignItems="center" >
|
|
1396
|
+
// <LocalizationProvider dateAdapter={AdapterDayjs} >
|
|
1397
|
+
// <DemoContainer components={['DatePicker', 'DatePicker']} >
|
|
1398
|
+
// <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en-au">
|
|
1399
|
+
// <DateRangePicker defaultValue={[dayjs.unix(minDate.current), dayjs.unix(maxDate.current)]} maxDate={dayjs.unix(maxDate.current)} minDate={dayjs.unix(minDate.current)}
|
|
1400
|
+
// value={[startDate, endDate]}
|
|
1401
|
+
// onChange={([newstart, newend]) => handleDateRangeChange(newstart || startDate, newend || endDate)}
|
|
1402
|
+
// sx={{
|
|
1403
|
+
// // Customize the date text color
|
|
1404
|
+
// '& .MuiInputBase-input': {
|
|
1405
|
+
// color: 'var(--accentIconBorder)', // Change text color for the selected date
|
|
1406
|
+
// },
|
|
1407
|
+
|
|
1408
|
+
// // Customize the start & end labels
|
|
1409
|
+
// '& .MuiInputLabel-root': {
|
|
1410
|
+
// color: 'var(--accentIconBorder)', // Change label color for both start and end dates
|
|
1411
|
+
// },
|
|
1412
|
+
|
|
1413
|
+
// // Customize the border color of the input
|
|
1414
|
+
// '& .MuiOutlinedInput-root': {
|
|
1415
|
+
// '& fieldset': {
|
|
1416
|
+
// borderColor: 'var(--accentIconBorder)', // Change border color when the input is not focused
|
|
1417
|
+
// },
|
|
1418
|
+
// '&:hover fieldset': {
|
|
1419
|
+
// borderColor: 'var(--accentIconBorder)', // Change border color on hover
|
|
1420
|
+
// },
|
|
1421
|
+
// '&.Mui-focused fieldset': {
|
|
1422
|
+
// borderColor: 'var(--accentIconBorder)', // Change border color when focused
|
|
1423
|
+
// },
|
|
1424
|
+
// },
|
|
1425
|
+
// }}
|
|
1426
|
+
|
|
1427
|
+
// />
|
|
1428
|
+
// </LocalizationProvider>
|
|
1429
|
+
// </DemoContainer>
|
|
1430
|
+
// </LocalizationProvider>
|
|
1431
|
+
// </Box>
|
|
1432
|
+
|
|
1433
|
+
<Box display="flex" alignItems="center" gap={1}>
|
|
1434
|
+
<input
|
|
1435
|
+
type="date"
|
|
1436
|
+
value={startDate ? startDate.format('YYYY-MM-DD') : ''}
|
|
1437
|
+
min={dayjs.unix(minDate.current).format('YYYY-MM-DD')}
|
|
1438
|
+
max={dayjs.unix(maxDate.current).format('YYYY-MM-DD')}
|
|
1439
|
+
onChange={(e) => {
|
|
1440
|
+
const newstart = e.target.value ? dayjs(e.target.value) : startDate;
|
|
1441
|
+
handleDateRangeChange(newstart, endDate);
|
|
1442
|
+
}}
|
|
1443
|
+
style={{ color: 'var(--accentIconBorder)', border: '1px solid var(--accentIconBorder)', borderRadius: 4, padding: '4px 8px', marginRight: 8 }}
|
|
1444
|
+
/>
|
|
1445
|
+
<span>to</span>
|
|
1446
|
+
<input
|
|
1447
|
+
type="date"
|
|
1448
|
+
value={endDate ? endDate.format('YYYY-MM-DD') : ''}
|
|
1449
|
+
min={dayjs.unix(minDate.current).format('YYYY-MM-DD')}
|
|
1450
|
+
max={dayjs.unix(maxDate.current).format('YYYY-MM-DD')}
|
|
1451
|
+
onChange={(e) => {
|
|
1452
|
+
const newend = e.target.value ? dayjs(e.target.value) : endDate;
|
|
1453
|
+
handleDateRangeChange(startDate, newend);
|
|
1454
|
+
}}
|
|
1455
|
+
style={{ color: 'var(--accentIconBorder)', border: '1px solid var(--accentIconBorder)', borderRadius: 4, padding: '4px 8px', marginLeft: 8 }}
|
|
1456
|
+
/>
|
|
1431
1457
|
</Box>
|
|
1432
1458
|
:
|
|
1433
1459
|
ISnumberAttribute && (attribute.uniqValue.length > 6) ?
|
|
1434
1460
|
//Display String search or number Range , don't display if value amount is limited
|
|
1435
1461
|
<Box display="flex" alignItems="center">
|
|
1436
|
-
|
|
1437
|
-
value={minValue == -Infinity ? undefined : minValue} step={0.1}
|
|
1438
|
-
onChange={(event, val) => {
|
|
1439
|
-
if (((val || val == 0) && (val <= maxValue))) { setProperty("minValue" as any, val) };
|
|
1440
|
-
if (val == undefined || val == null) { setProperty("minValue" as any, -Infinity) };
|
|
1441
|
-
|
|
1442
|
-
if ((val || val == 0) && val > maxValue) {
|
|
1443
|
-
setProperty("minValue" as any, maxValue - 1)
|
|
1444
|
-
const filterrangeResult = filterRange(attribute, maxValue - 1, maxValue)
|
|
1445
|
-
setProperty("filteredValues" as any, filterrangeResult)
|
|
1446
|
-
}
|
|
1447
|
-
else {
|
|
1448
|
-
const filterrangeResult = filterRange(attribute, val !== null ? val : -Infinity, maxValue !== Infinity ? maxValue : Infinity)
|
|
1449
|
-
setProperty("filteredValues" as any, filterrangeResult)
|
|
1450
|
-
}
|
|
1451
|
-
}}
|
|
1452
|
-
/>
|
|
1453
|
-
<CustomNumberInput aria-label="max value input" placeholder={`Max (${attributeMin} ~ ${attributeMax})`} min={attributeMin - 1} max={attributeMax + 1}
|
|
1454
|
-
value={maxValue == Infinity ? undefined : maxValue} step={0.1}
|
|
1455
|
-
onChange={(_event, val) => {
|
|
1456
|
-
if ((val || val == 0) && (val >= minValue)) { setProperty("maxValue" as any, val) };
|
|
1457
|
-
if (val == undefined) { setProperty("maxValue" as any, Infinity) };
|
|
1458
|
-
if ((val || val == 0) && val < minValue) {
|
|
1459
|
-
setProperty("maxValue" as any, minValue + 1)
|
|
1460
|
-
const filterrangeResult = filterRange(attribute, minValue, minValue + 1)
|
|
1461
|
-
setProperty("filteredValues" as any, filterrangeResult)
|
|
1462
|
-
}
|
|
1463
|
-
else {
|
|
1464
|
-
const filterrangeResult = filterRange(attribute, minValue !== -Infinity ? minValue : -Infinity, val !== null ? val : Infinity || Infinity)
|
|
1465
|
-
setProperty("filteredValues" as any, filterrangeResult)
|
|
1466
|
-
}
|
|
1467
|
-
}}
|
|
1468
|
-
/>
|
|
1462
|
+
|
|
1469
1463
|
</Box>
|
|
1470
1464
|
:
|
|
1471
1465
|
(attribute.uniqValue.length > 6) || searchValue ?
|
|
@@ -2,11 +2,11 @@ import * as React from "react";
|
|
|
2
2
|
import { MantineProvider } from '@mantine/core';
|
|
3
3
|
import { FormElementProps, FormElementRegistration } from "@vertigis/workflow";
|
|
4
4
|
import { ColorInput, ColorPicker } from '@mantine/core';
|
|
5
|
-
|
|
5
|
+
type ColorFormat = "hex" | "rgba" | "rgb" | "hsl" | "hsla";
|
|
6
6
|
|
|
7
7
|
import "@mantine/core/styles.css";
|
|
8
8
|
|
|
9
|
-
import Color from "
|
|
9
|
+
import Color from "@arcgis/core/Color";
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
declare module "@vertigis/workflow" {
|
|
@@ -73,7 +73,7 @@ function CsColorPickerV2(props: CsColorPickerV2Props): React.ReactElement {
|
|
|
73
73
|
esriColor = new Color(color);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
setProperty("esriColor" as any, esriColor);
|
|
77
77
|
setValue(color);
|
|
78
78
|
raiseEvent("changed", color);
|
|
79
79
|
};
|
|
@@ -195,7 +195,7 @@ function ItemSearcherDEV(props: ItemSearcherDEVProps): React.ReactElement {
|
|
|
195
195
|
const [data, setData] = React.useState<Category[]>(props.itemData);
|
|
196
196
|
const isFirstRender = React.useRef(true);
|
|
197
197
|
const preSearchOpenCategories = React.useRef<Set<string>>(new Set());
|
|
198
|
-
const loadDataTime = React.useRef<number>();
|
|
198
|
+
const loadDataTime = React.useRef<number | undefined>(undefined);
|
|
199
199
|
const currentOpenCategories = React.useRef<Set<string>>(new Set());
|
|
200
200
|
const prevSearchValue = React.useRef<string>('')
|
|
201
201
|
const [selection, setSelection] = React.useState<{ [key: string]: { [key: string]: Item[] } }>({});
|
|
@@ -183,7 +183,7 @@ function ItemSearcher_V2(props: ItemSearcher_V2Props): React.ReactElement {
|
|
|
183
183
|
const [filteredData, setFilteredData] = React.useState<Category[]>([]); // New state for filtered data
|
|
184
184
|
const requestTime = React.useRef<number | null>(null)
|
|
185
185
|
const [inputValue, setInputValue] = React.useState<string>(searchQuery);
|
|
186
|
-
const debounceTimer = React.useRef<ReturnType<typeof setTimeout>>();
|
|
186
|
+
const debounceTimer = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
187
187
|
const latestOpenCategories = React.useRef<Set<string>>(openCategories);
|
|
188
188
|
|
|
189
189
|
|
|
@@ -12,6 +12,14 @@ import { Pannellum } from "pannellum-react-update";
|
|
|
12
12
|
* runtime, and passed in as `props`. You can update the properties by using
|
|
13
13
|
* `props.setProperty(key, value)`.
|
|
14
14
|
*/
|
|
15
|
+
|
|
16
|
+
interface Camera {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
pitch: number;
|
|
20
|
+
yaw: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
interface Viewer360Props extends FormElementProps<string> {
|
|
16
24
|
value: string //url of image
|
|
17
25
|
pitch: number; //pitch of the image
|
|
@@ -24,6 +32,7 @@ interface Viewer360Props extends FormElementProps<string> {
|
|
|
24
32
|
width?: string;
|
|
25
33
|
height?: string;
|
|
26
34
|
};
|
|
35
|
+
cameras?: Camera[];
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -33,49 +42,155 @@ interface Viewer360Props extends FormElementProps<string> {
|
|
|
33
42
|
* @param props The props that will be provided by the Workflow runtime.
|
|
34
43
|
*/
|
|
35
44
|
function Viewer360(props: Viewer360Props): React.ReactElement {
|
|
36
|
-
const { setting, value, pitch, yaw, hfov, minHfov, haov, vaov, raiseEvent } = props;
|
|
45
|
+
const { setting, value, pitch, yaw, hfov, minHfov, haov, vaov, cameras = [], raiseEvent } = props;
|
|
37
46
|
|
|
38
|
-
const handleHotspotClick = (
|
|
39
|
-
raiseEvent("clicked", JSON.stringify({ args }))
|
|
47
|
+
const handleHotspotClick = (_evt: MouseEvent, args: { id: string; name: string }) => {
|
|
48
|
+
raiseEvent("clicked", JSON.stringify({ id: args.id, name: args.name }));
|
|
40
49
|
};
|
|
41
50
|
|
|
42
|
-
// Copy SVG assets to consumer's public folder and reference them
|
|
43
51
|
React.useEffect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const css = `
|
|
53
|
+
/* ── hotspot host ── */
|
|
46
54
|
.pnlm-hotspot.custom-hotspot {
|
|
47
|
-
width:
|
|
48
|
-
height:
|
|
49
|
-
overflow: visible;
|
|
55
|
+
width: 40px;
|
|
56
|
+
height: 40px;
|
|
50
57
|
background: none;
|
|
51
58
|
border: none;
|
|
59
|
+
overflow: visible;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── pulse ring ── */
|
|
66
|
+
.hs-btn {
|
|
67
|
+
position: relative;
|
|
68
|
+
width: 36px;
|
|
69
|
+
height: 36px;
|
|
70
|
+
border-radius: 50%;
|
|
71
|
+
background: rgba(255, 255, 255, 0.50);
|
|
72
|
+
backdrop-filter: blur(4px);
|
|
73
|
+
border: 1.5px solid rgba(255, 255, 255, 0.75);
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
transition: background 0.2s ease, transform 0.2s ease;
|
|
79
|
+
animation: hs-shake 3s ease-in-out infinite;
|
|
52
80
|
}
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
|
|
82
|
+
.hs-btn::before {
|
|
83
|
+
content: '';
|
|
84
|
+
position: absolute;
|
|
85
|
+
inset: -6px;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
border: 1.5px solid rgba(255, 255, 255, 0.35);
|
|
88
|
+
animation: hs-pulse 2.4s ease-out infinite;
|
|
89
|
+
pointer-events: none;
|
|
55
90
|
}
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
|
|
92
|
+
@keyframes hs-pulse {
|
|
93
|
+
0% { transform: scale(0.85); opacity: 0.6; }
|
|
94
|
+
70% { transform: scale(1.35); opacity: 0; }
|
|
95
|
+
100% { transform: scale(1.35); opacity: 0; }
|
|
58
96
|
}
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
|
|
98
|
+
@keyframes hs-shake {
|
|
99
|
+
0%, 100% { transform: translateY(0px); }
|
|
100
|
+
20% { transform: translateY(-3px); }
|
|
101
|
+
40% { transform: translateY(0px); }
|
|
102
|
+
60% { transform: translateY(-2px); }
|
|
103
|
+
80% { transform: translateY(0px); }
|
|
61
104
|
}
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
|
|
106
|
+
/* ── camera icon svg ── */
|
|
107
|
+
.hs-icon {
|
|
108
|
+
width: 16px;
|
|
109
|
+
height: 16px;
|
|
110
|
+
fill: white;
|
|
111
|
+
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.4));
|
|
112
|
+
flex-shrink: 0;
|
|
64
113
|
}
|
|
65
|
-
|
|
66
|
-
|
|
114
|
+
|
|
115
|
+
/* ── tooltip ── */
|
|
116
|
+
.hs-tooltip {
|
|
117
|
+
position: absolute;
|
|
118
|
+
bottom: calc(100% + 10px);
|
|
119
|
+
left: 50%;
|
|
120
|
+
transform: translateX(-50%);
|
|
121
|
+
background: rgba(0, 0, 0, 0.45);
|
|
122
|
+
backdrop-filter: blur(6px);
|
|
123
|
+
color: #fff;
|
|
124
|
+
font-size: 12px;
|
|
125
|
+
font-family: sans-serif;
|
|
126
|
+
font-weight: 500;
|
|
127
|
+
white-space: nowrap;
|
|
128
|
+
padding: 4px 10px;
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
131
|
+
pointer-events: none;
|
|
132
|
+
opacity: 0;
|
|
133
|
+
transition: opacity 0.18s ease;
|
|
67
134
|
}
|
|
68
|
-
`;
|
|
69
135
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
136
|
+
.hs-tooltip::after {
|
|
137
|
+
content: '';
|
|
138
|
+
position: absolute;
|
|
139
|
+
top: 100%;
|
|
140
|
+
left: 50%;
|
|
141
|
+
transform: translateX(-50%);
|
|
142
|
+
border: 5px solid transparent;
|
|
143
|
+
border-top-color: rgba(0,0,0,0.45);
|
|
144
|
+
}
|
|
73
145
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
146
|
+
/* show tooltip + brighten on hover */
|
|
147
|
+
.hs-btn:hover {
|
|
148
|
+
background: rgba(255, 255, 255, 0.75);
|
|
149
|
+
animation: none;
|
|
150
|
+
transform: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.hs-btn:hover .hs-tooltip {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── pannellum svg assets ── */
|
|
158
|
+
.pnlm-container { background-image: url('/img/background.svg'); }
|
|
159
|
+
.pnlm-compass { background-image: url('/img/compass.svg'); }
|
|
160
|
+
.pnlm-grab { cursor: url('/img/grab.svg'), grab; }
|
|
161
|
+
.pnlm-grabbing { cursor: url('/img/grabbing.svg'), grabbing; }
|
|
162
|
+
.pnlm-sprite { background-image: url('/img/sprites.svg'); }
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
const el = document.createElement("style");
|
|
166
|
+
el.textContent = css;
|
|
167
|
+
document.head.appendChild(el);
|
|
168
|
+
return () => { document.head.removeChild(el); };
|
|
77
169
|
}, []);
|
|
78
170
|
|
|
171
|
+
// Tooltip + icon injected once per hotspot div
|
|
172
|
+
const buildTooltip = (name: string) => (hotSpotDiv: HTMLElement) => {
|
|
173
|
+
hotSpotDiv.style.cssText = "width:40px;height:40px;overflow:visible;";
|
|
174
|
+
|
|
175
|
+
const btn = document.createElement("div");
|
|
176
|
+
btn.className = "hs-btn";
|
|
177
|
+
|
|
178
|
+
// Camera SVG icon
|
|
179
|
+
btn.innerHTML = `
|
|
180
|
+
<svg class="hs-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
181
|
+
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8
|
|
182
|
+
a2 2 0 0 1 2-2h4l2-3h6l2 3h4
|
|
183
|
+
a2 2 0 0 1 2 2z"/>
|
|
184
|
+
<circle cx="12" cy="13" r="4" fill="none"
|
|
185
|
+
stroke="rgba(0,0,0,0.55)" stroke-width="1.8"/>
|
|
186
|
+
</svg>
|
|
187
|
+
<div class="hs-tooltip">${name}</div>
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
hotSpotDiv.appendChild(btn);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
|
|
79
194
|
|
|
80
195
|
console.log("rendering 360VIEWER")
|
|
81
196
|
return (
|
|
@@ -96,39 +211,19 @@ function Viewer360(props: Viewer360Props): React.ReactElement {
|
|
|
96
211
|
console.log("panorama loaded");
|
|
97
212
|
}}
|
|
98
213
|
>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
padding: 6px 12px;
|
|
113
|
-
cursor: pointer;
|
|
114
|
-
font-size: 13px;
|
|
115
|
-
white-space: nowrap;
|
|
116
|
-
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
117
|
-
transform: translate(-50%, -100%);
|
|
118
|
-
`;
|
|
119
|
-
btn.textContent = "📍 View Detail";
|
|
120
|
-
hotSpotDiv.appendChild(btn); // append a child, don't style the host div
|
|
121
|
-
}}
|
|
122
|
-
tooltipArg={{}}
|
|
123
|
-
/>
|
|
124
|
-
|
|
125
|
-
<Pannellum.Hotspot
|
|
126
|
-
type="info"
|
|
127
|
-
pitch={31}
|
|
128
|
-
yaw={-57}
|
|
129
|
-
text="Info"
|
|
130
|
-
URL="https://github.com/farminf"
|
|
131
|
-
/>
|
|
214
|
+
{cameras.map((cam) => (
|
|
215
|
+
<Pannellum.Hotspot
|
|
216
|
+
key={cam.id}
|
|
217
|
+
type="custom"
|
|
218
|
+
pitch={cam.pitch}
|
|
219
|
+
yaw={cam.yaw}
|
|
220
|
+
cssClass="custom-hotspot"
|
|
221
|
+
handleClick={handleHotspotClick}
|
|
222
|
+
handleClickArg={{ id: cam.id, name: cam.name }}
|
|
223
|
+
tooltip={buildTooltip(cam.name)}
|
|
224
|
+
tooltipArg={{}}
|
|
225
|
+
/>
|
|
226
|
+
))}
|
|
132
227
|
|
|
133
228
|
</Pannellum>
|
|
134
229
|
</>
|