@sedoo/unidoo 0.1.16 → 0.1.17

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