@indiscale/linkahead-webui-ext-map 0.5.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/.eslintrc.json +45 -0
- package/.gitlab-ci.yml +44 -0
- package/CHANGELOG.md +78 -0
- package/README.md +97 -0
- package/RELEASE_GUIDELINES.md +45 -0
- package/__mocks__/fileMock.js +3 -0
- package/__mocks__/styleMock.js +1 -0
- package/babel.config.js +22 -0
- package/cypress/e2e/standalone-map.cy.js +55 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/e2e.js +17 -0
- package/cypress.config.js +10 -0
- package/dist/2b3e1faf89f94a483539.png +0 -0
- package/dist/416d91365b44e4b4f477.png +0 -0
- package/dist/8f2c4d11474275fbc161.png +0 -0
- package/dist/index.html +1 -0
- package/dist/linkahead-webui-ext-map.js +3 -0
- package/dist/linkahead-webui-ext-map.js.LICENSE.txt +45 -0
- package/dist/linkahead-webui-ext-map.js.map +1 -0
- package/iframe/index.html +6 -0
- package/indiscale-linkahead-webui-ext-map-0.4.1.tgz +0 -0
- package/jest.config.js +23 -0
- package/jest.setup.js +2 -0
- package/package.json +105 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +11 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/map_tile_caosdb_logo.png +0 -0
- package/public/mock.js +41 -0
- package/public/robots.txt +3 -0
- package/select_query.json +3 -0
- package/src/AllMapEntities.tsx +294 -0
- package/src/CurrentPageEntities.js +318 -0
- package/src/Map.helpers.css +8 -0
- package/src/Map.helpers.js +536 -0
- package/src/Map.js +288 -0
- package/src/Map.test.js +252 -0
- package/src/MapConfig.js +75 -0
- package/src/__snapshots__/Map.test.js.snap +1725 -0
- package/src/components/Coordinates.js +24 -0
- package/src/components/ErrorComponent.tsx +2 -0
- package/src/components/Graticule.js +27 -0
- package/src/components/Loader.module.css +17 -0
- package/src/components/Loader.tsx +36 -0
- package/src/components/PathDropDown.js +108 -0
- package/src/components/SearchControl.js +502 -0
- package/src/components/ToggleMapButton.js +194 -0
- package/src/components/ViewChangeControl.js +104 -0
- package/src/constants/index.js +1 -0
- package/src/context/ConfigProvider.test.js +232 -0
- package/src/context/ConfigProvider.tsx +189 -0
- package/src/context/LoadingProvider.test.js +124 -0
- package/src/context/LoadingProvider.tsx +117 -0
- package/src/context/PathIdProvider.js +102 -0
- package/src/contrib/latlnggraticule/LICENSE +20 -0
- package/src/contrib/latlnggraticule/README.md +68 -0
- package/src/contrib/latlnggraticule/leaflet.latlng-graticule.js +528 -0
- package/src/contrib/simplegraticule/L.Graticule.js +138 -0
- package/src/default_config.json +57 -0
- package/src/global.d.ts +8 -0
- package/src/index.js +6 -0
- package/src/index.scss +133 -0
- package/src/logging.js +7 -0
- package/src/renderHtmlTemplate.test.js +60 -0
- package/src/select-search.min.svg +1 -0
- package/src/select-search.svg +46 -0
- package/src/setupTests.js +5 -0
- package/src/utils/GenerateQueryString.js +200 -0
- package/src/utils/GenerateQueryString.test.js +304 -0
- package/src/utils/index.ts +3 -0
- package/standalone.config.js +5 -0
- package/static/map_tile_caosdb_logo.png +0 -0
- package/tsconfig.json +25 -0
- package/webpack.config.js +193 -0
|
Binary file
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// jest.config.js
|
|
2
|
+
module.exports = {
|
|
3
|
+
testEnvironment: "jsdom",
|
|
4
|
+
|
|
5
|
+
transform: {
|
|
6
|
+
"^.+\\.(t|j)sx?$": "babel-jest",
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
|
|
10
|
+
|
|
11
|
+
moduleNameMapper: {
|
|
12
|
+
"\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
|
|
13
|
+
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
|
|
14
|
+
"^.+\\.svg$": "jest-svg-transformer",
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Let Babel transform these ESM deps from node_modules
|
|
18
|
+
transformIgnorePatterns: [
|
|
19
|
+
"/node_modules/(?!(react-leaflet|@react-leaflet|leaflet)/)",
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
|
|
23
|
+
};
|
package/jest.setup.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@indiscale/linkahead-webui-ext-map",
|
|
3
|
+
"homepage": "webinterface/reactmap",
|
|
4
|
+
"version": "0.5.0",
|
|
5
|
+
"author": "Timm Fitschen",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"main": "dist/linkahead-webui-ext-map.js",
|
|
8
|
+
"src": "src/index.js",
|
|
9
|
+
"peerDependencies": {
|
|
10
|
+
"bootstrap": "^5.3.0",
|
|
11
|
+
"bootstrap-icons": "^1.10.3",
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@indiscale/linkahead-webui-core-components": "^0.4.3",
|
|
17
|
+
"@indiscale/linkahead-webui-entity-service": "^0.1.0",
|
|
18
|
+
"core-js": "^3.31.1",
|
|
19
|
+
"leaflet": "^1.9.3",
|
|
20
|
+
"leaflet-graticule": "^0.0.1",
|
|
21
|
+
"leaflet.coordinates": "^0.1.5",
|
|
22
|
+
"lodash": "^4.17.21",
|
|
23
|
+
"loglevel": "^1.8.1",
|
|
24
|
+
"proj4leaflet": "^1.0.2",
|
|
25
|
+
"react-leaflet": "^4.2.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@babel/core": "^7.28.5",
|
|
29
|
+
"@babel/eslint-parser": "^7.21.3",
|
|
30
|
+
"@babel/plugin-transform-modules-commonjs": "^7.21.2",
|
|
31
|
+
"@babel/preset-env": "^7.28.5",
|
|
32
|
+
"@babel/preset-react": "^7.28.5",
|
|
33
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
34
|
+
"@testing-library/cypress": "^10.1.0",
|
|
35
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
36
|
+
"@testing-library/react": "^14.0.0",
|
|
37
|
+
"@testing-library/user-event": "^14.4.3",
|
|
38
|
+
"@types/leaflet": "^1.9.1",
|
|
39
|
+
"@types/react": "^19.2.7",
|
|
40
|
+
"@types/react-dom": "^19.2.3",
|
|
41
|
+
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
|
42
|
+
"@typescript-eslint/parser": "^8.61.0",
|
|
43
|
+
"babel-jest": "^29.7.0",
|
|
44
|
+
"babel-loader": "^9.1.2",
|
|
45
|
+
"bootstrap": "^5.3.0",
|
|
46
|
+
"bootstrap-icons": "^1.10.3",
|
|
47
|
+
"css-loader": "^6.7.3",
|
|
48
|
+
"cypress": "^15.2.0",
|
|
49
|
+
"eslint": "^8.36.0",
|
|
50
|
+
"eslint-plugin-jest": "^29.15.2",
|
|
51
|
+
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
52
|
+
"eslint-plugin-react": "^7.32.2",
|
|
53
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
54
|
+
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
|
55
|
+
"html-webpack-plugin": "^5.5.0",
|
|
56
|
+
"identity-obj-proxy": "^3.0.0",
|
|
57
|
+
"jest": "^29.5.0",
|
|
58
|
+
"jest-environment-jsdom": "^29.5.0",
|
|
59
|
+
"jest-svg-transformer": "^1.0.0",
|
|
60
|
+
"jsdoc": "^4.0.2",
|
|
61
|
+
"prettier": "^2.8.4",
|
|
62
|
+
"process": "^0.11.10",
|
|
63
|
+
"react": "^18.2.0",
|
|
64
|
+
"react-dom": "^18.2.0",
|
|
65
|
+
"regenerator-runtime": "^0.13.11",
|
|
66
|
+
"sass": "^1.58.3",
|
|
67
|
+
"sass-loader": "^13.2.0",
|
|
68
|
+
"start-server-and-test": "^2.1.0",
|
|
69
|
+
"style-loader": "^3.3.1",
|
|
70
|
+
"svg-url-loader": "^8.0.0",
|
|
71
|
+
"typescript": "^5.9.3",
|
|
72
|
+
"webpack": "^5.75.0",
|
|
73
|
+
"webpack-cli": "^5.0.1",
|
|
74
|
+
"webpack-dev-server": "^5.2.4"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"lint": "eslint src/",
|
|
78
|
+
"format": "prettier -w src/",
|
|
79
|
+
"build": "webpack --mode production",
|
|
80
|
+
"build:standalone": "webpack --mode production --env standalone",
|
|
81
|
+
"build:dev:standalone": "webpack --mode development --env standalone",
|
|
82
|
+
"prepare": "rm -rf ./dist && npm run build",
|
|
83
|
+
"build:dev": "webpack --mode development",
|
|
84
|
+
"mock": "webpack serve --mode development --env include-peer-deps",
|
|
85
|
+
"mock:standalone": "webpack serve --mode production --env standalone",
|
|
86
|
+
"mock:dev:standalone": "webpack serve --mode development --env standalone",
|
|
87
|
+
"test": "jest --env=jsdom",
|
|
88
|
+
"cypress": "cypress open",
|
|
89
|
+
"cypress:run": "cypress run",
|
|
90
|
+
"cypress:cicd": "start-server-and-test start \"$URL\" cypress:run",
|
|
91
|
+
"tsc": "tsc --noEmit"
|
|
92
|
+
},
|
|
93
|
+
"browserslist": {
|
|
94
|
+
"production": [
|
|
95
|
+
">0.2%",
|
|
96
|
+
"not dead",
|
|
97
|
+
"not op_mini all"
|
|
98
|
+
],
|
|
99
|
+
"development": [
|
|
100
|
+
"last 1 chrome version",
|
|
101
|
+
"last 1 firefox version",
|
|
102
|
+
"last 1 safari version"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
8
|
+
<div id="caosdb-f-map-panel"></div>
|
|
9
|
+
<div id="caosdb-f-toggle-map-button"></div>
|
|
10
|
+
</body>
|
|
11
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"short_name": "React App",
|
|
3
|
+
"name": "Create React App Sample",
|
|
4
|
+
"icons": [
|
|
5
|
+
{
|
|
6
|
+
"src": "favicon.ico",
|
|
7
|
+
"sizes": "64x64 32x32 24x24 16x16",
|
|
8
|
+
"type": "image/x-icon"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"src": "logo192.png",
|
|
12
|
+
"type": "image/png",
|
|
13
|
+
"sizes": "192x192"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"src": "logo512.png",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"sizes": "512x512"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"start_url": ".",
|
|
22
|
+
"display": "standalone",
|
|
23
|
+
"theme_color": "#000000",
|
|
24
|
+
"background_color": "#ffffff"
|
|
25
|
+
}
|
|
Binary file
|
package/public/mock.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import "bootstrap/scss/bootstrap.scss";
|
|
4
|
+
import "bootstrap-icons/font/bootstrap-icons.css";
|
|
5
|
+
import "../src/index.scss";
|
|
6
|
+
import "regenerator-runtime/runtime";
|
|
7
|
+
import { Map } from "../src/Map";
|
|
8
|
+
import { ToggleMapButton } from "../src/components/ToggleMapButton";
|
|
9
|
+
import { standaloneConfig } from "../standalone.config";
|
|
10
|
+
|
|
11
|
+
window.getEntities = () => [];
|
|
12
|
+
|
|
13
|
+
// Custom config of map components in standalone mode.
|
|
14
|
+
// using standalone.config.js and run using npm run mock:standalone
|
|
15
|
+
const standaloneConfigObj =
|
|
16
|
+
process.env.STANDALONE_MODE === true ? standaloneConfig : {};
|
|
17
|
+
|
|
18
|
+
const caosdb_webui_ext_map = {
|
|
19
|
+
init: (container, toggle_button) => {
|
|
20
|
+
const root = ReactDOM.createRoot(container);
|
|
21
|
+
root.render(
|
|
22
|
+
<React.StrictMode>
|
|
23
|
+
<Map standaloneConfig={standaloneConfigObj} />
|
|
24
|
+
</React.StrictMode>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const root2 = ReactDOM.createRoot(toggle_button);
|
|
28
|
+
// If standalone mode, show map without toggle button
|
|
29
|
+
!process.env.STANDALONE_MODE === true &&
|
|
30
|
+
root2.render(
|
|
31
|
+
<React.StrictMode>
|
|
32
|
+
<ToggleMapButton mapContainer="#caosdb-f-map-panel" />
|
|
33
|
+
</React.StrictMode>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
caosdb_webui_ext_map.init(
|
|
39
|
+
document.getElementById("caosdb-f-map-panel"),
|
|
40
|
+
document.getElementById("caosdb-f-toggle-map-button")
|
|
41
|
+
);
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { useEffect, useState, type FC } from "react";
|
|
2
|
+
import { logger } from "./logging";
|
|
3
|
+
import { LayerGroup, LayersControl } from "react-leaflet";
|
|
4
|
+
import { useConfig, type TIframeSettings } from "./context/ConfigProvider";
|
|
5
|
+
import { usePathId } from "./context/PathIdProvider";
|
|
6
|
+
import { useLoading } from "./context/LoadingProvider";
|
|
7
|
+
import {
|
|
8
|
+
get_select_results,
|
|
9
|
+
get_transaction_service,
|
|
10
|
+
EntityMarkers,
|
|
11
|
+
get_selector,
|
|
12
|
+
make_layer_chooser_html,
|
|
13
|
+
get_select_with_path,
|
|
14
|
+
} from "./Map.helpers";
|
|
15
|
+
import { ErrorComponent } from "./components/ErrorComponent";
|
|
16
|
+
import { generateQueryString } from "./utils/GenerateQueryString";
|
|
17
|
+
import { isStandaloneMode } from "./utils";
|
|
18
|
+
|
|
19
|
+
type TAllMapEntitiesProps = {
|
|
20
|
+
hideLayerSwitch?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type TEntity = {
|
|
24
|
+
parents?: unknown;
|
|
25
|
+
id: string | number;
|
|
26
|
+
name?: string;
|
|
27
|
+
lat?: number;
|
|
28
|
+
lng?: number;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type TCurrentPath = string[];
|
|
33
|
+
|
|
34
|
+
type TControlOptions = {
|
|
35
|
+
name: string;
|
|
36
|
+
checked: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the SELECT query for "all map entities".
|
|
41
|
+
* - If a path is selected: use helper that builds a path-based query.
|
|
42
|
+
* - Otherwise: build a generic query using the datamodel fields.
|
|
43
|
+
*/
|
|
44
|
+
function _make_all_map_entities_query(datamodel: any, path: string[]) {
|
|
45
|
+
if (path?.length) {
|
|
46
|
+
// NOTE:
|
|
47
|
+
// get_select_with_path is currently untyped in Map.helpers,
|
|
48
|
+
// so TS can't validate the signature yet.
|
|
49
|
+
// @ts-expect-error function is not typed yet
|
|
50
|
+
return get_select_with_path(datamodel, path);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let ret = `SELECT id,name,parent,${datamodel.lat},${datamodel.lng} FROM ${datamodel.role}`;
|
|
54
|
+
if (datamodel.entity) {
|
|
55
|
+
ret += ` "${datamodel.entity}"`;
|
|
56
|
+
}
|
|
57
|
+
ret += ` WITH "${datamodel.lat}" AND "${datamodel.lng}"`;
|
|
58
|
+
return ret;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fetch all displayable entities for the current map context.
|
|
63
|
+
* - Standalone mode uses a query string generated from iframeSettings.
|
|
64
|
+
* - Embedded mode uses the normal datamodel/path-based query.
|
|
65
|
+
*/
|
|
66
|
+
async function get_all_map_entities(
|
|
67
|
+
datamodel: any,
|
|
68
|
+
path: string[],
|
|
69
|
+
iframeSettings: TIframeSettings = {}
|
|
70
|
+
): Promise<TEntity[]> {
|
|
71
|
+
const service = get_transaction_service() as any;
|
|
72
|
+
|
|
73
|
+
const query = isStandaloneMode()
|
|
74
|
+
? (generateQueryString(iframeSettings as TIframeSettings) as string)
|
|
75
|
+
: _make_all_map_entities_query(datamodel, path);
|
|
76
|
+
|
|
77
|
+
logger.debug(`get_all_map_entities query: ${query}`, datamodel, path);
|
|
78
|
+
|
|
79
|
+
const response = await service.executeQuery(query);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const selectResult = response
|
|
83
|
+
?.getResponsesList?.()?.[0]
|
|
84
|
+
?.getRetrieveResponse?.()
|
|
85
|
+
?.getSelectResult?.();
|
|
86
|
+
|
|
87
|
+
const entitiesRes = (await get_select_results(selectResult)) as any[];
|
|
88
|
+
|
|
89
|
+
const entities: TEntity[] = entitiesRes.map((obj: any) => {
|
|
90
|
+
// Selector differs depending on standalone vs embedded mode:
|
|
91
|
+
// - Standalone: selector comes from config (iframeSettings)
|
|
92
|
+
// - Embedded: selector is derived from the current path
|
|
93
|
+
const selector = isStandaloneMode()
|
|
94
|
+
? iframeSettings?.selector || ""
|
|
95
|
+
: get_selector(path) ?? "";
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
parents: obj["parent"],
|
|
99
|
+
id: obj.id,
|
|
100
|
+
name: obj.name,
|
|
101
|
+
lat: obj[`${selector}${datamodel.lat}`],
|
|
102
|
+
lng: obj[`${selector}${datamodel.lng}`],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
logger.debug("get_all_map_entities entities: ", entities);
|
|
107
|
+
return entities;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logger.error(err, response);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* AllMapEntities
|
|
117
|
+
* -------------
|
|
118
|
+
* Renders the "All entities" overlay layer and fetches entity data when the
|
|
119
|
+
* *data-driving* inputs change (path/datamodel/iframeSettings).
|
|
120
|
+
*
|
|
121
|
+
* Key idea:
|
|
122
|
+
* - We store only raw entity data in state (entities: TEntity[]),
|
|
123
|
+
* NOT a React element, so presentation-only changes (like zIndexOffset or icon_options)
|
|
124
|
+
* don't automatically refetch data.
|
|
125
|
+
*/
|
|
126
|
+
export const AllMapEntities: FC<TAllMapEntitiesProps> = ({
|
|
127
|
+
hideLayerSwitch,
|
|
128
|
+
}) => {
|
|
129
|
+
const { startLoading, stopLoading } = useLoading() as {
|
|
130
|
+
startLoading: () => void;
|
|
131
|
+
stopLoading: () => void;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const { pathId } = usePathId() as { pathId: string };
|
|
135
|
+
|
|
136
|
+
// State holds only data + error
|
|
137
|
+
const [entities, setEntities] = useState<TEntity[] | null>(null);
|
|
138
|
+
const [error, setError] = useState<unknown>(null);
|
|
139
|
+
|
|
140
|
+
const { config } = useConfig();
|
|
141
|
+
|
|
142
|
+
const {
|
|
143
|
+
iframeSettings = {},
|
|
144
|
+
datamodel,
|
|
145
|
+
entityLayers: {
|
|
146
|
+
all_map_entities: {
|
|
147
|
+
zIndexOffset = 0,
|
|
148
|
+
icon_options,
|
|
149
|
+
name,
|
|
150
|
+
description,
|
|
151
|
+
active,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
select,
|
|
155
|
+
} = config;
|
|
156
|
+
|
|
157
|
+
const currentPath: TCurrentPath = select.paths[pathId] || [];
|
|
158
|
+
|
|
159
|
+
logger.trace(
|
|
160
|
+
"AllMapEntities",
|
|
161
|
+
currentPath,
|
|
162
|
+
zIndexOffset,
|
|
163
|
+
icon_options,
|
|
164
|
+
datamodel
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* We intentionally derive "stable keys" used by the effect dependencies.
|
|
169
|
+
*
|
|
170
|
+
* Why?
|
|
171
|
+
* - `currentPath`, `datamodel`, `iframeSettings` may be new object references
|
|
172
|
+
* on each render even when values are the same (depending on upstream code).
|
|
173
|
+
* - Depending on raw objects would trigger unnecessary refetches.
|
|
174
|
+
*
|
|
175
|
+
* Here, we reduce dependencies to *value-based* keys:
|
|
176
|
+
* - currentPathKey: stable string representation of the path
|
|
177
|
+
* - datamodelKey: stable string representation (only if datamodel is JSON-safe)
|
|
178
|
+
* - iFrameKey: stable string representation (same caveat)
|
|
179
|
+
*
|
|
180
|
+
* This makes the effect run only when the underlying values actually change.
|
|
181
|
+
*/
|
|
182
|
+
const currentPathKey = Array.isArray(currentPath)
|
|
183
|
+
? currentPath.join("/")
|
|
184
|
+
: "";
|
|
185
|
+
const iFrameKey = JSON.stringify(iframeSettings);
|
|
186
|
+
const datamodelKey = JSON.stringify(datamodel);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
/**
|
|
190
|
+
* Cancellation guard:
|
|
191
|
+
* If the component unmounts or the effect re-runs due to dependency changes
|
|
192
|
+
* while an async request is still in flight, we avoid calling setState on an
|
|
193
|
+
* unmounted component / stale effect instance.
|
|
194
|
+
*/
|
|
195
|
+
let cancelled = false;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Loading counter correctness:
|
|
199
|
+
* Our LoadingProvider uses an in-flight counter.
|
|
200
|
+
* We must call stopLoading() *exactly once* for each startLoading().
|
|
201
|
+
*
|
|
202
|
+
* We therefore track whether we already stopped to prevent:
|
|
203
|
+
* - double stop on .finally + cleanup
|
|
204
|
+
* - or missing stop when unmounting early
|
|
205
|
+
*/
|
|
206
|
+
let stopped = false;
|
|
207
|
+
|
|
208
|
+
// Mark "one async operation started"
|
|
209
|
+
startLoading();
|
|
210
|
+
|
|
211
|
+
// Reset state for this fetch cycle
|
|
212
|
+
// Which will also cause the current pins to dissapear from the map
|
|
213
|
+
setEntities(null);
|
|
214
|
+
setError(null);
|
|
215
|
+
|
|
216
|
+
get_all_map_entities(datamodel, currentPath, iframeSettings)
|
|
217
|
+
.then((resolved) => {
|
|
218
|
+
if (cancelled) return;
|
|
219
|
+
setEntities(resolved);
|
|
220
|
+
})
|
|
221
|
+
.catch((err) => {
|
|
222
|
+
if (cancelled) return;
|
|
223
|
+
logger.error("[AllMapEntities] error fetching entities", err);
|
|
224
|
+
setError(err || new Error("Unknown error"));
|
|
225
|
+
})
|
|
226
|
+
.finally(() => {
|
|
227
|
+
// Ensure we decrement the loading counter once
|
|
228
|
+
if (stopped) return;
|
|
229
|
+
stopped = true;
|
|
230
|
+
stopLoading();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Cleanup:
|
|
235
|
+
* Runs when:
|
|
236
|
+
* - component unmounts OR
|
|
237
|
+
* - dependencies change and effect re-runs
|
|
238
|
+
*
|
|
239
|
+
* We mark cancelled and ensure the loading counter is decremented once.
|
|
240
|
+
*/
|
|
241
|
+
return () => {
|
|
242
|
+
cancelled = true;
|
|
243
|
+
|
|
244
|
+
if (stopped) return;
|
|
245
|
+
stopped = true;
|
|
246
|
+
stopLoading();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Dependency rationale:
|
|
251
|
+
* We *do not* depend on the raw objects, but on their stable keys.
|
|
252
|
+
* This avoids refetching due to referential changes only.
|
|
253
|
+
*
|
|
254
|
+
* We keep startLoading/stopLoading in deps because they are callbacks
|
|
255
|
+
* from context (and should be stable anyway, but including them is correct).
|
|
256
|
+
*
|
|
257
|
+
* NOTE:
|
|
258
|
+
* react-hooks/exhaustive-deps will suggest adding currentPath/datamodel/iframeSettings.
|
|
259
|
+
* We intentionally do not, because that would defeat the key-based approach.
|
|
260
|
+
*/
|
|
261
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
262
|
+
}, [currentPathKey, datamodelKey, iFrameKey, startLoading, stopLoading]);
|
|
263
|
+
|
|
264
|
+
const isChecked =
|
|
265
|
+
Boolean(active) || (isStandaloneMode() && Boolean(hideLayerSwitch));
|
|
266
|
+
|
|
267
|
+
// Prefer building the object immutably instead of mutating after creation
|
|
268
|
+
const control_options: TControlOptions = {
|
|
269
|
+
name: make_layer_chooser_html(
|
|
270
|
+
icon_options.html,
|
|
271
|
+
name,
|
|
272
|
+
description
|
|
273
|
+
) as string,
|
|
274
|
+
checked: isChecked,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<LayersControl.Overlay {...control_options}>
|
|
279
|
+
<ErrorComponent error={error} />
|
|
280
|
+
{entities && (
|
|
281
|
+
<LayerGroup>
|
|
282
|
+
<EntityMarkers
|
|
283
|
+
path={currentPath}
|
|
284
|
+
entities={entities}
|
|
285
|
+
zIndexOffset={zIndexOffset as number}
|
|
286
|
+
icon_options={icon_options}
|
|
287
|
+
/>
|
|
288
|
+
</LayerGroup>
|
|
289
|
+
)}
|
|
290
|
+
</LayersControl.Overlay>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export default AllMapEntities;
|