@kalisio/kdk 1.6.0 → 1.7.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 (188) hide show
  1. package/.travis.test.sh +1 -1
  2. package/CHANGELOG.md +63 -13
  3. package/extras/tours/map/catalog-panel.js +61 -10
  4. package/extras/tours/map/create-view.js +25 -0
  5. package/extras/tours/map/fab.js +10 -1
  6. package/extras/tours/map/navigation-bar.js +7 -9
  7. package/lib/core/api/application.js +9 -1
  8. package/lib/core/api/application.js.map +1 -1
  9. package/lib/core/client/components/chart/KChart.vue +115 -0
  10. package/lib/core/client/components/chart/KStatsChart.vue +76 -0
  11. package/lib/core/client/components/chart/index.js +20 -0
  12. package/lib/core/client/components/chart/index.js.map +1 -0
  13. package/lib/core/client/components/collection/KFilter.vue +6 -0
  14. package/lib/core/client/components/collection/KHistoryEntry.vue +1 -1
  15. package/lib/core/client/components/collection/KItem.vue +4 -4
  16. package/lib/core/client/components/form/KChipsField.vue +4 -4
  17. package/lib/core/client/components/form/KView.vue +1 -1
  18. package/lib/core/client/components/frame/KAction.vue +40 -5
  19. package/lib/core/client/components/frame/KBlock.vue +7 -7
  20. package/lib/core/client/components/frame/KContent.vue +1 -1
  21. package/lib/core/client/components/frame/KOpener.vue +1 -1
  22. package/lib/core/client/components/frame/KPanel.vue +8 -7
  23. package/lib/core/client/components/frame/KPopupAction.vue +6 -0
  24. package/lib/core/client/components/frame/KScreen.vue +1 -1
  25. package/lib/core/client/components/frame/KStamp.vue +4 -2
  26. package/lib/core/client/components/frame/index.js +1 -6
  27. package/lib/core/client/components/frame/index.js.map +1 -1
  28. package/lib/core/client/components/input/KIconChooser.vue +5 -2
  29. package/lib/core/client/components/input/KPalette.vue +1 -1
  30. package/lib/core/client/components/input/index.js +14 -4
  31. package/lib/core/client/components/input/index.js.map +1 -1
  32. package/lib/core/client/components/layout/KFab.vue +1 -1
  33. package/lib/core/client/components/layout/KLayout.vue +10 -2
  34. package/lib/core/client/components/layout/KPage.vue +19 -18
  35. package/lib/core/client/components/layout/KTour.vue +5 -3
  36. package/lib/core/client/components/layout/KWindow.vue +211 -59
  37. package/lib/core/client/components/media/KMediaBrowser.vue +6 -6
  38. package/lib/core/client/components/menu/KMenu.vue +13 -5
  39. package/lib/core/client/components/menu/KRadialFab.vue +22 -24
  40. package/lib/core/client/components/menu/KRadialFabItem.vue +17 -18
  41. package/lib/core/client/components/team/KGroupsActivity.vue +1 -1
  42. package/lib/core/client/components/team/KJoinGroup.vue +2 -2
  43. package/lib/core/client/components/time/KAbsoluteTimeRange.vue +190 -0
  44. package/lib/core/client/components/time/KRelativeTimeRanges.vue +192 -0
  45. package/lib/core/client/components/time/index.js +20 -0
  46. package/lib/core/client/components/time/index.js.map +1 -0
  47. package/lib/core/client/i18n/core_en.json +35 -12
  48. package/lib/core/client/i18n/core_fr.json +36 -13
  49. package/lib/core/client/index.js +1 -1
  50. package/lib/core/client/index.js.map +1 -1
  51. package/lib/core/client/mixins/mixin.base-collection.js +11 -1
  52. package/lib/core/client/mixins/mixin.base-collection.js.map +1 -1
  53. package/lib/core/client/mixins/mixin.base-widget.js +25 -13
  54. package/lib/core/client/mixins/mixin.base-widget.js.map +1 -1
  55. package/lib/core/client/services/index.js +2 -1
  56. package/lib/core/client/services/index.js.map +1 -1
  57. package/lib/core/client/services/local-settings.service.js +4 -0
  58. package/lib/core/client/services/local-settings.service.js.map +1 -1
  59. package/lib/core/client/time.js +25 -15
  60. package/lib/core/client/time.js.map +1 -1
  61. package/lib/core/client/units.js +12 -0
  62. package/lib/core/client/units.js.map +1 -1
  63. package/lib/core/client/utils.js +11 -0
  64. package/lib/core/client/utils.js.map +1 -1
  65. package/lib/core/common/schemas/settings.update.json +14 -4
  66. package/lib/map/api/hooks/hooks.catalog.js +20 -0
  67. package/lib/map/api/hooks/hooks.catalog.js.map +1 -1
  68. package/lib/map/api/models/catalog.model.mongodb.js +6 -1
  69. package/lib/map/api/models/catalog.model.mongodb.js.map +1 -1
  70. package/lib/map/api/models/features.model.mongodb.js +5 -0
  71. package/lib/map/api/models/features.model.mongodb.js.map +1 -1
  72. package/lib/map/api/services/catalog/catalog.hooks.js +3 -1
  73. package/lib/map/api/services/catalog/catalog.hooks.js.map +1 -1
  74. package/lib/map/client/components/KColorLegend.vue +22 -21
  75. package/lib/map/client/components/KFeaturesChart.vue +81 -110
  76. package/lib/map/client/components/KLayerStyleForm.vue +119 -47
  77. package/lib/map/client/components/KLevelSlider.vue +30 -29
  78. package/lib/map/client/components/KLocationMap.vue +2 -2
  79. package/lib/map/client/components/KMeasureTool.vue +30 -6
  80. package/lib/map/client/components/KPositionIndicator.vue +4 -4
  81. package/lib/map/client/components/KTimeline.vue +25 -27
  82. package/lib/map/client/components/KTimezoneMap.vue +156 -0
  83. package/lib/map/client/components/KUrlLegend.vue +11 -10
  84. package/lib/map/client/components/catalog/KBaseLayersSelector.vue +1 -1
  85. package/lib/map/client/components/catalog/KCatalogLayersPanel.vue +56 -0
  86. package/lib/map/client/components/catalog/KCreateView.vue +91 -0
  87. package/lib/map/client/components/catalog/KLayerCategories.vue +2 -1
  88. package/lib/map/client/components/catalog/{KCatalog.vue → KLayersPanel.vue} +19 -37
  89. package/lib/map/client/components/catalog/KUserLayersPanel.vue +40 -0
  90. package/lib/map/client/components/catalog/KViewSelector.vue +46 -0
  91. package/lib/map/client/components/catalog/KViewsPanel.vue +110 -0
  92. package/lib/map/client/components/catalog/KWeatherLayersSelector.vue +4 -13
  93. package/lib/map/client/components/form/KTimezoneField.vue +135 -0
  94. package/lib/map/client/components/widget/KElevationProfile.vue +488 -0
  95. package/lib/map/client/components/widget/KInformationBox.vue +48 -23
  96. package/lib/map/client/components/widget/KMapillaryViewer.vue +26 -20
  97. package/lib/map/client/components/widget/KTimeSeries.vue +267 -347
  98. package/lib/map/client/i18n/map_en.json +63 -40
  99. package/lib/map/client/i18n/map_fr.json +65 -42
  100. package/lib/map/client/leaflet/GradientPath.js +40 -19
  101. package/lib/map/client/leaflet/GradientPath.js.map +1 -1
  102. package/lib/map/client/leaflet/TiledFeatureLayer.js +527 -93
  103. package/lib/map/client/leaflet/TiledFeatureLayer.js.map +1 -1
  104. package/lib/map/client/leaflet/TiledMeshLayer.js +58 -35
  105. package/lib/map/client/leaflet/TiledMeshLayer.js.map +1 -1
  106. package/lib/map/client/leaflet/utils.js +44 -3
  107. package/lib/map/client/leaflet/utils.js.map +1 -1
  108. package/lib/map/client/mixins/globe/mixin.base-globe.js +16 -1
  109. package/lib/map/client/mixins/globe/mixin.base-globe.js.map +1 -1
  110. package/lib/map/client/mixins/globe/mixin.file-layers.js +12 -2
  111. package/lib/map/client/mixins/globe/mixin.file-layers.js.map +1 -1
  112. package/lib/map/client/mixins/globe/mixin.geojson-layers.js +7 -6
  113. package/lib/map/client/mixins/globe/mixin.geojson-layers.js.map +1 -1
  114. package/lib/map/client/mixins/globe/mixin.popup.js +4 -2
  115. package/lib/map/client/mixins/globe/mixin.popup.js.map +1 -1
  116. package/lib/map/client/mixins/globe/mixin.style.js +8 -4
  117. package/lib/map/client/mixins/globe/mixin.style.js.map +1 -1
  118. package/lib/map/client/mixins/globe/mixin.tooltip.js +4 -2
  119. package/lib/map/client/mixins/globe/mixin.tooltip.js.map +1 -1
  120. package/lib/map/client/mixins/index.js +23 -18
  121. package/lib/map/client/mixins/index.js.map +1 -1
  122. package/lib/map/client/mixins/map/mixin.base-map.js +20 -2
  123. package/lib/map/client/mixins/map/mixin.base-map.js.map +1 -1
  124. package/lib/map/client/mixins/map/mixin.edit-layers.js +8 -4
  125. package/lib/map/client/mixins/map/mixin.edit-layers.js.map +1 -1
  126. package/lib/map/client/mixins/map/mixin.geojson-layers.js +27 -5
  127. package/lib/map/client/mixins/map/mixin.geojson-layers.js.map +1 -1
  128. package/lib/map/client/mixins/map/mixin.heatmap-layers.js +6 -1
  129. package/lib/map/client/mixins/map/mixin.heatmap-layers.js.map +1 -1
  130. package/lib/map/client/mixins/map/mixin.popup.js +1 -1
  131. package/lib/map/client/mixins/map/mixin.popup.js.map +1 -1
  132. package/lib/map/client/mixins/map/mixin.style.js +8 -4
  133. package/lib/map/client/mixins/map/mixin.style.js.map +1 -1
  134. package/lib/map/client/mixins/map/mixin.tiled-mesh-layers.js +4 -11
  135. package/lib/map/client/mixins/map/mixin.tiled-mesh-layers.js.map +1 -1
  136. package/lib/map/client/mixins/map/mixin.tiled-wind-layers.js +0 -11
  137. package/lib/map/client/mixins/map/mixin.tiled-wind-layers.js.map +1 -1
  138. package/lib/map/client/mixins/map/mixin.tooltip.js +1 -1
  139. package/lib/map/client/mixins/map/mixin.tooltip.js.map +1 -1
  140. package/lib/map/client/mixins/mixin.activity.js +150 -150
  141. package/lib/map/client/mixins/mixin.activity.js.map +1 -1
  142. package/lib/map/client/mixins/mixin.catalog-panel.js +36 -0
  143. package/lib/map/client/mixins/mixin.catalog-panel.js.map +1 -0
  144. package/lib/map/client/mixins/mixin.feature-selection.js +17 -8
  145. package/lib/map/client/mixins/mixin.feature-selection.js.map +1 -1
  146. package/lib/map/client/mixins/mixin.feature-service.js +36 -16
  147. package/lib/map/client/mixins/mixin.feature-service.js.map +1 -1
  148. package/lib/map/client/mixins/mixin.infobox.js +1 -1
  149. package/lib/map/client/mixins/mixin.infobox.js.map +1 -1
  150. package/lib/map/client/mixins/mixin.levels.js +26 -12
  151. package/lib/map/client/mixins/mixin.levels.js.map +1 -1
  152. package/lib/map/client/mixins/mixin.style.js +1 -0
  153. package/lib/map/client/mixins/mixin.style.js.map +1 -1
  154. package/lib/map/client/mixins/mixin.weacast.js +15 -23
  155. package/lib/map/client/mixins/mixin.weacast.js.map +1 -1
  156. package/lib/map/client/pixi-utils.js +8 -177
  157. package/lib/map/client/pixi-utils.js.map +1 -1
  158. package/lib/map/client/utils.js +1 -0
  159. package/lib/map/client/utils.js.map +1 -1
  160. package/lib/map/common/grid.js +131 -0
  161. package/lib/map/common/grid.js.map +1 -1
  162. package/lib/map/common/index.js +22 -11
  163. package/lib/map/common/index.js.map +1 -1
  164. package/lib/map/common/meteo-model-grid-source.js +5 -2
  165. package/lib/map/common/meteo-model-grid-source.js.map +1 -1
  166. package/lib/map/common/time-based-grid-source.js +5 -2
  167. package/lib/map/common/time-based-grid-source.js.map +1 -1
  168. package/lib/test/client/core/collection.js +31 -13
  169. package/lib/test/client/core/collection.js.map +1 -1
  170. package/lib/test/client/core/layout.js +137 -49
  171. package/lib/test/client/core/layout.js.map +1 -1
  172. package/lib/test/client/core/utils.js +89 -22
  173. package/lib/test/client/core/utils.js.map +1 -1
  174. package/lib/test/client/map/catalog.js +134 -41
  175. package/lib/test/client/map/catalog.js.map +1 -1
  176. package/lib/test/client/map/controls.js +7 -4
  177. package/lib/test/client/map/controls.js.map +1 -1
  178. package/lib/test/client/map/index.js +12 -0
  179. package/lib/test/client/map/index.js.map +1 -1
  180. package/lib/test/client/map/utils.js +67 -0
  181. package/lib/test/client/map/utils.js.map +1 -0
  182. package/package.json +5 -5
  183. package/extras/tours/map/favorite-views.js +0 -53
  184. package/lib/core/client/components/frame/KChart.vue +0 -60
  185. package/lib/core/client/components/input/KCodeInput.vue +0 -50
  186. package/lib/core/client/components/input/KTimeRangeChooser.vue +0 -109
  187. package/lib/core/client/components/time/KTimeRange.vue +0 -144
  188. package/lib/map/client/components/KFavoriteViews.vue +0 -217
@@ -25,16 +25,10 @@
25
25
  </q-select>
26
26
  </div>
27
27
  </template>
28
- <template v-if="hasArchiveLayers" v-slot:footer>
29
- <q-tabs class="q-ma-sm text-primary" no-caps v-model="mode" @input="onModeChanged">
30
- <q-tab id="forecast" name="forecast" :label="$t('KWeatherLayersSelector.FORECASTS_LABEL')" />
31
- <q-tab id="archive" name="archive" :label="$t('KWeatherLayersSelector.ARCHIVES_LABEL')" />
32
- </q-tabs>
33
- </template>
34
28
  </k-layers-selector>
35
29
  </div>
36
30
  <div v-else class="row justify-center q-pa-sm">
37
- <k-stamp icon="las la-exclamation-circle" icon-size="sm" :text="$t('KWeatherLayersSelector.NO_MODEL_AVAILABLE')" direction="horizontal" />
31
+ <k-stamp icon="las la-exclamation-circle" icon-size="sm" :text="$t('KWeatherLayersSelector.NO_MODEL_AVAILABLE')" text-size="0.9rem" direction="horizontal" />
38
32
  </div>
39
33
  </template>
40
34
 
@@ -66,9 +60,6 @@ export default {
66
60
  }
67
61
  },
68
62
  computed: {
69
- hasArchiveLayers () {
70
- return _.find(this.layers, (layer) => { return layer.tags.includes('archive') })
71
- },
72
63
  filteredLayers () {
73
64
  if (this.mode === 'forecast') return this.filterForecastLayers()
74
65
  return this.filterArchiveLayers()
@@ -121,14 +112,14 @@ export default {
121
112
  },
122
113
  onModelChanged (model) {
123
114
  this.callHandler('toggle', model)
124
- },
125
- onModeChanged (mode) {
126
115
  }
127
116
  },
128
- created () {
117
+ beforeCreate () {
129
118
  // Loads the required components
130
119
  this.$options.components['k-layers-selector'] = this.$load('catalog/KLayersSelector')
131
120
  this.$options.components['k-stamp'] = this.$load('frame/KStamp')
121
+ },
122
+ created () {
132
123
  // Set the current forecast model
133
124
  this.model = this.forecastModel
134
125
  }
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="readOnly" :id="properties.name + '-field'">
4
+ {{ model }}
5
+ </div>
6
+ <div v-else>
7
+ <q-select
8
+ :id="properties.name + '-field'"
9
+ v-model="model"
10
+ :label="label"
11
+ :options="options"
12
+ use-input
13
+ @input='onChanged'
14
+ @filter="onAutocomplete"
15
+ emit-value
16
+ map-options
17
+ :error="hasError"
18
+ :error-message="errorLabel"
19
+ :disabled="disabled"
20
+ bottom-slots>
21
+ <!-- Map display -->
22
+ <template v-slot:prepend>
23
+ <k-action
24
+ id="timezone-map"
25
+ icon="las la-map-marker"
26
+ color="primary"
27
+ :handler="openTimezoneMap"
28
+ :tooltip="$t('KTimezoneField.TIMEZONE_MAP_TOOLTIP')" />
29
+ </template>
30
+ <!-- Options display -->
31
+ <template v-slot:option="scope">
32
+ <q-item
33
+ :id="scope.opt.value"
34
+ v-bind="scope.itemProps"
35
+ v-on="scope.itemEvents"
36
+ >
37
+ <q-item-section>
38
+ <q-item-label>{{ scope.opt.label }}</q-item-label>
39
+ </q-item-section>
40
+ </q-item>
41
+ </template>
42
+ <template v-slot:append>
43
+ <q-icon
44
+ v-if="model"
45
+ class="cursor-pointer"
46
+ name="cancel"
47
+ @click.stop="fill('')"
48
+ />
49
+ </template>
50
+ <!-- Helper -->
51
+ <template v-if="helper" v-slot:hint>
52
+ <span v-html="helper"></span>
53
+ </template>
54
+ </q-select>
55
+ <k-modal ref="timezoneMapModal"
56
+ :title="$t('KTimezoneField.TIMEZONE_MAP_TITLE')"
57
+ :buttons="getTimezoneMapModalButtons()"
58
+ :options="{}">
59
+ <k-timezone-map id="timezones-map" style="min-height: 250px;" :value="this.model" @timezone-selected="onMapTimezoneSelected"/>
60
+ </k-modal>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <script>
66
+ import _ from 'lodash'
67
+ import moment from 'moment-timezone/builds/moment-timezone-with-data-10-year-range'
68
+ import { mixins as kCoreMixins, utils as kCoreUtils } from '../../../../core/client'
69
+ import meta from 'moment-timezone/data/meta/latest.json'
70
+
71
+ const timezones = moment.tz.names()
72
+ // Timezone names contains additional "usual" timezone namings like GMT+1, etc.
73
+ // const timezones = _.keys(meta.zones)
74
+ const countries = _.values(meta.countries)
75
+
76
+ export default {
77
+ name: 'k-timezone-field',
78
+ mixins: [kCoreMixins.baseField],
79
+ data () {
80
+ return {
81
+ options: timezones.map(timezone => ({ value: timezone, label: kCoreUtils.getTimezoneLabel(timezone) }))
82
+ }
83
+ },
84
+ methods: {
85
+ getTimezoneMapModalButtons () {
86
+ return [
87
+ { id: 'cancel-button', label: 'CANCEL', renderer: 'form-button', outline: true, handler: () => this.closeTimezoneMap() },
88
+ { id: 'apply-button', label: 'APPLY', renderer: 'form-button', handler: () => this.closeTimezoneMap(true) }
89
+ ]
90
+ },
91
+ onMapTimezoneSelected (timezone) {
92
+ this.mapTimezone = timezone
93
+ },
94
+ openTimezoneMap () {
95
+ this.$refs.timezoneMapModal.open()
96
+ },
97
+ async closeTimezoneMap (fill = false) {
98
+ this.$refs.timezoneMapModal.close()
99
+ if (fill) {
100
+ this.fill(this.mapTimezone)
101
+ // Seems to be required to correctly update the label in the q-select
102
+ await this.$nextTick()
103
+ }
104
+ },
105
+ onAutocomplete (value, update) {
106
+ // Check for any matching country also
107
+ const matchingCountries = countries.filter(country => country.name.toLocaleLowerCase().includes(value.toLocaleLowerCase()))
108
+ update(() => {
109
+ this.options = timezones
110
+ .filter(timezone => {
111
+ // Filter applies to timezone names or country names
112
+ const matchTimezone = timezone.toLocaleLowerCase().includes(value.toLocaleLowerCase())
113
+ if (matchTimezone) return true
114
+ // We have the list of timezones associated to each matching country
115
+ for (let i = 0; i < matchingCountries.length; i++) {
116
+ const matchingTimezones = matchingCountries[i].zones
117
+ if (matchingTimezones.includes(timezone)) return true
118
+ }
119
+ return false
120
+ })
121
+ .map(timezone => ({ value: timezone, label: kCoreUtils.getTimezoneLabel(timezone) }))
122
+ })
123
+ }
124
+ },
125
+ created () {
126
+ // Load the required components
127
+ this.$options.components['k-action'] = this.$load('frame/KAction')
128
+ this.$options.components['k-modal'] = this.$load('frame/KModal')
129
+ this.$options.components['k-timezone-map'] = this.$load('KTimezoneMap')
130
+
131
+ // Load metadata
132
+ this.meta = require('moment-timezone/data/meta/latest.json')
133
+ }
134
+ }
135
+ </script>
@@ -0,0 +1,488 @@
1
+ <template>
2
+ <div id="elevation-profile" class="column" :style="widgetStyle">
3
+ <k-chart ref="chart" class="col q-pl-sm q-pr-sm" />
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ import _ from 'lodash'
9
+ import logger from 'loglevel'
10
+ import { baseWidget } from '../../../../core/client/mixins'
11
+ import { Units } from '../../../../core/client/units'
12
+ import { colors, copyToClipboard, exportFile } from 'quasar'
13
+ import along from '@turf/along'
14
+ import length from '@turf/length'
15
+ import flatten from '@turf/flatten'
16
+ import { segmentEach, coordEach } from '@turf/meta'
17
+ import { featureCollection } from '@turf/helpers'
18
+
19
+ export default {
20
+ name: 'k-elevation-profile',
21
+ inject: ['kActivity'],
22
+ mixins: [baseWidget],
23
+ props: {
24
+ feature: {
25
+ type: Object,
26
+ default: null
27
+ },
28
+ layer: {
29
+ type: Object,
30
+ default: null
31
+ }
32
+ },
33
+ computed: {
34
+ title () {
35
+ return _.get(this.feature, 'name') ||
36
+ _.get(this.feature, 'label') ||
37
+ _.get(this.feature, 'properties.name') ||
38
+ _.get(this.feature, 'properties.label') ||
39
+ _.get(this.layer, 'name') ||
40
+ _.get(this.layer, 'properties.name')
41
+ }
42
+ },
43
+ data () {
44
+ return {
45
+ profile: null
46
+ }
47
+ },
48
+ watch: {
49
+ feature: {
50
+ immediate: true,
51
+ handler () {
52
+ this.refresh()
53
+ }
54
+ }
55
+ },
56
+ methods: {
57
+ refreshActions () {
58
+ this.$store.patch('window', {
59
+ widgetActions: [
60
+ {
61
+ id: 'center-view',
62
+ icon: 'las la-eye',
63
+ tooltip: this.$t('KElevationProfile.CENTER_ON'),
64
+ visible: this.feature,
65
+ handler: this.onCenterOn
66
+ },
67
+ {
68
+ id: 'copy-properties',
69
+ icon: 'las la-clipboard',
70
+ tooltip: this.$t('KElevationProfile.COPY_PROFILE'),
71
+ visible: this.profile,
72
+ handler: this.onCopyProfile
73
+ },
74
+ {
75
+ id: 'export-feature',
76
+ icon: 'img:statics/json-icon.svg',
77
+ tooltip: this.$t('KElevationProfile.EXPORT_PROFILE'),
78
+ visible: this.profile,
79
+ handler: this.onExportProfile
80
+ }
81
+ ]
82
+ })
83
+ },
84
+ extractProfileData (profiles) {
85
+ // Extract profile heights if available on the segments used to compute elevation
86
+ const profileHeights = []
87
+ const profileLabels = []
88
+ let allCoordsHaveHeight = true
89
+ let curvilinearOffset = 0
90
+ for (let i = 0; i < profiles.length && allCoordsHaveHeight; ++i) {
91
+ const dataUnit = _.get(profiles[i], 'properties.altitudeUnit', 'm')
92
+ // Gather elevation at each coord, make sure all coords have height along the way
93
+ coordEach(profiles[i], (coord) => {
94
+ if (coord.length > 2) profileHeights.push(Units.convert(coord[2], dataUnit, this.chartHeightUnit))
95
+ else allCoordsHaveHeight = false
96
+ })
97
+ // Compute curvilinear abscissa at each point
98
+ if (allCoordsHaveHeight) {
99
+ segmentEach(profiles[i], (segment) => {
100
+ if (profileLabels.length === 0) profileLabels.push(0)
101
+ curvilinearOffset += length(segment, { units: 'kilometers' }) * 1000
102
+ profileLabels.push(Units.convert(curvilinearOffset, 'm', this.chartDistanceUnit))
103
+ })
104
+ }
105
+ }
106
+
107
+ return allCoordsHaveHeight ? [profileHeights, profileLabels] : [[], []]
108
+ },
109
+ updateChart (terrainHeights, terrainLabels, profileHeights, profileLabels, chartWidth) {
110
+ const update = {
111
+ type: 'line',
112
+ data: { datasets: [] },
113
+ plugins: [{
114
+ // a simple plugin to display a vertical line at cursor position
115
+ beforeEvent: (chart, args) => {
116
+ if (args.event.type === 'mousemove') {
117
+ if ((args.event.x >= chart.chartArea.left) &&
118
+ (args.event.x <= chart.chartArea.right)) {
119
+ chart.config.options.vline.enabled = true
120
+ chart.config.options.vline.x = args.event.x
121
+ } else {
122
+ chart.config.options.vline.enabled = false
123
+ }
124
+ } else if (args.event.type === 'mouseout') {
125
+ chart.config.options.vline.enabled = false
126
+ }
127
+ },
128
+ afterDraw: (chart) => {
129
+ const x = chart.config.options.vline.x
130
+ const ctx = chart.ctx
131
+ if (chart.config.options.vline.enabled && !isNaN(x)) {
132
+ ctx.save()
133
+ ctx.translate(0.5, 0.5)
134
+ ctx.lineWidth = 1
135
+ ctx.strokeStyle = chart.config.options.vline.color
136
+ ctx.beginPath()
137
+ ctx.moveTo(x, chart.chartArea.bottom)
138
+ ctx.lineTo(x, chart.chartArea.top)
139
+ ctx.stroke()
140
+ ctx.restore()
141
+ }
142
+ }
143
+ }],
144
+ options: {
145
+ maintainAspectRatio: false,
146
+ // stepped: 'middle',
147
+ parsing: false, // because we'll provide data in chart native format
148
+ onHover: (context, elements) => {
149
+ // update marker highlight along profile
150
+ if (elements.length) {
151
+ const abscissa = _.get(elements[0].element, '$context.parsed.x')
152
+ if (abscissa !== undefined) {
153
+ let abscissaKm = Units.convert(abscissa, this.chartDistanceUnit, 'km')
154
+ let segment = this.feature
155
+ // handle multi line strings too, find segment in which abscissa is
156
+ if (_.get(this.feature, 'geometry.type') === 'MultiLineString') {
157
+ const lines = flatten(this.feature).features
158
+ for (let i = 0; i < lines.length && segment === this.feature; ++i) {
159
+ const len = length(lines[i], { units: 'kilometers' })
160
+ if (i !== lines.length - 1) {
161
+ if (abscissaKm > len) { abscissaKm -= len }
162
+ else { segment = lines[i] }
163
+ } else {
164
+ // last multi line segment, must be on this one
165
+ if (abscissaKm > len) { abscissaKm = len }
166
+ segment = lines[i]
167
+ }
168
+ }
169
+ }
170
+
171
+ const feature = along(segment, abscissaKm, { units: 'kilometers' })
172
+ this.kActivity.updateSelectionHighlight('elevation-profile', feature)
173
+ }
174
+ }
175
+
176
+ // restore tooltip and vline if they've been disabled during
177
+ // pan or zoom animation
178
+ if (context.chart.config.options.plugins.tooltip.enabled) return
179
+ context.chart.config.options.plugins.tooltip.enabled = true
180
+ context.chart.config.options.vline.enabled = true
181
+ context.chart.update()
182
+ },
183
+ interaction: {
184
+ mode: 'xSingle',
185
+ intersect: false
186
+ },
187
+ scales: {
188
+ x: {
189
+ type: 'linear',
190
+ beginAtZero: true,
191
+ title: {
192
+ display: true,
193
+ text: this.$t('KElevationProfile.CURVILINEAR_AXIS_LEGEND', { unit: this.chartDistanceUnit })
194
+ }
195
+ },
196
+ y: {
197
+ type: 'linear',
198
+ beginAtZero: true,
199
+ title: {
200
+ display: true,
201
+ text: this.$t('KElevationProfile.HEIGHT_AXIS_LEGEND', { unit: this.chartHeightUnit })
202
+ }
203
+ }
204
+ },
205
+ vline: { // option values related to vertical line plugin defined inline
206
+ enabled: false,
207
+ x: 0,
208
+ color: 'black'
209
+ },
210
+ plugins: {
211
+ title: {
212
+ display: true,
213
+ text: this.title,
214
+ align: 'start'
215
+ },
216
+ legend: {
217
+ display: false
218
+ },
219
+ datalabels: {
220
+ display: false
221
+ },
222
+ tooltip: {
223
+ position: 'cursorPosition',
224
+ callbacks: {
225
+ /*
226
+ title: (context) => {
227
+ let title = `${context[0].parsed.x.toFixed(2)} ${this.chartDistanceUnit}`
228
+ return title
229
+ },
230
+ */
231
+ label: (context) => {
232
+ let label = context.dataset.label || ''
233
+ if (label) label += ': '
234
+ if (context.parsed.y !== null) label += Units.format(context.parsed.y, this.chartHeightUnit)
235
+ return label
236
+ }
237
+ }
238
+ },
239
+ decimation: {
240
+ enabled: true,
241
+ algorithm: 'lttb',
242
+ // algorithm: 'min-max',
243
+ samples: Math.floor(chartWidth / 6),
244
+ threshold: Math.floor(chartWidth / 2)
245
+ },
246
+ zoom: {
247
+ pan: {
248
+ // pan with mouse and no modifiers
249
+ enabled: true,
250
+ // modifierKey: 'ctrl',
251
+ onPanStart: (context) => {
252
+ // robin: for some reason, pan starts even with some modifiers keys
253
+ // make sure here there's no modifiers here
254
+ const event = _.get(context, 'event.srcEvent')
255
+ const hasModifiers = event ? (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) : false
256
+ if (hasModifiers) return false
257
+
258
+ // hide tooltip & vline while zooming
259
+ context.chart.config.options.plugins.tooltip.enabled = false
260
+ context.chart.config.options.vline.enabled = false
261
+ return true
262
+ }
263
+ },
264
+ limits: {
265
+ x: { min: 'original', max: 'original' },
266
+ y: { min: 'original', max: 'original' }
267
+ },
268
+ zoom: {
269
+ // zoom with mouse + ctrl, or wheel
270
+ drag: {
271
+ enabled: true,
272
+ modifierKey: 'ctrl',
273
+ backgroundColor: colors.getBrand('secondary')
274
+ },
275
+ wheel: {
276
+ enabled: true
277
+ },
278
+ mode: 'x',
279
+ onZoomStart: (context) => {
280
+ // hide tooltip & vline while zooming
281
+ context.chart.config.options.plugins.tooltip.enabled = false
282
+ context.chart.config.options.vline.enabled = false
283
+ return true
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ // Add profile elevation if provided
292
+ if (profileHeights.length) {
293
+ update.data.datasets.push({
294
+ label: this.$t('KElevationProfile.PROFILE_CHART_LEGEND'),
295
+ data: profileHeights.map((h, i) => { return { x: profileLabels[i], y: h } }),
296
+ fill: false,
297
+ borderColor: '#51b0e8',
298
+ backgroundColor: '#0986bc',
299
+ pointRadius: 3
300
+ })
301
+ update.options.plugins.legend.display = true
302
+ }
303
+
304
+ // Add terrain elevation dataset
305
+ update.data.datasets.push({
306
+ label: this.$t('KElevationProfile.TERRAIN_CHART_LEGEND'),
307
+ data: terrainHeights.map((h, i) => { return { x: terrainLabels[i], y: h } }),
308
+ fill: true,
309
+ borderColor: '#635541',
310
+ backgroundColor: '#c9b8a1',
311
+ pointRadius: 2
312
+ })
313
+
314
+ this.$refs.chart.update(update)
315
+ },
316
+
317
+ async refresh () {
318
+ const maxResolution = 30
319
+ this.profile = null
320
+ this.refreshActions()
321
+ if (!this.layer || !this.feature) return
322
+
323
+ // Check supported geometry
324
+ const geometry = _.get(this.feature, 'geometry.type')
325
+ if (geometry !== 'LineString' && geometry !== 'MultiLineString') {
326
+ logger.warn('the selected feature has an invald geometry')
327
+ this.$toast({ type: 'negative', message: this.$t('KElevationProfile.INVALID_GEOMETRY') })
328
+ return
329
+ }
330
+
331
+ const featureStyle = { properties: { 'marker-type': 'marker' } }
332
+ this.kActivity.addSelectionHighlight('elevation-profile', featureStyle)
333
+
334
+ this.chartDistanceUnit = 'm'
335
+ this.chartHeightUnit = Units.getDefaultUnit('altitude')
336
+
337
+ // TODO: this is the window size, not the widget size ...
338
+ const windowSize = this.$store.get('window.size')
339
+ const chartWidth = windowSize[0]
340
+
341
+ const queries = []
342
+ const resolution = _.get(this.feature, 'properties.elevationProfile.resolution')
343
+ const resolutionUnit = _.get(this.feature, 'properties.elevationProfile.resolutionUnit', 'm')
344
+ const corridor = _.get(this.feature, 'properties.elevationProfile.corridorWidth')
345
+ const corridorUnit = _.get(this.feature, 'properties.elevationProfile.corridorWidthUnit', 'm')
346
+ const securityMargin = _.get(this.feature, 'properties.elevationProfile.securityMargin')
347
+ const securityMarginUnit = _.get(this.feature, 'properties.elevationProfile.securityMarginUnit', 'm')
348
+ if (geometry === 'MultiLineString') {
349
+ flatten(this.feature).features.forEach((feature, index) => {
350
+ queries.push({
351
+ profile: feature,
352
+ resolution: Units.convert(resolution[index], resolutionUnit, 'm'),
353
+ corridorWidth: corridor ? Units.convert(corridor[index], corridorUnit, 'm') : null,
354
+ securityMargin: securityMargin ? Units.convert(securityMargin[index], securityMarginUnit, 'm') : null
355
+ })
356
+ })
357
+ } else {
358
+ const pixelStep = 5
359
+ const res = resolution ? Units.convert(resolution, resolutionUnit, 'm') : Math.max(length(this.feature, { units: 'kilometers' }) * 1000 / (chartWidth / pixelStep), maxResolution)
360
+ queries.push({
361
+ profile: this.feature,
362
+ resolution: res,
363
+ corridorWidth: corridor ? Units.convert(corridor, corridorUnit, 'm') : null,
364
+ securityMargin: securityMargin ? Units.convert(securityMargin, securityMarginUnit, 'm') : null
365
+ })
366
+ }
367
+
368
+ // Extract heights from profile if available
369
+ const [profileHeights, profileLabels] = this.extractProfileData(queries.map((q) => q.profile))
370
+
371
+ // Setup the request url options
372
+ const endpoint = this.$store.get('capabilities.api.gateway') + '/elevation'
373
+ const headers = { 'Content-Type': 'application/json' }
374
+ // Add the Authorization header if jwt is defined
375
+ const jwt = this.$api.get('storage').getItem(this.$config('gatewayJwt'))
376
+ if (jwt) headers.Authorization = 'Bearer ' + jwt
377
+
378
+ // Perform the requests
379
+ let dismiss = null
380
+ dismiss = this.$q.notify({
381
+ group: 'profile',
382
+ icon: 'las la-hourglass-half',
383
+ message: this.$t('KElevationProfile.COMPUTING_PROFILE'),
384
+ color: 'primary',
385
+ timeout: 0,
386
+ spinner: true
387
+ })
388
+
389
+ // Build a fetch per profile
390
+ const fetchs = []
391
+ for (const query of queries) {
392
+ fetchs.push(fetch(endpoint
393
+ + `?resolution=${query.resolution}`
394
+ + (query.corridorWidth ? `&corridorWidth=${query.corridorWidth}` : '')
395
+ + (query.securityMargin ? `&elevationOffset=${query.securityMargin}` : ''), {
396
+ method: 'POST',
397
+ mode: 'cors',
398
+ body: JSON.stringify(query.profile),
399
+ headers
400
+ }))
401
+ }
402
+
403
+ let responses
404
+ try {
405
+ responses = await Promise.all(fetchs)
406
+ for (const res of responses) {
407
+ if (!res.ok) throw new Error('Fetch failed')
408
+ }
409
+ } catch (error) {
410
+ // Network error
411
+ dismiss()
412
+ this.$toast({ type: 'negative', message: this.$t('errors.NETWORK_ERROR') })
413
+ return
414
+ }
415
+
416
+ dismiss()
417
+
418
+ // Each profile will have a point on start and end points
419
+ // When we have multi line string, we skip the first point for all segment
420
+ // after the first one
421
+ let skipFirstPoint = false
422
+ const terrainHeights = []
423
+ const terrainLabels = []
424
+ let curvilinearOffset = 0
425
+ this.profile = []
426
+ for (let i = 0; i < queries.length; ++i) {
427
+ const points = await responses[i].json()
428
+ // Each point on the elevation profile will contains two properties;
429
+ // - z: the elevation in meters
430
+ // - t: the curvilinear abscissa relative to the queried profile in meters
431
+ points.features.forEach((point, index) => {
432
+ if (skipFirstPoint && index === 0) return
433
+
434
+ const clone = _.cloneDeep(point)
435
+ // Since we may have multiple profile with different query parameters
436
+ // offset t accordingly
437
+ clone.properties.t = Units.convert(curvilinearOffset + _.get(point, 'properties.t', 0), 'm', this.chartDistanceUnit)
438
+ this.profile.push(clone)
439
+
440
+ terrainHeights.push(Units.convert(point.properties.z, 'm', this.chartHeightUnit))
441
+ terrainLabels.push(clone.properties.t)
442
+ })
443
+ // Update curvilinear offset for next profile, and skip next profile's first point
444
+ // since it'll match with the current profile last point
445
+ curvilinearOffset += length(queries[i].profile, { units: 'kilometers' }) * 1000
446
+ skipFirstPoint = true
447
+ }
448
+
449
+ this.updateChart(terrainHeights, terrainLabels, profileHeights, profileLabels, chartWidth)
450
+
451
+ this.profile = featureCollection(this.profile)
452
+
453
+ // Refresh the actions
454
+ this.refreshActions()
455
+ },
456
+ onCenterOn () {
457
+ this.kActivity.centerOnSelection()
458
+ },
459
+ async onCopyProfile () {
460
+ if (this.profile) {
461
+ try {
462
+ await copyToClipboard(JSON.stringify(this.profile))
463
+ this.$toast({ type: 'positive', message: this.$t('KElevationProfile.PROFILE_COPIED') })
464
+ } catch (_) {
465
+ this.$toast({ type: 'negative', message: this.$t('KElevationProfile.CANNOT_COPY_PROFILE') })
466
+ }
467
+ }
468
+ },
469
+ onExportProfile () {
470
+ if (this.profile) {
471
+ const file = this.title + '.geojson'
472
+ const status = exportFile(file, JSON.stringify(this.profile))
473
+ if (status) this.$toast({ type: 'positive', message: this.$t('KElevationProfile.PROFILE_EXPORTED', { file }) })
474
+ else this.$toast({ type: 'negative', message: this.$t('KElevationProfile.CANNOT_EXPORT_PROFILE') })
475
+ }
476
+ }
477
+ },
478
+ beforeCreate () {
479
+ // laod the required components
480
+ this.$options.components['k-chart'] = this.$load('chart/KChart')
481
+ this.$options.components['k-panel'] = this.$load('frame/KPanel')
482
+ this.$options.components['k-stamp'] = this.$load('frame/KStamp')
483
+ },
484
+ beforeDestroy () {
485
+ this.kActivity.removeSelectionHighlight('elevation-profile')
486
+ }
487
+ }
488
+ </script>