@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.
Files changed (76) hide show
  1. package/.eslintrc.json +45 -0
  2. package/.gitlab-ci.yml +44 -0
  3. package/CHANGELOG.md +78 -0
  4. package/README.md +97 -0
  5. package/RELEASE_GUIDELINES.md +45 -0
  6. package/__mocks__/fileMock.js +3 -0
  7. package/__mocks__/styleMock.js +1 -0
  8. package/babel.config.js +22 -0
  9. package/cypress/e2e/standalone-map.cy.js +55 -0
  10. package/cypress/support/commands.js +25 -0
  11. package/cypress/support/e2e.js +17 -0
  12. package/cypress.config.js +10 -0
  13. package/dist/2b3e1faf89f94a483539.png +0 -0
  14. package/dist/416d91365b44e4b4f477.png +0 -0
  15. package/dist/8f2c4d11474275fbc161.png +0 -0
  16. package/dist/index.html +1 -0
  17. package/dist/linkahead-webui-ext-map.js +3 -0
  18. package/dist/linkahead-webui-ext-map.js.LICENSE.txt +45 -0
  19. package/dist/linkahead-webui-ext-map.js.map +1 -0
  20. package/iframe/index.html +6 -0
  21. package/indiscale-linkahead-webui-ext-map-0.4.1.tgz +0 -0
  22. package/jest.config.js +23 -0
  23. package/jest.setup.js +2 -0
  24. package/package.json +105 -0
  25. package/public/favicon.ico +0 -0
  26. package/public/index.html +11 -0
  27. package/public/logo192.png +0 -0
  28. package/public/logo512.png +0 -0
  29. package/public/manifest.json +25 -0
  30. package/public/map_tile_caosdb_logo.png +0 -0
  31. package/public/mock.js +41 -0
  32. package/public/robots.txt +3 -0
  33. package/select_query.json +3 -0
  34. package/src/AllMapEntities.tsx +294 -0
  35. package/src/CurrentPageEntities.js +318 -0
  36. package/src/Map.helpers.css +8 -0
  37. package/src/Map.helpers.js +536 -0
  38. package/src/Map.js +288 -0
  39. package/src/Map.test.js +252 -0
  40. package/src/MapConfig.js +75 -0
  41. package/src/__snapshots__/Map.test.js.snap +1725 -0
  42. package/src/components/Coordinates.js +24 -0
  43. package/src/components/ErrorComponent.tsx +2 -0
  44. package/src/components/Graticule.js +27 -0
  45. package/src/components/Loader.module.css +17 -0
  46. package/src/components/Loader.tsx +36 -0
  47. package/src/components/PathDropDown.js +108 -0
  48. package/src/components/SearchControl.js +502 -0
  49. package/src/components/ToggleMapButton.js +194 -0
  50. package/src/components/ViewChangeControl.js +104 -0
  51. package/src/constants/index.js +1 -0
  52. package/src/context/ConfigProvider.test.js +232 -0
  53. package/src/context/ConfigProvider.tsx +189 -0
  54. package/src/context/LoadingProvider.test.js +124 -0
  55. package/src/context/LoadingProvider.tsx +117 -0
  56. package/src/context/PathIdProvider.js +102 -0
  57. package/src/contrib/latlnggraticule/LICENSE +20 -0
  58. package/src/contrib/latlnggraticule/README.md +68 -0
  59. package/src/contrib/latlnggraticule/leaflet.latlng-graticule.js +528 -0
  60. package/src/contrib/simplegraticule/L.Graticule.js +138 -0
  61. package/src/default_config.json +57 -0
  62. package/src/global.d.ts +8 -0
  63. package/src/index.js +6 -0
  64. package/src/index.scss +133 -0
  65. package/src/logging.js +7 -0
  66. package/src/renderHtmlTemplate.test.js +60 -0
  67. package/src/select-search.min.svg +1 -0
  68. package/src/select-search.svg +46 -0
  69. package/src/setupTests.js +5 -0
  70. package/src/utils/GenerateQueryString.js +200 -0
  71. package/src/utils/GenerateQueryString.test.js +304 -0
  72. package/src/utils/index.ts +3 -0
  73. package/standalone.config.js +5 -0
  74. package/static/map_tile_caosdb_logo.png +0 -0
  75. package/tsconfig.json +25 -0
  76. package/webpack.config.js +193 -0
@@ -0,0 +1,502 @@
1
+ import PropTypes from "prop-types";
2
+ import { logger } from "../logging";
3
+ import L from "leaflet";
4
+ import { useMap } from "react-leaflet";
5
+ import { get_with_POV } from "../Map.helpers";
6
+ import { useConfig } from "../context/ConfigProvider";
7
+ import SelectSearchIcon from "../select-search.min.svg";
8
+
9
+ /**
10
+ * Plug-in for leaflet which lets the user select an area in the map
11
+ * and execute a query using a latitude/longitude filter for entities.
12
+ *
13
+ * This handler adds a select button as a control to the map. The
14
+ * select button toggles and indicates the select mode (on/off).
15
+ *
16
+ * The selection can be started by either clicking on the map when the
17
+ * select mode is on (after enabling it via the select button).
18
+ *
19
+ * The query button apears as soon as an selection has being finished.
20
+ *
21
+ * The query button generates a query from for searching inside the
22
+ * selected area and calls the `queryCallback` function.
23
+ */
24
+ const select_handler = {
25
+ /**
26
+ * Initialize the handler after it has been added to the map.
27
+ *
28
+ * This function is called by the map after the handler has been
29
+ * added via {@link L.Map.addHandler} or the handler added itself
30
+ * to the map via {@link L.Handler.addTo}.
31
+ *
32
+ * This method
33
+ * 1) Adds the select button.
34
+ * 2) Adds a listener for the `mousedown` event.
35
+ *
36
+ * Both are means to start the process of selecting an area in the
37
+ * map.
38
+ */
39
+ addHooks: function () {
40
+ const select_button = this._get_select_button((event) => {
41
+ logger.trace("select button clicked", this);
42
+ event.preventDefault();
43
+ event.stopPropagation();
44
+ this._toggle_select_mode();
45
+ });
46
+ this._select_button = select_button;
47
+ this._map.addControl(select_button);
48
+ this._map.on("mousedown", this._mousedown_listener);
49
+ },
50
+
51
+ /**
52
+ * Clean up after the select handler has been removed from the map.
53
+ *
54
+ * This method removes the select button and the `mousedown`
55
+ * listener.
56
+ */
57
+ removeHooks: function () {
58
+ this._select_button.remove();
59
+ this._map.off("mousedown", this._mousedown_listener);
60
+ },
61
+
62
+ /**
63
+ * Change the color of the select button in order to highlight it.
64
+ *
65
+ * This is used to indicate that the select mode is on and that the
66
+ * user can select something by clicking and moving the mouse on
67
+ * the map.
68
+ */
69
+ _highlight_select_button: function () {
70
+ this._select_button.button.classList.add("highlight");
71
+ },
72
+
73
+ /**
74
+ * Change the color of the select button back to normal.
75
+ */
76
+ _unhighlight_select_button: function () {
77
+ this._select_button.button.classList.remove("highlight");
78
+ },
79
+
80
+ /**
81
+ * Toggle the select mode (on/off).
82
+ *
83
+ * This includes setting the _select_mode_on to true/false,
84
+ * highlighting/unhighlighting the select button and
85
+ * disabling/enabling the moving of the map center by dragging.
86
+ */
87
+ _toggle_select_mode: function () {
88
+ logger.trace("toggle select mode", this);
89
+ if (this._select_mode_on) {
90
+ this._unhighlight_select_button();
91
+ this._map.dragging.enable();
92
+ this._reset_selection();
93
+ this._select_mode_on = false;
94
+ } else {
95
+ this._highlight_select_button();
96
+ this._map.dragging.disable();
97
+ this._select_mode_on = true;
98
+ }
99
+ },
100
+
101
+ /**
102
+ * Return a button for toggling and indicating the select mode.
103
+ *
104
+ * The select button shows a litte dashed square as its icon.
105
+ *
106
+ * @param {function} callback - a callback which toggles the select
107
+ * mode.
108
+ * @returns {L.Control} the select button.
109
+ */
110
+ _get_select_button: function (callback) {
111
+ // TODO flatten the structure of the code and possibly merge it with the query_button code.
112
+ var select_button = L.Control.extend({
113
+ options: {
114
+ position: "topleft",
115
+ },
116
+
117
+ onAdd: function () {
118
+ return this.button;
119
+ },
120
+
121
+ button: (function () {
122
+ var button = L.DomUtil.create(
123
+ "div",
124
+ "leaflet-bar leaflet-control leaflet-control-custom caosdb-f-map-select-search-btn"
125
+ );
126
+ button.title = "Select and area and search.";
127
+ button.addEventListener("click", callback);
128
+
129
+ const icon = L.DomUtil.create("img", "");
130
+ icon.src = SelectSearchIcon;
131
+ button.appendChild(icon);
132
+
133
+ button.onmousedown = (event) => {
134
+ event.stopPropagation();
135
+ };
136
+ button.onmouseup = (event) => {
137
+ event.stopPropagation();
138
+ };
139
+ return button;
140
+ })(),
141
+ });
142
+ return new select_button();
143
+ },
144
+
145
+ /**
146
+ * Return a button for opening the query panel with a pre-filled query.
147
+ *
148
+ * The query button has a loupe icon.
149
+ *
150
+ * The query button is added after an area has been selected and
151
+ * only visible as long an area is selected.
152
+ *
153
+ * @param {function} callback - a callback for opening the query
154
+ * panel and fill in the query.
155
+ * @returns {L.Control} the query button.
156
+ */
157
+ _get_query_button: function (callback) {
158
+ var query_button = L.Control.extend({
159
+ options: {
160
+ position: "topleft",
161
+ },
162
+
163
+ onAdd: function () {
164
+ return this.button;
165
+ },
166
+
167
+ button: (function () {
168
+ var button = L.DomUtil.create(
169
+ "div",
170
+ "leaflet-bar leaflet-control leaflet-control-custom caosdb-f-map-search-btn"
171
+ );
172
+ button.title = "Search within this area";
173
+ button.innerHTML = '<i class="bi-search"></i>';
174
+ button.onclick = callback;
175
+
176
+ button.onmousedown = (event) => {
177
+ event.stopPropagation();
178
+ };
179
+ button.onmouseup = (event) => {
180
+ event.stopPropagation();
181
+ };
182
+ return button;
183
+ })(),
184
+ });
185
+ return new query_button();
186
+ },
187
+
188
+ /**
189
+ * Listens on the mousedown event of the map and calls the
190
+ * _startSelect method if either (1) the select mode is on or (2)
191
+ * the shift key is pressed during the click on the map.
192
+ */
193
+ _mousedown_listener: function (event) {
194
+ logger.trace("mousedown", event, "on", this);
195
+ if (!event.originalEvent.shiftKey && !this.select._select_mode_on) {
196
+ return;
197
+ }
198
+ event.originalEvent.preventDefault();
199
+ event.originalEvent.stopPropagation();
200
+ this.select._startSelect(event.latlng);
201
+ },
202
+
203
+ /**
204
+ * Remove a pre-existing selection and start the process of a new
205
+ * selection.
206
+ *
207
+ * When the user clicks on the map with shift key pressed or
208
+ * _select_mode this method is called with the coordinates of the
209
+ * click.
210
+ *
211
+ * This also adds listeners on `mousemove` events (for redrawing
212
+ * the selected area) and on `mouseup` events for finishing the
213
+ * process of selection.
214
+ *
215
+ * @param {L.LatLng} start_point - the coordinates where the
216
+ * selection begins.
217
+ */
218
+ _startSelect: function (start_point) {
219
+ this._reset_selection();
220
+ this._point1 = start_point;
221
+ logger.trace("point1", this._point1);
222
+
223
+ this._map.on("mousemove", this._drawRect);
224
+ this._map.on("mouseup", this._endSelect);
225
+ },
226
+
227
+ /**
228
+ * If present, remove the selected area and the query button from
229
+ * the map.
230
+ */
231
+ _reset_selection: function () {
232
+ this._point1 = undefined;
233
+ if (this._rectangle) {
234
+ this._rectangle.remove();
235
+ }
236
+ this._remove_query_button();
237
+ },
238
+
239
+ /**
240
+ * (Re-)draw the rectangle which indicates the currently selected
241
+ * area.
242
+ *
243
+ * This method is added as a listener on the `mousemove` event by
244
+ * the _startSelect method.
245
+ */
246
+ _drawRect: function (event) {
247
+ logger.trace("mousemove", event, "on", this);
248
+ event.originalEvent.preventDefault();
249
+ event.originalEvent.stopPropagation();
250
+
251
+ // remove old rectangle
252
+ if (this.select._rectangle) {
253
+ this.select._rectangle.remove();
254
+ }
255
+
256
+ // draw new rectangle
257
+ const point2 = event.latlng;
258
+ const area = this.select._get_area(this.select._point1, point2);
259
+ this.select._rectangle = this.select._get_select_rectangle(area);
260
+ this.select._rectangle.addTo(this);
261
+ },
262
+
263
+ /**
264
+ * Return a colored rectangle covering an area.
265
+ *
266
+ * @param {L.LatLngBounds} area.
267
+ * @returns {L.Rectangle} the colored rectangle.
268
+ */
269
+ _get_select_rectangle: function (area) {
270
+ return L.rectangle(area, {
271
+ color: "#ff7800",
272
+ weight: 1,
273
+ });
274
+ },
275
+
276
+ /**
277
+ * Finish the process of selection, add the query button to the map.
278
+ *
279
+ * This method is a listener on the `mouseup` event and is added by
280
+ * the _startSelect method.
281
+ *
282
+ * It removes itself as a listener and also the _drawRect listener
283
+ * on the `mousemove` event.
284
+ */
285
+ _endSelect: function (event) {
286
+ logger.trace("mouseup", event, "on", this);
287
+ event.originalEvent.preventDefault();
288
+ event.originalEvent.stopPropagation();
289
+ this.off("mouseup", this.select._endSelect);
290
+ this.off("mousemove", this.select._drawRect);
291
+
292
+ const point2 = event.latlng;
293
+ const point1 = this.select._point1;
294
+ logger.trace("point1", point1);
295
+ logger.trace("point2", point2);
296
+
297
+ this.select._point1 = undefined;
298
+ if (point2.lat === point1.lat && point2.lng === point1.lng) {
299
+ return;
300
+ }
301
+ const area = this.select._get_area(point1, point2);
302
+ this.select._add_query_button(area);
303
+ },
304
+
305
+ /**
306
+ * Add a `query` button to the map (showing a loupe icon) which
307
+ * opens the query panel with a pre-filled query.
308
+ *
309
+ * The generated query searches for entities inside the selected
310
+ * area `a`.
311
+ *
312
+ * A pre-existing query button is removed and the new query button
313
+ * is stored into this._query_button for later references.
314
+ *
315
+ * @param {L.LatLngBounds} a - the selected area.
316
+ * @return {L.Control} the new query button.
317
+ */
318
+ _add_query_button: function (a) {
319
+ logger.trace("_add_query_button", a, this);
320
+
321
+ // remove older query button
322
+ this._remove_query_button();
323
+
324
+ const north = this._round(a.getNorth());
325
+ const south = this._round(a.getSouth());
326
+ const east = this._round(a.getEast());
327
+ const west = this._round(a.getWest());
328
+ const query = this.generate_query_from_bounds(north, south, west, east);
329
+
330
+ // generate a call-back which opens the query panel with the
331
+ // generated query
332
+ const callback = (event) => {
333
+ logger.trace("click query_button", query, this);
334
+ event.stopPropagation();
335
+ this._query_callback(query);
336
+ };
337
+
338
+ this._query_button = this._get_query_button(callback);
339
+ this._map.addControl(this._query_button);
340
+ return this._query_button;
341
+ },
342
+
343
+ /**
344
+ * Remove a `query` button if present.
345
+ *
346
+ * @return {L.Control} the old query button if present, `undefined`
347
+ * otherwise.
348
+ */
349
+ _remove_query_button: function () {
350
+ const old = this._query_button;
351
+ if (old) {
352
+ old.remove();
353
+ this._query_button = undefined;
354
+ return old;
355
+ }
356
+ },
357
+
358
+ /**
359
+ * Return the area specified by two coordinates.
360
+ *
361
+ * The edges are parallel to the latitude and longitude of the two
362
+ * points.
363
+ *
364
+ * @param {L.LatLng} point1
365
+ * @param {L.LatLng} point2
366
+ * @returns {L.LatLngBounds} the area.
367
+ */
368
+ _get_area: function (point1, point2) {
369
+ return L.latLngBounds(point1, point2);
370
+ },
371
+
372
+ /**
373
+ * Round the double value to 3 decimal places.
374
+ *
375
+ * Note: This function is used to round map coordinates to
376
+ * meaningful values.
377
+ *
378
+ * @param {number} d - a double value
379
+ * @returns {number} a rounded double value.
380
+ */
381
+ _round: function (d) {
382
+ return Math.round(d * 1000) / 1000;
383
+ },
384
+
385
+ /**
386
+ * The first point of the selected area.
387
+ *
388
+ * It is stored by the _startSelect method is used by subsequent
389
+ * execution of the _drawRect method and eventually the _endSelect
390
+ * method.
391
+ */
392
+ _point1: undefined,
393
+
394
+ /**
395
+ * _select_mode_on indicates whether clicks on the map without
396
+ * pressing shift will start/end/reset selection.
397
+ */
398
+ _select_mode_on: false,
399
+
400
+ _datamodel: undefined,
401
+
402
+ /**
403
+ * Generate a query to search inside a map area bounded by maximum and
404
+ * minimum latitudes and longitudes.
405
+ *
406
+ * This function uses the configuration from {@link SelectConfig} for
407
+ * the role and entity, the configuration from {@link DataModelConfig}
408
+ * for the latitude and longitude properties, and constructs a query
409
+ * filter from the rectangular bounding box such that all entities with
410
+ * coordinates inside that area are returned.
411
+ *
412
+ * The area is specified as follows:
413
+ *
414
+ * <code>
415
+ * north
416
+ * +-----------------+
417
+ * | |
418
+ * | |
419
+ * west| |east
420
+ * | |
421
+ * | |
422
+ * +-----------------+
423
+ * south
424
+ * </code>
425
+ *
426
+ * The horizontal lines (-) mark the maximum and minimum latitude and
427
+ * the vertical lines (|) mark the maximum and minimum longitude of the
428
+ * area to be searched in.
429
+ *
430
+ * @param {number} north
431
+ * @param {number} south
432
+ * @param {number} west
433
+ * @param {number} east
434
+ * @return {string} a query string.
435
+ */
436
+ generate_query_from_bounds: function (north, south, west, east) {
437
+ const role = this._datamodel.role;
438
+ var entity = this._datamodel.entity;
439
+ const lat = this._datamodel.lat;
440
+ const lng = this._datamodel.lng;
441
+
442
+ let path = this._current_path;
443
+ if (path && path.length > 0 && entity == "") {
444
+ entity = path[0];
445
+ }
446
+
447
+ var additional_path = "";
448
+ if (path && path.length > 1) {
449
+ additional_path = get_with_POV(path.slice(1, path.length));
450
+ }
451
+
452
+ const query_filter =
453
+ " ( " +
454
+ lat +
455
+ " < '" +
456
+ north +
457
+ "' AND " +
458
+ lat +
459
+ " > '" +
460
+ south +
461
+ "' AND " +
462
+ lng +
463
+ " > '" +
464
+ west +
465
+ "' AND " +
466
+ lng +
467
+ " < '" +
468
+ east +
469
+ "' ) ";
470
+
471
+ const query =
472
+ "FIND " + role + " " + entity + additional_path + " WITH " + query_filter;
473
+ return query;
474
+ },
475
+
476
+ _query_callback: (query) => {
477
+ logger.error("Unconfigured queryCallback", query);
478
+ },
479
+ };
480
+
481
+ export function SearchControl({ currentPath, queryCallback }) {
482
+ const {
483
+ config: { datamodel },
484
+ } = useConfig();
485
+ const map = useMap();
486
+
487
+ if (!map.select) {
488
+ map.addHandler("select", L.Handler.extend(select_handler));
489
+ map.select.enable();
490
+ }
491
+
492
+ map.select._datamodel = datamodel;
493
+ map.select._query_callback = queryCallback;
494
+ map.select._current_path = currentPath;
495
+
496
+ return null;
497
+ }
498
+
499
+ SearchControl.propTypes = {
500
+ currentPath: PropTypes.arrayOf(PropTypes.string),
501
+ queryCallback: PropTypes.func,
502
+ };
@@ -0,0 +1,194 @@
1
+ import React, { useState, useCallback, useEffect } from "react";
2
+ import PropTypes from "prop-types";
3
+ import { logger } from "../logging";
4
+ import { Await } from "@indiscale/linkahead-webui-core-components";
5
+ import { get_map_config, get_local_config } from "../MapConfig";
6
+
7
+ /**
8
+ * ON: map is visible, toggle button click will be processed
9
+ * OFF: map is not visible, toggle button click will be processed
10
+ * SWITCH_ON: map is about to be shown, clicks will be ignored
11
+ * SWITCH_OFF: map is about to be hidden, clicks will be ignored
12
+ */
13
+ const STATE = {
14
+ ON: "on",
15
+ OFF: "off",
16
+ SWITCH_ON: "switch_on",
17
+ SWITCH_OFF: "switch_off",
18
+ ERROR: "error",
19
+ };
20
+
21
+ const get_initial_state = (isOn) => {
22
+ if (typeof isOn === "undefined") {
23
+ isOn = get_local_config()["show"];
24
+ }
25
+ if (isOn) {
26
+ return STATE.SWITCH_ON;
27
+ }
28
+ return STATE.SWITCH_OFF;
29
+ };
30
+
31
+ const store_session_state = (state) => {
32
+ const isOn = state === STATE.ON;
33
+ sessionStorage.setItem("caosdb_map.show", JSON.stringify(isOn));
34
+ };
35
+
36
+ const toggle = (container, state, delay) => {
37
+ if (state !== STATE.SWITCH_OFF && state !== STATE.SWITCH_ON) {
38
+ throw new Error(`Illegal state transition from current state '${state}'`);
39
+ }
40
+
41
+ const containerRef = document.querySelector(container);
42
+ if (!containerRef) throw new Error(`Map container not found: ${container}.`);
43
+
44
+ return new Promise((resolve, reject) => {
45
+ try {
46
+ // TODO animate transition
47
+ let nextState = STATE.OFF;
48
+ if (state === STATE.SWITCH_ON) {
49
+ nextState = STATE.ON;
50
+ containerRef.classList.remove("d-none");
51
+ } else {
52
+ containerRef.classList.add("d-none");
53
+ }
54
+
55
+ containerRef.dispatchEvent(
56
+ new Event("caosdb-webui-ext-map.after-toggle")
57
+ );
58
+
59
+ setTimeout(() => {
60
+ resolve(nextState);
61
+ }, delay);
62
+ } catch (err) {
63
+ reject(err);
64
+ }
65
+ });
66
+ };
67
+
68
+ const _ToggleMapButton = ({
69
+ tag,
70
+ showInitial,
71
+ className,
72
+ mapContainer,
73
+ labelOn,
74
+ labelOff,
75
+ delay,
76
+ titleOn,
77
+ titleOff,
78
+ config = {},
79
+ }) => {
80
+ // We want that if show is present in the config, it overrides the
81
+ // showInitial prop and shows the map on load.
82
+ const [state, setState] = useState(
83
+ get_initial_state(config.show || showInitial)
84
+ );
85
+
86
+ const callback = useCallback(() => {
87
+ setState((prevState) => {
88
+ if (prevState === STATE.ON) {
89
+ logger.trace("setState", STATE.SWITCH_OFF);
90
+ store_session_state(STATE.OFF);
91
+ return STATE.SWITCH_OFF;
92
+ } else if (prevState === STATE.OFF) {
93
+ if (
94
+ !document.querySelector(mapContainer) ||
95
+ document.querySelector(mapContainer).children.length <= 1
96
+ ) {
97
+ // If the map container is not found or has only one child,
98
+ // we trigger a reload event to re-render the map as this means Leaflet is not loaded
99
+ logger.trace(`Map container not found or only one child.`);
100
+ document.dispatchEvent(new Event("map:reload"));
101
+ }
102
+ logger.trace("setState", STATE.SWITCH_ON);
103
+ store_session_state(STATE.ON);
104
+ return STATE.SWITCH_ON;
105
+ }
106
+ });
107
+ // else: no-op
108
+ }, [mapContainer]);
109
+
110
+ useEffect(() => {
111
+ if (state === STATE.SWITCH_ON || state === STATE.SWITCH_OFF) {
112
+ toggle(mapContainer, state, delay).then(
113
+ (nextState) => {
114
+ logger.trace("setState", nextState);
115
+ setState(nextState);
116
+ },
117
+ (err) => {
118
+ logger.error(err);
119
+ setState(STATE.ERROR);
120
+ }
121
+ );
122
+ }
123
+ // else: no-op
124
+ }, [state, setState, mapContainer, delay]);
125
+
126
+ if (state === STATE.ERROR) {
127
+ return <span>{"ERROR"}</span>;
128
+ }
129
+
130
+ const label =
131
+ state === STATE.ON || state === STATE.SWITCH_ON ? labelOn : labelOff;
132
+
133
+ const props = {
134
+ title: state === STATE.ON || state === STATE.SWITCH_ON ? titleOn : titleOff,
135
+ role: "button",
136
+ href: "#",
137
+ onClick: callback,
138
+ };
139
+ if (className) {
140
+ props["className"] = className;
141
+ }
142
+
143
+ const result = React.createElement(tag, props, <span>{label}</span>);
144
+ return result;
145
+ };
146
+
147
+ _ToggleMapButton.propTypes = {
148
+ tag: PropTypes.oneOf(["a", "button"]),
149
+ showInitial: PropTypes.bool,
150
+ className: PropTypes.string,
151
+ mapContainer: PropTypes.string,
152
+ labelOn: PropTypes.string,
153
+ labelOff: PropTypes.string,
154
+ delay: PropTypes.number,
155
+ titleOn: PropTypes.string,
156
+ titleOff: PropTypes.string,
157
+ config: PropTypes.object,
158
+ };
159
+
160
+ const ToggleMapButton = (props) => (
161
+ <Await
162
+ promise={get_map_config()}
163
+ then={(config) => {
164
+ return config.disabled || <_ToggleMapButton {...props} config={config} />;
165
+ }}
166
+ loading={<span />}
167
+ />
168
+ );
169
+
170
+ ToggleMapButton.propTypes = {
171
+ tag: PropTypes.oneOf(["a", "button"]),
172
+ className: PropTypes.string,
173
+ mapContainer: PropTypes.string,
174
+ labelOn: PropTypes.string,
175
+ labelOff: PropTypes.string,
176
+ titleOn: PropTypes.string,
177
+ titleOff: PropTypes.string,
178
+ delay: PropTypes.number,
179
+ showInitial: PropTypes.bool,
180
+ };
181
+
182
+ ToggleMapButton.defaultProps = {
183
+ tag: "a",
184
+ className: undefined,
185
+ mapContainer: ".caosdb-f-map-panel",
186
+ labelOn: "Hide Map",
187
+ labelOff: "Show Map",
188
+ titleOn: "Click to hide the map.",
189
+ titleOff: "Click to show the map.",
190
+ delay: 500,
191
+ showInitial: undefined,
192
+ };
193
+
194
+ export { ToggleMapButton };