@sedoo/unidoo 0.1.16 → 0.1.18

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.
@@ -0,0 +1,637 @@
1
+ /* eslint-disable no-new */
2
+ <template>
3
+ <v-app>
4
+ <v-card height="564px">
5
+ <v-toolbar
6
+ flat
7
+ dark
8
+ :color="colorTitle"
9
+ >
10
+ <v-card-title>{{ title }}</v-card-title>
11
+ </v-toolbar>
12
+ <div ref="map-root" style="width: 100%; height: 100%"></div>
13
+ <!-- Popup -->
14
+ <div v-show="content_element" id="popup" class="ol-popup">
15
+ <a href="#" id="popup-closer" class="ol-popup-closer" @click.prevent="closePopup"></a>
16
+ <div id="popup-content"></div>
17
+ </div>
18
+ <!-- Snackbar (Toast Notification) -->
19
+ <v-snackbar v-model="showToast" :bottom="true" :timeout="10000" :color="colorFeature">
20
+ Click on a feature drawn to view more details!
21
+ <v-btn color="pink" text @click="showToast = false">
22
+ Close
23
+ </v-btn>
24
+ </v-snackbar>
25
+ </v-card>
26
+ </v-app>
27
+ </template>
28
+
29
+ <script>
30
+ import View from 'ol/View'
31
+ import Map from 'ol/Map'
32
+ import TileLayer from 'ol/layer/Tile'
33
+ import OSM from 'ol/source/OSM'
34
+ import GeoJSON from 'ol/format/GeoJSON';
35
+ import VectorLayer from 'ol/layer/Vector'
36
+ import VectorSource from 'ol/source/Vector'
37
+ import {Fill, RegularShape, Stroke, Style} from 'ol/style';
38
+ // importing the OpenLayers stylesheet is required for having
39
+ // good looking buttons!
40
+ import 'ol/ol.css'
41
+ import {Select} from "ol/interaction";
42
+ import {click, pointerMove} from "ol/events/condition";
43
+ import {FullScreen} from "ol/control";
44
+ import {Overlay} from "ol";
45
+ import CircleStyle from "ol/style/Circle";
46
+ import {
47
+ DragRotateAndZoom,
48
+ defaults as defaultInteractions,
49
+ } from 'ol/interaction.js';
50
+
51
+
52
+ export default {
53
+ name: 'MapContainer',
54
+ components: {},
55
+ data() {
56
+ return {
57
+ map: null,
58
+ vectorLayer: null,
59
+ vectorSource: null,
60
+ openStyle: null,
61
+ selectedStyle: null,
62
+ loadingStations: false,
63
+ selectInteraction: null,
64
+ features: {},
65
+ feature: null,
66
+ popupOverlay: null,
67
+ featureProperties: {},
68
+ geoJsonObject: null,
69
+ content_element: null,
70
+ showToast: false,
71
+ bannedKeysArray: [],
72
+ };
73
+ },
74
+ props: {
75
+ title: {
76
+ type: String,
77
+ default: 'title',
78
+ },
79
+ colorTitle: {
80
+ type: String,
81
+ default: '#3f4a75',
82
+ },
83
+ colorFeature: {
84
+ type: String,
85
+ default: 'green',
86
+ },
87
+ colorOfSelectedFeature: {
88
+ type: String,
89
+ default: 'blue',
90
+ },
91
+ serviceUrl: {
92
+ type: String,
93
+ default: '',
94
+ },
95
+ bannedKeys: {
96
+ type: String,
97
+ default: () => {
98
+ return 'geometry,origin,interval,source,technicalId,externalLinkPrefix,externalLink,UTC_time,altitude';
99
+ }
100
+ },
101
+ shapeStylePoint: {
102
+ type: String,
103
+ default: 'circle',
104
+ },
105
+ disablePopup: {
106
+ type: String,
107
+ default: 'false',
108
+ }
109
+ },
110
+ created() {
111
+ // Show the toast notification if the disablePopup prop is not set to true.
112
+ if (this.disablePopup.toLowerCase() !== 'true') {
113
+ this.showToast = true;
114
+ }
115
+ },
116
+ mounted() {
117
+ this.initMap();
118
+ //
119
+ if (!this.disablePopup !== 'true') {
120
+ console.log('axios.then : initPopup()');
121
+ this.initPopup();
122
+ }
123
+ this.loadFeatures();
124
+ },
125
+ watch: {
126
+ serviceUrl: {
127
+ immediate: true, // also call the handler right after component initialization
128
+ handler(newVal, oldVal) {
129
+ if (newVal !== oldVal) {
130
+ console.log(`Service URL changed from ${oldVal} to ${newVal}`);
131
+ this.loadFeatures();
132
+ }
133
+ }
134
+ }
135
+ },
136
+ methods: {
137
+ loadFeatures() {
138
+
139
+ if (this.popupOverlay) {
140
+ // Close the popup if it is already open before loading new features.
141
+ this.closePopup();
142
+ }
143
+
144
+ this.axios.get(this.serviceUrl)
145
+ .then(res => {
146
+ const geoJsonObject = res.data
147
+ geoJsonObject.crs = {
148
+ type: 'name',
149
+ properties: {
150
+ name: 'EPSG:3857',
151
+ },
152
+ }
153
+ this.geoJsonObject = geoJsonObject;
154
+ this.updateMapFeatures(geoJsonObject);
155
+
156
+
157
+ })
158
+ },
159
+
160
+ initMap() {
161
+
162
+ //Initialisation de la Map
163
+
164
+ console.log('initMap()');
165
+
166
+ const styles = {
167
+
168
+ LineString: new Style({
169
+ stroke: new Stroke({
170
+ color: this.colorFeature,
171
+ width: 2,
172
+ }),
173
+ }),
174
+ /* For SAFIRE */
175
+ MultiLineString: new Style({
176
+ stroke: new Stroke({
177
+ color: this.colorFeature,
178
+ width: 1,
179
+ }),
180
+ }),
181
+ Polygon: new Style({
182
+ stroke: new Stroke({
183
+ color: this.colorFeature,
184
+ width: 3,
185
+ }),
186
+ fill: new Fill({
187
+ color: 'rgba(0, 0, 255, 0.1)',
188
+ }),
189
+ }),
190
+ Point: this.getPointStyle(false), // setting Point style dynamically when it is not selected (false)
191
+ }
192
+
193
+ const selectedStyle = {
194
+
195
+ LineString: new Style({
196
+ stroke: new Stroke({
197
+ color: this.colorOfSelectedFeature,
198
+ width: 2,
199
+ }),
200
+ }),
201
+ /* For SAFIRE */
202
+ MultiLineString: new Style({
203
+ stroke: new Stroke({
204
+ color: this.colorOfSelectedFeature,
205
+ width: 1,
206
+ }),
207
+ }),
208
+ Polygon: new Style({
209
+ stroke: new Stroke({
210
+ color: this.colorOfSelectedFeature,
211
+ width: 3,
212
+ }),
213
+ fill: new Fill({
214
+ color: 'rgba(0, 0, 255, 0.1)',
215
+ }),
216
+ }),
217
+ Point: this.getPointStyle(true), // setting Point style dynamically when it is selected (true)
218
+ }
219
+
220
+ this.selectedStyle = selectedStyle;
221
+
222
+ const styleFunction = function (feature) {
223
+ return styles[feature.getGeometry().getType()];
224
+ };
225
+
226
+ // Initialization of vectorSource should be empty at first.
227
+ this.vectorSource = new VectorSource({wrapX: false});
228
+
229
+ const vectorLayer = new VectorLayer({
230
+ source: this.vectorSource,
231
+ style: styleFunction,
232
+ });
233
+
234
+ // this is where we create the OpenLayers map
235
+ // eslint-disable-next-line no-new
236
+ this.map = new Map({
237
+ interactions: defaultInteractions().extend([new DragRotateAndZoom()]),
238
+ // the map will be created using the 'map-root' ref
239
+ target: this.$refs['map-root'],
240
+ layers: [
241
+ // adding a background tiled layer
242
+ new TileLayer({
243
+ source: new OSM() // tiles are served by OpenStreetMap
244
+ }),
245
+ vectorLayer
246
+ ],
247
+ //overlays: [this.popupOverlay],
248
+ // the map view will initially show the whole world
249
+ view: new View({
250
+ zoom: 0,
251
+ center: [0, 0],
252
+ constrainResolution: true,
253
+ projection: 'EPSG:3857'
254
+ }),
255
+ });
256
+
257
+ if (this.vectorSource.getFeatures().length > 0) {
258
+ this.map.getView().fit(this.vectorSource.getExtent());
259
+ }
260
+
261
+ let fullscreen = new FullScreen();
262
+ this.map.addControl(fullscreen);
263
+
264
+
265
+ //add select interaction to change the color of a selected feature
266
+ this.selectInteraction = new Select({
267
+ condition: click,
268
+ style: this.selectedStyleFunction,
269
+ });
270
+
271
+ this.map.addInteraction(this.selectInteraction);
272
+
273
+ //add hover interaction to show that a feature is selectable (change the color style)
274
+ this.hoverInteraction = new Select({
275
+ condition: pointerMove,
276
+ style: this.selectedStyleFunction,
277
+ });
278
+
279
+ this.map.addInteraction(this.hoverInteraction);
280
+
281
+
282
+ },
283
+
284
+ async updateMapFeatures(geoJsonObject) {
285
+ // Clear existing features.
286
+ this.vectorSource.clear();
287
+
288
+ // Convert GeoJSON features and add them to the source.
289
+ let geoJson = new GeoJSON({
290
+ dataProjection: 'EPSG:4326',
291
+ featureProjection: 'EPSG:3857'
292
+ });
293
+
294
+ let features = geoJsonObject.features;
295
+ if (features) {
296
+ this.features = features;
297
+ for (const element of features) {
298
+ this.vectorSource.addFeature(geoJson.readFeature(element));
299
+ }
300
+ }
301
+
302
+ // The $nextTick method is used to wait for Vue’s next update cycle to perform operations after the DOM has been updated. By using 'await', we ensure that the code following this line does not execute until the DOM updates are completed.
303
+ // This is particularly useful here to make sure that any features added to the vector source are rendered on the map before trying to adjust the view to fit them.
304
+ // Thus, we prevent potential issues where the view adjustment might occur before the features are fully rendered, which could result in an incorrect view or visual bugs.
305
+ await this.$nextTick();
306
+
307
+ // Log the extent for debugging:
308
+ console.log(this.vectorSource.getExtent());
309
+
310
+ // Check if there are features to fit to:
311
+ if (this.vectorSource.getFeatures().length > 0) {
312
+ // Ensure the view is adjusted to show the new features.
313
+ this.map.getView().fit(this.vectorSource.getExtent());
314
+ }
315
+
316
+ },
317
+
318
+
319
+ selectedStyleFunction(feature) {
320
+
321
+ return this.selectedStyle[feature.getGeometry().getType()];
322
+
323
+ },
324
+
325
+ getPointStyle(isSelectedColor) {
326
+ let image;
327
+ let color = this.colorFeature;
328
+ let fillColor = 'rgba(0, 0, 255, 0.1)';
329
+ if (isSelectedColor) {
330
+ color = this.colorOfSelectedFeature;
331
+ fillColor = this.colorOfSelectedFeature;
332
+ }
333
+
334
+ if (this.shapeStylePoint === 'circle') {
335
+ image = new CircleStyle({
336
+ radius: 5,
337
+ fill: new Fill({
338
+ color: fillColor,
339
+ }),
340
+ stroke: new Stroke({
341
+ color: color,
342
+ width: 1,
343
+ }),
344
+ });
345
+ } else if (this.shapeStylePoint === 'triangle') {
346
+ image = new RegularShape({
347
+ fill: new Fill({
348
+ color: fillColor,
349
+ }),
350
+ stroke: new Stroke({
351
+ color: color,
352
+ width: 1,
353
+ }),
354
+ points: 3,
355
+ radius: 10,
356
+ angle: 0,
357
+ });
358
+ } else if (this.shapeStylePoint === 'square') {
359
+ image = new RegularShape({
360
+ fill: new Fill({
361
+ color: fillColor,
362
+ }),
363
+ stroke: new Stroke({
364
+ color: color,
365
+ width: 1,
366
+ }),
367
+ points: 4,
368
+ radius: 10,
369
+ angle: Math.PI / 4,
370
+ });
371
+ } else if (this.shapeStylePoint === 'star') {
372
+ image = new RegularShape({
373
+ fill: new Fill({
374
+ color: fillColor,
375
+ }),
376
+ stroke: new Stroke({
377
+ color: color,
378
+ width: 1,
379
+ }),
380
+ points: 5,
381
+ radius: 10,
382
+ radius2: 4,
383
+ angle: 0,
384
+ });
385
+ } else if (this.shapeStylePoint === 'cross') {
386
+ image = new RegularShape({
387
+ fill: new Fill({
388
+ color: fillColor,
389
+ }),
390
+ stroke: new Stroke({
391
+ color: color,
392
+ width: 2,
393
+ }),
394
+ points: 4,
395
+ radius: 10,
396
+ radius2: 0,
397
+ angle: 0,
398
+ });
399
+ } else if (this.shapeStylePoint === 'x') {
400
+ image = new RegularShape({
401
+ fill: new Fill({
402
+ color: fillColor,
403
+ }),
404
+ stroke: new Stroke({
405
+ color: color,
406
+ width: 2,
407
+ }),
408
+ points: 4,
409
+ radius: 10,
410
+ radius2: 0,
411
+ angle: Math.PI / 4,
412
+ });
413
+ }
414
+
415
+ return new Style({
416
+ image: image
417
+ });
418
+ },
419
+
420
+ initPopup() {
421
+
422
+ //Initialisation de la Popup
423
+
424
+ console.log('initPopup()');
425
+
426
+ //init Array of excluded properties
427
+ this.bannedKeysArray = this.bannedKeys.split(',');
428
+
429
+ /**
430
+ * Elements that make up the popup.
431
+ */
432
+ const container = document.getElementById('popup');
433
+ this.content_element = document.getElementById('popup-content');
434
+ const closer = document.getElementById('popup-closer');
435
+
436
+ //Create a popup as an overlay
437
+ this.popupOverlay = new Overlay({
438
+ element: container,
439
+ positioning: "top-center",
440
+ stopEvent: true,
441
+ autoPan: { //the map automatically pans to make the overlay (popup) entirely visible.
442
+ animation: {
443
+ duration: 250,
444
+ },
445
+ offset: [0, -10]
446
+ },
447
+ });
448
+
449
+ // Change the cursor when hovering over a feature in the vector layer
450
+ this.map.on("pointermove", (evt) => {
451
+ if (!evt.dragging) {
452
+ this.map.getTargetElement().style.cursor = this.map.hasFeatureAtPixel(
453
+ this.map.getEventPixel(evt.originalEvent)
454
+ )
455
+ ? "pointer"
456
+ : "";
457
+ }
458
+
459
+ });
460
+
461
+ // Add a click handler to get the clicked feature
462
+ this.map.on("click", (evt) => {
463
+ let features = [];
464
+ this.map.forEachFeatureAtPixel(
465
+ evt.pixel,
466
+ (feature, layer) => {
467
+ features.push(feature);
468
+ }
469
+ );
470
+ if (features.length) {
471
+ this.createPopup(features, evt.coordinate);
472
+ } else {
473
+ this.popupOverlay.setPosition(undefined);
474
+ closer.blur();
475
+ return false;
476
+ }
477
+
478
+ });
479
+
480
+
481
+ },
482
+
483
+ /**
484
+ * Build the content of the popup and display it by adding the popupOverlay to the map
485
+ * reference code reused and modified : safire-components projects (git repo https://gitlab.in2p3.fr/sedoo)
486
+ * @param features
487
+ * @param position
488
+ */
489
+ createPopup(features, position) {
490
+ let content = '';
491
+ let i = 0;
492
+ features.forEach(feature => {
493
+ if (feature.id_) { //if feature.id_ is defined (e.g for SAFIRE)
494
+ content += '<p><h4>' + feature.id_ + '</h4>';
495
+ } else { //if feature.id_ is not defined (e.g for IAGOS)
496
+ i++;
497
+ let featureTitle = this.getFeatureTitleBasedOnGeometryType(feature);
498
+ if (features.length > 1) {
499
+ content += '<details>';
500
+ content += '<summary><b>' + featureTitle + ' ' + i + '</b></summary>';
501
+ } else {
502
+ content += '<p><h4>' + featureTitle + '</h4>';
503
+ }
504
+ }
505
+ let keys = this.extractKeysFromValues(feature.values_);
506
+ keys.forEach(key => {
507
+
508
+ //if feature.values_[key] is an Array
509
+ if (Array.isArray(feature.values_[key])) {
510
+ content += '<details>';
511
+ content += '<summary>' + key + ':</summary>'
512
+ if (feature.values_[key].length) {
513
+ content += '<ul>'
514
+ feature.values_[key].forEach(value => {
515
+ content += '<li>' + value + '</li>';
516
+ });
517
+ content += '</ul>';
518
+ } else {
519
+ content += '<kbd style="margin-left: 2em;">Empty</kbd>';
520
+ }
521
+ content += '</details>';
522
+ } else {
523
+ //Display content for a non array value
524
+ content += '<p>' + key + ':<kbd style="margin-left: 2em;">' + feature.values_[key] + '</kbd></p>';
525
+ }
526
+ });
527
+ if (features.length > 1 && !feature.id_) {
528
+ content += '</details>';
529
+ } else {
530
+ content += '</p>';
531
+ }
532
+ });
533
+
534
+ this.content_element.innerHTML = content;
535
+
536
+ this.popupOverlay.setPosition(position);
537
+ this.map.addOverlay(this.popupOverlay);
538
+
539
+ },
540
+
541
+ getFeatureTitleBasedOnGeometryType(feature) {
542
+ switch (feature.getGeometry().getType()) {
543
+ case 'Point':
544
+ return 'Site';
545
+ case 'LineString':
546
+ case 'MultiLineString':
547
+ return 'Trajectory';
548
+ case 'Polygon':
549
+ return 'Area';
550
+ default:
551
+ return 'Feature';
552
+ }
553
+ },
554
+
555
+ extractKeysFromValues(values) {
556
+ let result = [];
557
+ if (!!values) {
558
+ Object.keys(values).forEach(value => {
559
+ if (!this.exactWordIsInArray(value, this.bannedKeysArray)) //this.bannedKeysArray : properties to not display in the popup
560
+ result.push(value);
561
+ });
562
+ }
563
+ return result;
564
+ },
565
+
566
+ exactWordIsInArray(word, array) {
567
+ let result = false;
568
+ if (word && array)
569
+ array.forEach((item) =>
570
+ item && item === word ? (result = true) : null
571
+ );
572
+ return result;
573
+ },
574
+
575
+ closePopup() {
576
+ this.map.removeOverlay(this.popupOverlay);
577
+ }
578
+
579
+ }
580
+
581
+ }
582
+ </script>
583
+ <style scoped>
584
+
585
+ .ol-popup {
586
+ position: absolute;
587
+ background-color: white;
588
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
589
+ padding: 15px;
590
+ border-radius: 10px;
591
+ border: 1px solid #cccccc;
592
+ bottom: 12px;
593
+ left: -50px;
594
+ /*min-width: 280px;*/
595
+ /*cf. SAFIR */
596
+ min-width: 450px;
597
+ max-height: 250px;
598
+ /*overflow-y: scroll; adding this makes the arrow disappear*/
599
+ color: black;
600
+ -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
601
+ filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
602
+ }
603
+
604
+ #popup-content {
605
+ max-height: 200px; /* Or whatever height you want */
606
+ overflow-y: auto; /* Use auto instead of scroll to only show scrollbar when necessary */
607
+ }
608
+
609
+
610
+ .ol-popup:after, .ol-popup:before {
611
+ top: 100%;
612
+ border: solid transparent;
613
+ content: " ";
614
+ height: 0;
615
+ width: 0;
616
+ position: absolute;
617
+ pointer-events: none;
618
+ }
619
+
620
+ .ol-popup:after {
621
+ border-top-color: white;
622
+ border-width: 10px;
623
+ left: 48px;
624
+ margin-left: -10px;
625
+ }
626
+
627
+ .ol-popup-closer {
628
+ text-decoration: none;
629
+ position: absolute;
630
+ top: 2px;
631
+ right: 8px;
632
+ }
633
+
634
+ .ol-popup-closer:after {
635
+ content: "✖";
636
+ }
637
+ </style>