@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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { logger } from "./logging";
|
|
3
|
+
import { LayerGroup, LayersControl } from "react-leaflet";
|
|
4
|
+
import { useConfig } from "./context/ConfigProvider";
|
|
5
|
+
import { usePathId } from "./context/PathIdProvider";
|
|
6
|
+
import { useLoading } from "./context/LoadingProvider";
|
|
7
|
+
import {
|
|
8
|
+
get_select_results,
|
|
9
|
+
get_selector,
|
|
10
|
+
get_transaction_service,
|
|
11
|
+
get_select_with_path,
|
|
12
|
+
EntityMarkers,
|
|
13
|
+
make_layer_chooser_html,
|
|
14
|
+
} from "./Map.helpers";
|
|
15
|
+
import { ErrorComponent } from "./components/ErrorComponent";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ---------------------------------------------------------------------------
|
|
19
|
+
* Legacy adapter functions
|
|
20
|
+
* ---------------------------------------------------------------------------
|
|
21
|
+
*
|
|
22
|
+
* These functions adapt the current component to the legacy web interface.
|
|
23
|
+
* They rely on globally injected functions (window.getEntityName, etc.).
|
|
24
|
+
*
|
|
25
|
+
* IMPORTANT:
|
|
26
|
+
* - This is legacy glue code.
|
|
27
|
+
* - The component itself does NOT own this logic.
|
|
28
|
+
* - Long-term this should move into a dedicated legacy adapter module.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function _getEntityName(entity) {
|
|
32
|
+
if (window.getEntityName) {
|
|
33
|
+
return window.getEntityName(entity);
|
|
34
|
+
}
|
|
35
|
+
throw new Error("no implementation for getEntityName");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _getProperty(entity, property) {
|
|
39
|
+
if (window.getProperty) {
|
|
40
|
+
return window.getProperty(entity, property);
|
|
41
|
+
}
|
|
42
|
+
throw new Error("no implementation for getProperty");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _getEntityID(entity) {
|
|
46
|
+
if (window.getEntityID) {
|
|
47
|
+
return window.getEntityID(entity);
|
|
48
|
+
}
|
|
49
|
+
throw new Error("no implementation for getEntityID");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _getParents(entity) {
|
|
53
|
+
if (window.getParents) {
|
|
54
|
+
return window.getParents(entity);
|
|
55
|
+
}
|
|
56
|
+
throw new Error("no implementation for getParents");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _getEntities() {
|
|
60
|
+
if (window.getEntities) {
|
|
61
|
+
return window.getEntities();
|
|
62
|
+
}
|
|
63
|
+
throw new Error("no implementation for getEntities");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return DOM elements representing entities that are displayable on the map.
|
|
68
|
+
*
|
|
69
|
+
* An entity is considered "map-displayable" if:
|
|
70
|
+
* - it contains BOTH latitude and longitude properties
|
|
71
|
+
*
|
|
72
|
+
* NOTE:
|
|
73
|
+
* This function is purely DOM-based and only used in legacy mode.
|
|
74
|
+
*/
|
|
75
|
+
function get_map_entities(container, datamodel) {
|
|
76
|
+
if (!container) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const map_entities = [
|
|
81
|
+
...container.getElementsByClassName("caosdb-entity-panel"),
|
|
82
|
+
].filter((entity) => {
|
|
83
|
+
let found = 0;
|
|
84
|
+
|
|
85
|
+
for (let e of entity.getElementsByClassName("caosdb-property-name")) {
|
|
86
|
+
const txt = e.textContent.trim();
|
|
87
|
+
if (txt === datamodel.lat) found += 1;
|
|
88
|
+
else if (txt === datamodel.lng) found += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return found === 2;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return map_entities;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fetch entities for the "current page" layer.
|
|
99
|
+
*
|
|
100
|
+
* Two distinct execution paths:
|
|
101
|
+
*
|
|
102
|
+
* 1) Path-based mode (modern / service-backed):
|
|
103
|
+
* - A path is selected
|
|
104
|
+
* - Entity IDs are taken from the legacy DOM
|
|
105
|
+
* - A SELECT query is executed via transaction service
|
|
106
|
+
*
|
|
107
|
+
* 2) DOM-only mode (legacy fallback):
|
|
108
|
+
* - No path selected
|
|
109
|
+
* - Entities are parsed directly from the page DOM
|
|
110
|
+
*/
|
|
111
|
+
async function get_current_page_entities(datamodel, path) {
|
|
112
|
+
logger.trace("get_current_page_entities", datamodel, path);
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// Path-based mode: fetch entity data from backend
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
if (path && path.length) {
|
|
118
|
+
const ids = [];
|
|
119
|
+
|
|
120
|
+
// Collect entity IDs from legacy DOM
|
|
121
|
+
for (let rec of _getEntities()) {
|
|
122
|
+
ids.push(_getEntityID(rec));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!ids.length) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const service = get_transaction_service();
|
|
130
|
+
|
|
131
|
+
// Build SELECT query including path and explicit IDs
|
|
132
|
+
const query = get_select_with_path(datamodel, path, ids);
|
|
133
|
+
logger.trace(`get_current_page_entities query: ${query}`);
|
|
134
|
+
|
|
135
|
+
const response = await service.executeQuery(query);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const selectResult = response
|
|
139
|
+
.getResponsesList()[0]
|
|
140
|
+
.getRetrieveResponse()
|
|
141
|
+
.getSelectResult();
|
|
142
|
+
|
|
143
|
+
const entities = get_select_results(selectResult).map((obj) => {
|
|
144
|
+
const selector = get_selector(path);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
parents: obj["parent"],
|
|
148
|
+
id: obj.id,
|
|
149
|
+
name: obj.name,
|
|
150
|
+
lat: obj[`${selector}${datamodel.lat}`],
|
|
151
|
+
lng: obj[`${selector}${datamodel.lng}`],
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
logger.trace("get_current_page_entities: ", entities);
|
|
156
|
+
return entities;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
logger.error(err, response);
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// -------------------------------------------------------------------------
|
|
164
|
+
// Legacy DOM-only mode (no path)
|
|
165
|
+
// -------------------------------------------------------------------------
|
|
166
|
+
const container = document.getElementsByClassName(
|
|
167
|
+
"caosdb-f-main-entities"
|
|
168
|
+
)[0];
|
|
169
|
+
|
|
170
|
+
return get_map_entities(container, datamodel).map((map_entity) => {
|
|
171
|
+
return {
|
|
172
|
+
name: _getEntityName(map_entity),
|
|
173
|
+
id: _getEntityID(map_entity),
|
|
174
|
+
parents: _getParents(map_entity).map((par) => par.name),
|
|
175
|
+
lat: _getProperty(map_entity, datamodel.lat),
|
|
176
|
+
lng: _getProperty(map_entity, datamodel.lng),
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* ===========================================================================
|
|
183
|
+
* CurrentPageEntities Component
|
|
184
|
+
* ===========================================================================
|
|
185
|
+
*
|
|
186
|
+
* Architectural principles (same as AllMapEntities):
|
|
187
|
+
*
|
|
188
|
+
* - State holds ONLY raw data (entities + error)
|
|
189
|
+
* - Presentation-only changes (icons, zIndex, visibility) do NOT refetch data
|
|
190
|
+
* - Data fetching is driven by stable, value-based keys
|
|
191
|
+
* - Loading counter is handled carefully to avoid double start/stop
|
|
192
|
+
*/
|
|
193
|
+
export function CurrentPageEntities() {
|
|
194
|
+
const { startLoading, stopLoading } = useLoading();
|
|
195
|
+
const { pathId } = usePathId();
|
|
196
|
+
|
|
197
|
+
const {
|
|
198
|
+
config: {
|
|
199
|
+
datamodel,
|
|
200
|
+
entityLayers: {
|
|
201
|
+
current_page_entities: {
|
|
202
|
+
zIndexOffset = 0,
|
|
203
|
+
icon_options,
|
|
204
|
+
name,
|
|
205
|
+
description,
|
|
206
|
+
active,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
select,
|
|
210
|
+
},
|
|
211
|
+
} = useConfig();
|
|
212
|
+
|
|
213
|
+
const currentPath = select.paths[pathId] || [];
|
|
214
|
+
|
|
215
|
+
// State stores DATA only (not React nodes)
|
|
216
|
+
const [entities, setEntities] = useState(null);
|
|
217
|
+
const [error, setError] = useState(null);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Stable keys for effect dependencies.
|
|
221
|
+
*
|
|
222
|
+
* Why?
|
|
223
|
+
* - currentPath and datamodel may be new object references
|
|
224
|
+
* even when their values didn't actually change.
|
|
225
|
+
* - Using value-based keys avoids unnecessary refetches.
|
|
226
|
+
*/
|
|
227
|
+
const currentPathKey = Array.isArray(currentPath)
|
|
228
|
+
? currentPath.join("/")
|
|
229
|
+
: "";
|
|
230
|
+
const datamodelKey = JSON.stringify(datamodel);
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
/**
|
|
234
|
+
* Cancellation guard:
|
|
235
|
+
* Prevents state updates after unmount or effect re-run.
|
|
236
|
+
*/
|
|
237
|
+
let cancelled = false;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Loading counter guard:
|
|
241
|
+
* Ensures stopLoading() is called exactly once.
|
|
242
|
+
*/
|
|
243
|
+
let stopped = false;
|
|
244
|
+
|
|
245
|
+
// Signal: one async operation started
|
|
246
|
+
startLoading();
|
|
247
|
+
|
|
248
|
+
// Reset state for this fetch cycle
|
|
249
|
+
// This also removes existing markers from the map
|
|
250
|
+
setEntities(null);
|
|
251
|
+
setError(null);
|
|
252
|
+
|
|
253
|
+
Promise.resolve()
|
|
254
|
+
.then(() => get_current_page_entities(datamodel, currentPath))
|
|
255
|
+
.then((resolvedEntities) => {
|
|
256
|
+
if (cancelled) return;
|
|
257
|
+
setEntities(resolvedEntities);
|
|
258
|
+
})
|
|
259
|
+
.catch((err) => {
|
|
260
|
+
if (cancelled) return;
|
|
261
|
+
|
|
262
|
+
const _error = err && err.message ? err.message : String(err);
|
|
263
|
+
logger.error("[CurrentPageEntities] error fetching entities", _error);
|
|
264
|
+
setError(_error);
|
|
265
|
+
})
|
|
266
|
+
.finally(() => {
|
|
267
|
+
if (stopped) return;
|
|
268
|
+
stopped = true;
|
|
269
|
+
stopLoading();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Cleanup:
|
|
274
|
+
* Runs on unmount OR before effect re-runs.
|
|
275
|
+
* Ensures loading counter is decremented exactly once.
|
|
276
|
+
*/
|
|
277
|
+
return () => {
|
|
278
|
+
cancelled = true;
|
|
279
|
+
|
|
280
|
+
if (stopped) return;
|
|
281
|
+
stopped = true;
|
|
282
|
+
stopLoading();
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* IMPORTANT:
|
|
287
|
+
* We intentionally depend on value-based keys, NOT raw objects.
|
|
288
|
+
*
|
|
289
|
+
* react-hooks/exhaustive-deps would suggest adding:
|
|
290
|
+
* - currentPath
|
|
291
|
+
* - datamodel
|
|
292
|
+
*
|
|
293
|
+
* That would defeat the key-based approach and cause refetch storms.
|
|
294
|
+
*/
|
|
295
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
296
|
+
}, [currentPathKey, datamodelKey]);
|
|
297
|
+
|
|
298
|
+
const control_options = {
|
|
299
|
+
name: make_layer_chooser_html(icon_options.html, name, description),
|
|
300
|
+
checked: !!active,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<LayersControl.Overlay {...control_options}>
|
|
305
|
+
<ErrorComponent error={error} />
|
|
306
|
+
{entities && (
|
|
307
|
+
<LayerGroup>
|
|
308
|
+
<EntityMarkers
|
|
309
|
+
path={currentPath}
|
|
310
|
+
entities={entities}
|
|
311
|
+
zIndexOffset={zIndexOffset}
|
|
312
|
+
icon_options={icon_options}
|
|
313
|
+
/>
|
|
314
|
+
</LayerGroup>
|
|
315
|
+
)}
|
|
316
|
+
</LayersControl.Overlay>
|
|
317
|
+
);
|
|
318
|
+
}
|