@kalisio/kdk 2.1.5 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/client/components/account/KSendResetPassword.vue +1 -1
- package/core/client/components/screen/KLoginScreen.vue +2 -3
- package/map/client/components/catalog/KLayersSelector.vue +4 -4
- package/map/client/components/catalog/KViewsPanel.vue +4 -4
- package/map/client/components/location/KLocationMap.vue +1 -1
- package/map/client/globe.js +1 -1
- package/map/client/index.js +1 -1
- package/map/client/map.js +1 -1
- package/map/client/mixins/globe/mixin.base-globe.js +1 -1
- package/map/client/mixins/globe/mixin.popup.js +1 -1
- package/map/client/mixins/globe/mixin.style.js +1 -1
- package/map/client/mixins/map/mixin.base-map.js +1 -1
- package/map/client/mixins/map/mixin.canvas-layers.js +1 -1
- package/map/client/mixins/map/mixin.edit-layers.js +1 -1
- package/map/client/mixins/map/mixin.geojson-layers.js +1 -1
- package/map/client/mixins/map/mixin.mapillary-layers.js +1 -1
- package/map/client/mixins/map/mixin.popup.js +1 -1
- package/map/client/mixins/map/mixin.style.js +1 -1
- package/map/client/utils/index.js +3 -0
- package/map/client/utils/utils.js +228 -0
- package/map/client/utils/utils.location.js +1 -1
- package/map/client/utils/utils.schema.js +75 -0
- package/map/client/utils.all.js +3 -0
- package/map/client/utils.globe.js +2 -0
- package/map/client/utils.js +4 -303
- package/map/client/utils.map.js +2 -0
- package/package.json +1 -1
- package/test/client/map/catalog.js +18 -0
|
@@ -89,7 +89,7 @@ async function apply () {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
} else {
|
|
92
|
-
$q.notify({ type: 'negative', message: i18n.t('KSendResetPassword.ERROR_INVALID_EMAIL') })
|
|
92
|
+
$q.notify({ type: 'negative', message: i18n.t('KSendResetPassword.ERROR_INVALID_EMAIL') })
|
|
93
93
|
}
|
|
94
94
|
send.value = true
|
|
95
95
|
processing.value = false
|
|
@@ -75,9 +75,8 @@ async function onLogin () {
|
|
|
75
75
|
} catch (error) {
|
|
76
76
|
$q.notify({ type: 'negative', message: i18n.t('KLoginScreen.LOGIN_ERROR') })
|
|
77
77
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
$q.notify({ type: 'negative', message: i18n.t('KLoginScreen.INVALID_EMAIL') })
|
|
78
|
+
} else {
|
|
79
|
+
$q.notify({ type: 'negative', message: i18n.t('KLoginScreen.INVALID_EMAIL') })
|
|
81
80
|
}
|
|
82
81
|
loading.value = false
|
|
83
82
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
<slot name="header" />
|
|
4
4
|
<div v-if="layers.length > 0">
|
|
5
5
|
<template v-for="layer in layers">
|
|
6
|
-
<component
|
|
7
|
-
:is="layerRenderer.component"
|
|
8
|
-
v-bind="layerRenderer.options"
|
|
6
|
+
<component
|
|
7
|
+
:is="layerRenderer.component"
|
|
8
|
+
v-bind="layerRenderer.options"
|
|
9
9
|
:layer="layer"
|
|
10
|
-
@toggled="onLayerToggled"
|
|
10
|
+
@toggled="onLayerToggled"
|
|
11
11
|
@filter-toggled="onLayerFilterToggled"
|
|
12
12
|
/>
|
|
13
13
|
</template>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<q-list dense bordered>
|
|
3
3
|
<div class="no-padding" :style="panelStyle">
|
|
4
|
-
<KPanel
|
|
5
|
-
id="favorite-views-toolbar"
|
|
6
|
-
:content="toolbar"
|
|
7
|
-
class="no-wrap q-pl-sm q-pr-md"
|
|
4
|
+
<KPanel
|
|
5
|
+
id="favorite-views-toolbar"
|
|
6
|
+
:content="toolbar"
|
|
7
|
+
class="no-wrap q-pl-sm q-pr-md"
|
|
8
8
|
/>
|
|
9
9
|
<KColumn
|
|
10
10
|
class="q-pl-sm"
|
|
@@ -29,7 +29,7 @@ import { Geolocation } from '../../geolocation'
|
|
|
29
29
|
import {
|
|
30
30
|
setEngineJwt, coordinatesToGeoJSON, formatUserCoordinates,
|
|
31
31
|
bindLeafletEvents, unbindLeafletEvents, createLeafletMarkerFromStyle, convertToLeafletFromSimpleStyleSpec
|
|
32
|
-
} from '../../utils'
|
|
32
|
+
} from '../../utils.map.js'
|
|
33
33
|
|
|
34
34
|
export default {
|
|
35
35
|
components: {
|
package/map/client/globe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as commonMixins from './mixins/index.js'
|
|
2
2
|
import * as globeMixins from './mixins/globe/index.js'
|
|
3
|
-
import * as utils from './utils.js'
|
|
3
|
+
import * as utils from './utils.globe.js'
|
|
4
4
|
import init from './init.js'
|
|
5
5
|
|
|
6
6
|
const mixins = Object.assign({}, commonMixins, { globe: globeMixins })
|
package/map/client/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as composables from './composables/index.js'
|
|
|
2
2
|
import * as commonMixins from './mixins/index.js'
|
|
3
3
|
import * as mapMixins from './mixins/map/index.js'
|
|
4
4
|
import * as globeMixins from './mixins/globe/index.js'
|
|
5
|
-
import * as utils from './utils.js'
|
|
5
|
+
import * as utils from './utils.all.js'
|
|
6
6
|
import * as elevationUtils from './elevation-utils.js'
|
|
7
7
|
import init from './init.js'
|
|
8
8
|
const mixins = Object.assign({}, commonMixins, { map: mapMixins, globe: globeMixins })
|
package/map/client/map.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as composables from './composables/index.js'
|
|
2
2
|
import * as commonMixins from './mixins/index.js'
|
|
3
3
|
import * as mapMixins from './mixins/map/index.js'
|
|
4
|
-
import * as utils from './utils.js'
|
|
4
|
+
import * as utils from './utils.map.js'
|
|
5
5
|
import init from './init.js'
|
|
6
6
|
|
|
7
7
|
const mixins = Object.assign({}, commonMixins, { map: mapMixins })
|
|
@@ -8,7 +8,7 @@ import Cesium from 'cesium/Source/Cesium.js'
|
|
|
8
8
|
import 'cesium/Source/Widgets/widgets.css'
|
|
9
9
|
import BuildModuleUrl from 'cesium/Source/Core/buildModuleUrl.js'
|
|
10
10
|
import { Geolocation } from '../../geolocation.js'
|
|
11
|
-
import { convertCesiumHandlerEvent } from '../../utils.js'
|
|
11
|
+
import { convertCesiumHandlerEvent } from '../../utils.globe.js'
|
|
12
12
|
// Cesium has its own dynamic module loader requiring to be configured
|
|
13
13
|
// Cesium files need to be also added as static assets of the applciation
|
|
14
14
|
BuildModuleUrl.setBaseUrl('/Cesium/')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Cesium from 'cesium/Source/Cesium.js'
|
|
2
2
|
import _ from 'lodash'
|
|
3
3
|
import chroma from 'chroma-js'
|
|
4
|
-
import { convertToCesiumFromSimpleStyleSpec, convertToCesiumObjects, CesiumStyleMappings, CesiumEntityTypes } from '../../utils.js'
|
|
4
|
+
import { convertToCesiumFromSimpleStyleSpec, convertToCesiumObjects, CesiumStyleMappings, CesiumEntityTypes } from '../../utils.globe.js'
|
|
5
5
|
|
|
6
6
|
export const style = {
|
|
7
7
|
methods: {
|
|
@@ -28,7 +28,7 @@ import { getAppLocale } from '../../../../core/client/utils/index.js'
|
|
|
28
28
|
import { uid } from 'quasar'
|
|
29
29
|
import '../../leaflet/BoxSelection.js'
|
|
30
30
|
import { Geolocation } from '../../geolocation.js'
|
|
31
|
-
import { LeafletEvents, bindLeafletEvents, generatePropertiesSchema } from '../../utils.js' // https://github.com/socib/Leaflet.TimeDimension/issues/124
|
|
31
|
+
import { LeafletEvents, bindLeafletEvents, generatePropertiesSchema } from '../../utils.map.js' // https://github.com/socib/Leaflet.TimeDimension/issues/124
|
|
32
32
|
|
|
33
33
|
import markerIcon from 'leaflet/dist/images/marker-icon.png'
|
|
34
34
|
import retinaIcon from 'leaflet/dist/images/marker-icon-2x.png'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
import { CanvasDrawContext } from '../../canvas-draw-context.js'
|
|
3
|
-
import { bindLeafletEvents } from '../../utils.js'
|
|
3
|
+
import { bindLeafletEvents } from '../../utils.map.js'
|
|
4
4
|
import L from 'leaflet'
|
|
5
5
|
|
|
6
6
|
// Helper function to forward events when click through is enabled
|
|
@@ -2,7 +2,7 @@ import _ from 'lodash'
|
|
|
2
2
|
import L from 'leaflet'
|
|
3
3
|
import { getType, getGeom } from '@turf/invariant'
|
|
4
4
|
import { Dialog, uid } from 'quasar'
|
|
5
|
-
import { bindLeafletEvents, unbindLeafletEvents } from '../../utils.js'
|
|
5
|
+
import { bindLeafletEvents, unbindLeafletEvents } from '../../utils.map.js'
|
|
6
6
|
|
|
7
7
|
// Events we listen while layer is in edition mode
|
|
8
8
|
const mapEditEvents = ['pm:create']
|
|
@@ -7,7 +7,7 @@ import { Time } from '../../../../core/client/time.js'
|
|
|
7
7
|
import { GradientPath } from '../../leaflet/GradientPath.js'
|
|
8
8
|
import { MaskLayer } from '../../leaflet/MaskLayer.js'
|
|
9
9
|
import { TiledFeatureLayer } from '../../leaflet/TiledFeatureLayer.js'
|
|
10
|
-
import { fetchGeoJson, LeafletEvents, bindLeafletEvents, unbindLeafletEvents, getFeatureId } from '../../utils.js'
|
|
10
|
+
import { fetchGeoJson, LeafletEvents, bindLeafletEvents, unbindLeafletEvents, getFeatureId } from '../../utils.map.js'
|
|
11
11
|
import * as wfs from '../../../common/wfs-utils.js'
|
|
12
12
|
|
|
13
13
|
// Override default remove handler for leaflet-realtime due to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import L from 'leaflet'
|
|
2
2
|
import _ from 'lodash'
|
|
3
3
|
import chroma from 'chroma-js'
|
|
4
|
-
import { createLeafletMarkerFromStyle, convertToLeafletFromSimpleStyleSpec, LeafletStyleMappings } from '../../utils.js'
|
|
4
|
+
import { createLeafletMarkerFromStyle, convertToLeafletFromSimpleStyleSpec, LeafletStyleMappings } from '../../utils.map.js'
|
|
5
5
|
|
|
6
6
|
export const style = {
|
|
7
7
|
methods: {
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import rhumbBearing from '@turf/rhumb-bearing'
|
|
3
|
+
import rhumbDistance from '@turf/rhumb-distance'
|
|
4
|
+
import rotate from '@turf/transform-rotate'
|
|
5
|
+
import scale from '@turf/transform-scale'
|
|
6
|
+
import translate from '@turf/transform-translate'
|
|
7
|
+
import chroma from 'chroma-js'
|
|
8
|
+
import config from 'config'
|
|
9
|
+
import formatcoords from 'formatcoords'
|
|
10
|
+
import { buildUrl } from '../../../core/common/index.js'
|
|
11
|
+
import { api, Store } from '../../../core/client/index.js'
|
|
12
|
+
|
|
13
|
+
// Build a color map from a JS object specification
|
|
14
|
+
export function buildColorMap (options) {
|
|
15
|
+
let colorMap
|
|
16
|
+
const classes = _.get(options, 'classes')
|
|
17
|
+
const domain = _.get(options, 'domain')
|
|
18
|
+
const scale = _.get(options, 'scale')
|
|
19
|
+
const invert = _.get(options, 'invertScale')
|
|
20
|
+
if (scale) {
|
|
21
|
+
if (classes) {
|
|
22
|
+
colorMap = chroma.scale(scale).classes(invert ? classes.slice().reverse() : classes)
|
|
23
|
+
} else if (domain) {
|
|
24
|
+
colorMap = chroma.scale(scale).domain(invert ? domain.slice().reverse() : domain)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return colorMap
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function transformFeatures (features, transform) {
|
|
31
|
+
features.forEach(feature => {
|
|
32
|
+
const scaling = _.get(transform, 'scale')
|
|
33
|
+
const rotation = _.get(transform, 'rotate')
|
|
34
|
+
const translation = _.get(transform, 'translate')
|
|
35
|
+
if (scaling) {
|
|
36
|
+
scale(feature, scaling.factor,
|
|
37
|
+
Object.assign(_.omit(scaling, ['factor']), { mutate: true }))
|
|
38
|
+
}
|
|
39
|
+
if (rotation) {
|
|
40
|
+
rotate(feature, rotation.angle,
|
|
41
|
+
Object.assign(_.omit(rotation, ['angle']), { mutate: true }))
|
|
42
|
+
}
|
|
43
|
+
if (translation) {
|
|
44
|
+
// Could be expressed as direction/distance or target point
|
|
45
|
+
// Take care that turfjs uses a rhumb line
|
|
46
|
+
if (translation.point) {
|
|
47
|
+
translation.distance = rhumbDistance(translation.pivot || [0, 0], translation.point)
|
|
48
|
+
translation.direction = rhumbBearing(translation.pivot || [0, 0], translation.point)
|
|
49
|
+
delete translation.pivot
|
|
50
|
+
delete translation.point
|
|
51
|
+
}
|
|
52
|
+
translate(feature, translation.distance, translation.direction,
|
|
53
|
+
Object.assign(_.omit(translation, ['direction', 'distance']), { mutate: true }))
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function fetchGeoJson (dataSource, options = {}) {
|
|
59
|
+
const response = await fetch(dataSource)
|
|
60
|
+
if (response.status !== 200) {
|
|
61
|
+
throw new Error(`Impossible to fetch ${dataSource}: ` + response.status)
|
|
62
|
+
}
|
|
63
|
+
const data = await response.json()
|
|
64
|
+
const features = (data.type === 'FeatureCollection' ? data.features : [data])
|
|
65
|
+
if (typeof options.processor === 'function') {
|
|
66
|
+
features.forEach(feature => options.processor(feature))
|
|
67
|
+
} else if (typeof options.processor === 'string') {
|
|
68
|
+
const compiler = _.template(options.processor)
|
|
69
|
+
features.forEach(feature => compiler({ feature, properties: feature.properties }))
|
|
70
|
+
}
|
|
71
|
+
if (options.transform) {
|
|
72
|
+
transformFeatures(features, options.transform)
|
|
73
|
+
}
|
|
74
|
+
return data
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Find the nearest time of a given one in a given moment time list
|
|
78
|
+
export function getNearestTime (time, times) {
|
|
79
|
+
// Look for the nearest time
|
|
80
|
+
let timeIndex = -1
|
|
81
|
+
let minDiff = Infinity
|
|
82
|
+
times.forEach((currentTime, index) => {
|
|
83
|
+
const diff = Math.abs(time.diff(currentTime))
|
|
84
|
+
if (diff < minDiff) {
|
|
85
|
+
minDiff = diff
|
|
86
|
+
timeIndex = index
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
return { index: timeIndex, difference: minDiff }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Find the minimum or maximum time interval in a given moment time list
|
|
93
|
+
export function getTimeInterval (times, mode = 'minimum') {
|
|
94
|
+
// Look for the nearest time
|
|
95
|
+
let interval = (mode === 'minimum' ? Infinity : 0)
|
|
96
|
+
times.forEach((currentTime, index) => {
|
|
97
|
+
if (index < (times.length - 1)) {
|
|
98
|
+
const diff = Math.abs(currentTime.diff(times[index + 1]))
|
|
99
|
+
if (mode === 'minimum') {
|
|
100
|
+
if (diff < interval) interval = diff
|
|
101
|
+
} else {
|
|
102
|
+
if (diff > interval) interval = diff
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
return interval
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Format (reverse) geocoding output
|
|
110
|
+
export function formatGeocodingResult (element) {
|
|
111
|
+
let label = element.formattedAddress || ''
|
|
112
|
+
if (!label) {
|
|
113
|
+
if (element.streetNumber) label += (element.streetNumber + ', ')
|
|
114
|
+
if (element.streetName) label += (element.streetName + ' ')
|
|
115
|
+
if (element.city) label += (element.city + ' ')
|
|
116
|
+
if (element.zipcode) label += (' (' + element.zipcode + ')')
|
|
117
|
+
}
|
|
118
|
+
return label
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Helper to set a JWT as query param in a target URL
|
|
122
|
+
export function setUrlJwt (item, path, baseUrl, jwtField, jwt) {
|
|
123
|
+
const url = _.get(item, path)
|
|
124
|
+
if (!url) return
|
|
125
|
+
// Check it conforms to required base URL
|
|
126
|
+
if (!url.startsWith(baseUrl)) return
|
|
127
|
+
// FIXME: specific case of Cesium OpenStreetMap provider
|
|
128
|
+
// Because Cesium generates the final url as base url + tile scheme + extension
|
|
129
|
+
// updating the base url property breaks it, for now we modify the extension
|
|
130
|
+
if ((path === 'cesium.url') && _.get(item, 'cesium.type') === 'OpenStreetMap') {
|
|
131
|
+
const ext = _.get(item, 'cesium.fileExtension', 'png')
|
|
132
|
+
_.set(item, 'cesium.fileExtension', ext + `?${jwtField}=${jwt}`)
|
|
133
|
+
} else {
|
|
134
|
+
_.set(item, path, buildUrl(url, { [jwtField]: jwt }))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Helper to set required JWT as query param in a given set of layers for underlying engine
|
|
139
|
+
export async function setEngineJwt (layers) {
|
|
140
|
+
// If we need to use API gateway forward token as query parameter
|
|
141
|
+
// (Leaflet does not support anything else by default as it mainly uses raw <img> tags)
|
|
142
|
+
let jwt = (config.gatewayJwt ? await api.get('storage').getItem(config.gatewayJwt) : null)
|
|
143
|
+
let jwtField = config.gatewayJwtField
|
|
144
|
+
// Check both the default built-in config or the server provided one if any (eg mobile apps)
|
|
145
|
+
const gatewayUrl = Store.get('capabilities.api.gateway', config.gateway)
|
|
146
|
+
if (jwt) {
|
|
147
|
+
layers.forEach(layer => {
|
|
148
|
+
setUrlJwt(layer, 'iconUrl', gatewayUrl, jwtField, jwt)
|
|
149
|
+
setUrlJwt(layer, 'leaflet.source', gatewayUrl, jwtField, jwt)
|
|
150
|
+
setUrlJwt(layer, 'opendap.url', gatewayUrl, jwtField, jwt)
|
|
151
|
+
setUrlJwt(layer, 'geotiff.url', gatewayUrl, jwtField, jwt)
|
|
152
|
+
setUrlJwt(layer, 'wfs.url', gatewayUrl, jwtField, jwt)
|
|
153
|
+
setUrlJwt(layer, 'wcs.url', gatewayUrl, jwtField, jwt)
|
|
154
|
+
setUrlJwt(layer, 'cesium.url', gatewayUrl, jwtField, jwt)
|
|
155
|
+
setUrlJwt(layer, 'cesium.source', gatewayUrl, jwtField, jwt)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
// We might also proxy some data directly from the app when using object storage
|
|
159
|
+
// This is only for raw raster data not OGC protocols
|
|
160
|
+
jwt = (config.apiJwt ? await api.get('storage').getItem(config.apiJwt) : null)
|
|
161
|
+
jwtField = 'jwt'
|
|
162
|
+
const apiUrl = api.getBaseUrl()
|
|
163
|
+
if (jwt) {
|
|
164
|
+
layers.forEach(layer => {
|
|
165
|
+
setUrlJwt(layer, 'geotiff.url', apiUrl, jwtField, jwt)
|
|
166
|
+
})
|
|
167
|
+
// We also allow absolute URLs for app like /api/storage/xxx
|
|
168
|
+
layers.forEach(layer => {
|
|
169
|
+
setUrlJwt(layer, 'geotiff.url', '/', jwtField, jwt)
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
return layers
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function getFeatureId (feature, layer) {
|
|
176
|
+
let featureId = _.get(layer, 'featureId')
|
|
177
|
+
// We need at least an internal ID to uniquely identify features for updates
|
|
178
|
+
if (!featureId) featureId = '_id'
|
|
179
|
+
// Support compound index
|
|
180
|
+
featureId = (Array.isArray(featureId) ? featureId : [featureId])
|
|
181
|
+
return featureId.map(id => _.get(feature, 'properties.' + id, _.get(feature, id))).join('-')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function formatUserCoordinates (lat, lon, format, options) {
|
|
185
|
+
if (format === 'aeronautical') {
|
|
186
|
+
const coords = formatcoords(lat, lon)
|
|
187
|
+
// longitude group: DDMMML where DD is degree (2 digits mandatory)
|
|
188
|
+
// MMM unit is in 0.1 minutes (trailing 0 optional)
|
|
189
|
+
// L is N/S
|
|
190
|
+
const latDeg = coords.latValues.degreesInt.toString().padStart(2, '0')
|
|
191
|
+
const latMin = Math.floor(coords.latValues.secondsTotal / 6).toString().padStart(3, '0')
|
|
192
|
+
const latDir = coords.north ? 'N' : 'S'
|
|
193
|
+
// longitude group: DDDMMML where DDD is degree (3 digits mandatory)
|
|
194
|
+
// MMM unit is in 0.1 minutes (trailing 0 optional)
|
|
195
|
+
// L is W/E
|
|
196
|
+
const lonDeg = coords.lonValues.degreesInt.toString().padStart(3, '0')
|
|
197
|
+
const lonMin = Math.floor(coords.lonValues.secondsTotal / 6).toString().padStart(3, '0')
|
|
198
|
+
const lonDir = coords.east ? 'E' : 'W'
|
|
199
|
+
return `${latDeg}${latMin}${latDir} ${lonDeg}${lonMin}${lonDir}`
|
|
200
|
+
}
|
|
201
|
+
return formatcoords(lat, lon).format(format, options)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function coordinatesToGeoJSON (lat, lon, format, options) {
|
|
205
|
+
return {
|
|
206
|
+
type: 'Feature',
|
|
207
|
+
geometry: {
|
|
208
|
+
type: 'Point',
|
|
209
|
+
coordinates: [lon, lat]
|
|
210
|
+
},
|
|
211
|
+
properties: {
|
|
212
|
+
name: formatcoords(lat, lon).format(format, options)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function parseCoordinates (str) {
|
|
218
|
+
const coords = _.split(_.trim(str), ',')
|
|
219
|
+
if (coords.length !== 2) return
|
|
220
|
+
const latitude = Number(coords[0])
|
|
221
|
+
if (_.isNaN(latitude)) return
|
|
222
|
+
const longitude = Number(coords[1])
|
|
223
|
+
if (_.isNaN(longitude)) return
|
|
224
|
+
return {
|
|
225
|
+
latitude,
|
|
226
|
+
longitude
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Store, api } from '../../../core.client.js'
|
|
2
|
-
import { formatGeocodingResult, parseCoordinates, formatUserCoordinates } from '
|
|
2
|
+
import { formatGeocodingResult, parseCoordinates, formatUserCoordinates } from './utils.js'
|
|
3
3
|
|
|
4
4
|
export async function searchLocation (pattern, options) {
|
|
5
5
|
const locations = []
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { utils as kCoreUtils } from '../../../core/client/index.js'
|
|
3
|
+
|
|
4
|
+
// Get JSON schema from GeoJson feature' properties
|
|
5
|
+
export function generatePropertiesSchema (geoJson, name) {
|
|
6
|
+
const schema = {
|
|
7
|
+
$id: `http://www.kalisio.xyz/schemas/${_.kebabCase(name)}#`,
|
|
8
|
+
title: name,
|
|
9
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Enumerate all available properties/values in all features
|
|
15
|
+
const features = (geoJson.type === 'FeatureCollection' ? geoJson.features : [geoJson])
|
|
16
|
+
features.forEach(feature => {
|
|
17
|
+
// FIXME: we don't yet support nested objects in schema
|
|
18
|
+
const properties = (feature.properties ? kCoreUtils.dotify(feature.properties) : {})
|
|
19
|
+
_.forOwn(properties, (value, key) => {
|
|
20
|
+
// Property already registered ?
|
|
21
|
+
if (schema.properties['{key}']) {
|
|
22
|
+
const property = schema.properties[`${key}`]
|
|
23
|
+
// Try to find first non void value to select appropriate type
|
|
24
|
+
if (_.isNil(property)) schema.properties[`${key}`] = value
|
|
25
|
+
} else {
|
|
26
|
+
schema.properties[`${key}`] = value
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
_.forOwn(schema.properties, (value, key) => {
|
|
31
|
+
let type = (typeof value)
|
|
32
|
+
// For null/undefined value we will assume string by default
|
|
33
|
+
if ((type === 'object') || (type === 'undefined')) type = 'string'
|
|
34
|
+
schema.properties[`${key}`] = {
|
|
35
|
+
type,
|
|
36
|
+
nullable: true,
|
|
37
|
+
field: {
|
|
38
|
+
component: (type === 'number'
|
|
39
|
+
? 'form/KNumberField'
|
|
40
|
+
: (type === 'boolean' ? 'form/KToggleField' : 'form/KTextField')),
|
|
41
|
+
helper: key,
|
|
42
|
+
label: key
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
return schema
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function updatePropertiesSchema (schema) {
|
|
50
|
+
const props = schema.properties
|
|
51
|
+
if (!props) return
|
|
52
|
+
|
|
53
|
+
const bestGuesses = {
|
|
54
|
+
undefined: 'form/KTextField',
|
|
55
|
+
object: 'form/KTextField',
|
|
56
|
+
string: 'form/KTextField',
|
|
57
|
+
number: 'form/KNumberField',
|
|
58
|
+
boolean: 'form/KToggleField'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Loop over declared props and add best guesses to field components based on property type
|
|
62
|
+
for (const prop in props) {
|
|
63
|
+
const propEntry = props[prop]
|
|
64
|
+
// Field already here, skip entry
|
|
65
|
+
if (propEntry.field && propEntry.field.component) continue
|
|
66
|
+
|
|
67
|
+
propEntry.field = {
|
|
68
|
+
component: bestGuesses[propEntry.type],
|
|
69
|
+
label: prop,
|
|
70
|
+
helper: propEntry.description
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return schema
|
|
75
|
+
}
|
package/map/client/utils.js
CHANGED
|
@@ -1,303 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import scale from '@turf/transform-scale'
|
|
6
|
-
import translate from '@turf/transform-translate'
|
|
7
|
-
import chroma from 'chroma-js'
|
|
8
|
-
import config from 'config'
|
|
9
|
-
import formatcoords from 'formatcoords'
|
|
10
|
-
import { buildUrl } from '../../core/common/index.js'
|
|
11
|
-
import { api, Store, utils as kCoreUtils } from '../../core/client/index.js'
|
|
12
|
-
export * from './leaflet/utils.js'
|
|
13
|
-
export * from './cesium/utils.js'
|
|
14
|
-
|
|
15
|
-
// Build a color map from a JS object specification
|
|
16
|
-
export function buildColorMap (options) {
|
|
17
|
-
let colorMap
|
|
18
|
-
const classes = _.get(options, 'classes')
|
|
19
|
-
const domain = _.get(options, 'domain')
|
|
20
|
-
const scale = _.get(options, 'scale')
|
|
21
|
-
const invert = _.get(options, 'invertScale')
|
|
22
|
-
if (scale) {
|
|
23
|
-
if (classes) {
|
|
24
|
-
colorMap = chroma.scale(scale).classes(invert ? classes.slice().reverse() : classes)
|
|
25
|
-
} else if (domain) {
|
|
26
|
-
colorMap = chroma.scale(scale).domain(invert ? domain.slice().reverse() : domain)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return colorMap
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function transformFeatures (features, transform) {
|
|
33
|
-
features.forEach(feature => {
|
|
34
|
-
const scaling = _.get(transform, 'scale')
|
|
35
|
-
const rotation = _.get(transform, 'rotate')
|
|
36
|
-
const translation = _.get(transform, 'translate')
|
|
37
|
-
if (scaling) {
|
|
38
|
-
scale(feature, scaling.factor,
|
|
39
|
-
Object.assign(_.omit(scaling, ['factor']), { mutate: true }))
|
|
40
|
-
}
|
|
41
|
-
if (rotation) {
|
|
42
|
-
rotate(feature, rotation.angle,
|
|
43
|
-
Object.assign(_.omit(rotation, ['angle']), { mutate: true }))
|
|
44
|
-
}
|
|
45
|
-
if (translation) {
|
|
46
|
-
// Could be expressed as direction/distance or target point
|
|
47
|
-
// Take care that turfjs uses a rhumb line
|
|
48
|
-
if (translation.point) {
|
|
49
|
-
translation.distance = rhumbDistance(translation.pivot || [0, 0], translation.point)
|
|
50
|
-
translation.direction = rhumbBearing(translation.pivot || [0, 0], translation.point)
|
|
51
|
-
delete translation.pivot
|
|
52
|
-
delete translation.point
|
|
53
|
-
}
|
|
54
|
-
translate(feature, translation.distance, translation.direction,
|
|
55
|
-
Object.assign(_.omit(translation, ['direction', 'distance']), { mutate: true }))
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function fetchGeoJson (dataSource, options = {}) {
|
|
61
|
-
const response = await fetch(dataSource)
|
|
62
|
-
if (response.status !== 200) {
|
|
63
|
-
throw new Error(`Impossible to fetch ${dataSource}: ` + response.status)
|
|
64
|
-
}
|
|
65
|
-
const data = await response.json()
|
|
66
|
-
const features = (data.type === 'FeatureCollection' ? data.features : [data])
|
|
67
|
-
if (typeof options.processor === 'function') {
|
|
68
|
-
features.forEach(feature => options.processor(feature))
|
|
69
|
-
} else if (typeof options.processor === 'string') {
|
|
70
|
-
const compiler = _.template(options.processor)
|
|
71
|
-
features.forEach(feature => compiler({ feature, properties: feature.properties }))
|
|
72
|
-
}
|
|
73
|
-
if (options.transform) {
|
|
74
|
-
transformFeatures(features, options.transform)
|
|
75
|
-
}
|
|
76
|
-
return data
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Find the nearest time of a given one in a given moment time list
|
|
80
|
-
export function getNearestTime (time, times) {
|
|
81
|
-
// Look for the nearest time
|
|
82
|
-
let timeIndex = -1
|
|
83
|
-
let minDiff = Infinity
|
|
84
|
-
times.forEach((currentTime, index) => {
|
|
85
|
-
const diff = Math.abs(time.diff(currentTime))
|
|
86
|
-
if (diff < minDiff) {
|
|
87
|
-
minDiff = diff
|
|
88
|
-
timeIndex = index
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
return { index: timeIndex, difference: minDiff }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Find the minimum or maximum time interval in a given moment time list
|
|
95
|
-
export function getTimeInterval (times, mode = 'minimum') {
|
|
96
|
-
// Look for the nearest time
|
|
97
|
-
let interval = (mode === 'minimum' ? Infinity : 0)
|
|
98
|
-
times.forEach((currentTime, index) => {
|
|
99
|
-
if (index < (times.length - 1)) {
|
|
100
|
-
const diff = Math.abs(currentTime.diff(times[index + 1]))
|
|
101
|
-
if (mode === 'minimum') {
|
|
102
|
-
if (diff < interval) interval = diff
|
|
103
|
-
} else {
|
|
104
|
-
if (diff > interval) interval = diff
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
return interval
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Format (reverse) geocoding output
|
|
112
|
-
export function formatGeocodingResult (element) {
|
|
113
|
-
let label = element.formattedAddress || ''
|
|
114
|
-
if (!label) {
|
|
115
|
-
if (element.streetNumber) label += (element.streetNumber + ', ')
|
|
116
|
-
if (element.streetName) label += (element.streetName + ' ')
|
|
117
|
-
if (element.city) label += (element.city + ' ')
|
|
118
|
-
if (element.zipcode) label += (' (' + element.zipcode + ')')
|
|
119
|
-
}
|
|
120
|
-
return label
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Helper to set a JWT as query param in a target URL
|
|
124
|
-
export function setUrlJwt (item, path, baseUrl, jwtField, jwt) {
|
|
125
|
-
const url = _.get(item, path)
|
|
126
|
-
if (!url) return
|
|
127
|
-
// Check it conforms to required base URL
|
|
128
|
-
if (!url.startsWith(baseUrl)) return
|
|
129
|
-
// FIXME: specific case of Cesium OpenStreetMap provider
|
|
130
|
-
// Because Cesium generates the final url as base url + tile scheme + extension
|
|
131
|
-
// updating the base url property breaks it, for now we modify the extension
|
|
132
|
-
if ((path === 'cesium.url') && _.get(item, 'cesium.type') === 'OpenStreetMap') {
|
|
133
|
-
const ext = _.get(item, 'cesium.fileExtension', 'png')
|
|
134
|
-
_.set(item, 'cesium.fileExtension', ext + `?${jwtField}=${jwt}`)
|
|
135
|
-
} else {
|
|
136
|
-
_.set(item, path, buildUrl(url, { [jwtField]: jwt }))
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Helper to set required JWT as query param in a given set of layers for underlying engine
|
|
141
|
-
export async function setEngineJwt (layers) {
|
|
142
|
-
// If we need to use API gateway forward token as query parameter
|
|
143
|
-
// (Leaflet does not support anything else by default as it mainly uses raw <img> tags)
|
|
144
|
-
let jwt = (config.gatewayJwt ? await api.get('storage').getItem(config.gatewayJwt) : null)
|
|
145
|
-
let jwtField = config.gatewayJwtField
|
|
146
|
-
// Check both the default built-in config or the server provided one if any (eg mobile apps)
|
|
147
|
-
const gatewayUrl = Store.get('capabilities.api.gateway', config.gateway)
|
|
148
|
-
if (jwt) {
|
|
149
|
-
layers.forEach(layer => {
|
|
150
|
-
setUrlJwt(layer, 'iconUrl', gatewayUrl, jwtField, jwt)
|
|
151
|
-
setUrlJwt(layer, 'leaflet.source', gatewayUrl, jwtField, jwt)
|
|
152
|
-
setUrlJwt(layer, 'opendap.url', gatewayUrl, jwtField, jwt)
|
|
153
|
-
setUrlJwt(layer, 'geotiff.url', gatewayUrl, jwtField, jwt)
|
|
154
|
-
setUrlJwt(layer, 'wfs.url', gatewayUrl, jwtField, jwt)
|
|
155
|
-
setUrlJwt(layer, 'wcs.url', gatewayUrl, jwtField, jwt)
|
|
156
|
-
setUrlJwt(layer, 'cesium.url', gatewayUrl, jwtField, jwt)
|
|
157
|
-
setUrlJwt(layer, 'cesium.source', gatewayUrl, jwtField, jwt)
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
// We might also proxy some data directly from the app when using object storage
|
|
161
|
-
// This is only for raw raster data not OGC protocols
|
|
162
|
-
jwt = (config.apiJwt ? await api.get('storage').getItem(config.apiJwt) : null)
|
|
163
|
-
jwtField = 'jwt'
|
|
164
|
-
const apiUrl = api.getBaseUrl()
|
|
165
|
-
if (jwt) {
|
|
166
|
-
layers.forEach(layer => {
|
|
167
|
-
setUrlJwt(layer, 'geotiff.url', apiUrl, jwtField, jwt)
|
|
168
|
-
})
|
|
169
|
-
// We also allow absolute URLs for app like /api/storage/xxx
|
|
170
|
-
layers.forEach(layer => {
|
|
171
|
-
setUrlJwt(layer, 'geotiff.url', '/', jwtField, jwt)
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
return layers
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function getFeatureId (feature, layer) {
|
|
178
|
-
let featureId = _.get(layer, 'featureId')
|
|
179
|
-
// We need at least an internal ID to uniquely identify features for updates
|
|
180
|
-
if (!featureId) featureId = '_id'
|
|
181
|
-
// Support compound index
|
|
182
|
-
featureId = (Array.isArray(featureId) ? featureId : [featureId])
|
|
183
|
-
return featureId.map(id => _.get(feature, 'properties.' + id, _.get(feature, id))).join('-')
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Get JSON schema from GeoJson feature' properties
|
|
187
|
-
export function generatePropertiesSchema (geoJson, name) {
|
|
188
|
-
const schema = {
|
|
189
|
-
$id: `http://www.kalisio.xyz/schemas/${_.kebabCase(name)}#`,
|
|
190
|
-
title: name,
|
|
191
|
-
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
192
|
-
type: 'object',
|
|
193
|
-
properties: {
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// Enumerate all available properties/values in all features
|
|
197
|
-
const features = (geoJson.type === 'FeatureCollection' ? geoJson.features : [geoJson])
|
|
198
|
-
features.forEach(feature => {
|
|
199
|
-
// FIXME: we don't yet support nested objects in schema
|
|
200
|
-
const properties = (feature.properties ? kCoreUtils.dotify(feature.properties) : {})
|
|
201
|
-
_.forOwn(properties, (value, key) => {
|
|
202
|
-
// Property already registered ?
|
|
203
|
-
if (schema.properties['{key}']) {
|
|
204
|
-
const property = schema.properties[`${key}`]
|
|
205
|
-
// Try to find first non void value to select appropriate type
|
|
206
|
-
if (_.isNil(property)) schema.properties[`${key}`] = value
|
|
207
|
-
} else {
|
|
208
|
-
schema.properties[`${key}`] = value
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
_.forOwn(schema.properties, (value, key) => {
|
|
213
|
-
let type = (typeof value)
|
|
214
|
-
// For null/undefined value we will assume string by default
|
|
215
|
-
if ((type === 'object') || (type === 'undefined')) type = 'string'
|
|
216
|
-
schema.properties[`${key}`] = {
|
|
217
|
-
type,
|
|
218
|
-
nullable: true,
|
|
219
|
-
field: {
|
|
220
|
-
component: (type === 'number'
|
|
221
|
-
? 'form/KNumberField'
|
|
222
|
-
: (type === 'boolean' ? 'form/KToggleField' : 'form/KTextField')),
|
|
223
|
-
helper: key,
|
|
224
|
-
label: key
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
})
|
|
228
|
-
return schema
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function updatePropertiesSchema (schema) {
|
|
232
|
-
const props = schema.properties
|
|
233
|
-
if (!props) return
|
|
234
|
-
|
|
235
|
-
const bestGuesses = {
|
|
236
|
-
undefined: 'form/KTextField',
|
|
237
|
-
object: 'form/KTextField',
|
|
238
|
-
string: 'form/KTextField',
|
|
239
|
-
number: 'form/KNumberField',
|
|
240
|
-
boolean: 'form/KToggleField'
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Loop over declared props and add best guesses to field components based on property type
|
|
244
|
-
for (const prop in props) {
|
|
245
|
-
const propEntry = props[prop]
|
|
246
|
-
// Field already here, skip entry
|
|
247
|
-
if (propEntry.field && propEntry.field.component) continue
|
|
248
|
-
|
|
249
|
-
propEntry.field = {
|
|
250
|
-
component: bestGuesses[propEntry.type],
|
|
251
|
-
label: prop,
|
|
252
|
-
helper: propEntry.description
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return schema
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function formatUserCoordinates (lat, lon, format, options) {
|
|
260
|
-
if (format === 'aeronautical') {
|
|
261
|
-
const coords = formatcoords(lat, lon)
|
|
262
|
-
// longitude group: DDMMML where DD is degree (2 digits mandatory)
|
|
263
|
-
// MMM unit is in 0.1 minutes (trailing 0 optional)
|
|
264
|
-
// L is N/S
|
|
265
|
-
const latDeg = coords.latValues.degreesInt.toString().padStart(2, '0')
|
|
266
|
-
const latMin = Math.floor(coords.latValues.secondsTotal / 6).toString().padStart(3, '0')
|
|
267
|
-
const latDir = coords.north ? 'N' : 'S'
|
|
268
|
-
// longitude group: DDDMMML where DDD is degree (3 digits mandatory)
|
|
269
|
-
// MMM unit is in 0.1 minutes (trailing 0 optional)
|
|
270
|
-
// L is W/E
|
|
271
|
-
const lonDeg = coords.lonValues.degreesInt.toString().padStart(3, '0')
|
|
272
|
-
const lonMin = Math.floor(coords.lonValues.secondsTotal / 6).toString().padStart(3, '0')
|
|
273
|
-
const lonDir = coords.east ? 'E' : 'W'
|
|
274
|
-
return `${latDeg}${latMin}${latDir} ${lonDeg}${lonMin}${lonDir}`
|
|
275
|
-
}
|
|
276
|
-
return formatcoords(lat, lon).format(format, options)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function coordinatesToGeoJSON (lat, lon, format, options) {
|
|
280
|
-
return {
|
|
281
|
-
type: 'Feature',
|
|
282
|
-
geometry: {
|
|
283
|
-
type: 'Point',
|
|
284
|
-
coordinates: [lon, lat]
|
|
285
|
-
},
|
|
286
|
-
properties: {
|
|
287
|
-
name: formatcoords(lat, lon).format(format, options)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function parseCoordinates (str) {
|
|
293
|
-
const coords = _.split(_.trim(str), ',')
|
|
294
|
-
if (coords.length !== 2) return
|
|
295
|
-
const latitude = Number(coords[0])
|
|
296
|
-
if (_.isNaN(latitude)) return
|
|
297
|
-
const longitude = Number(coords[1])
|
|
298
|
-
if (_.isNaN(longitude)) return
|
|
299
|
-
return {
|
|
300
|
-
latitude,
|
|
301
|
-
longitude
|
|
302
|
-
}
|
|
303
|
-
}
|
|
1
|
+
export * from './utils/index.js'
|
|
2
|
+
// Now in separated files to avoid mixin Leaflet/Cesium elements
|
|
3
|
+
// export * from './leaflet/utils.js'
|
|
4
|
+
// export * from './cesium/utils.js'
|
package/package.json
CHANGED
|
@@ -52,6 +52,24 @@ export async function clickLayer (page, tabId, layer, wait = 1000) {
|
|
|
52
52
|
await page.waitForTimeout(wait)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export async function zoomToLayer (page, tabId, layer, wait = 1000) {
|
|
56
|
+
const isCatalogOpened = await clickCatalogTab(page, tabId)
|
|
57
|
+
const layerId = getLayerId(layer)
|
|
58
|
+
const categoryId = await getLayerCategoryId(page, layerId)
|
|
59
|
+
let isCategoryOpened
|
|
60
|
+
if (categoryId) {
|
|
61
|
+
isCategoryOpened = await isLayerCategoryOpened(page, categoryId)
|
|
62
|
+
if (!isCategoryOpened) await core.clickPaneAction(page, 'right', categoryId, 1000)
|
|
63
|
+
}
|
|
64
|
+
await core.click(page, `#${layer}-actions`)
|
|
65
|
+
await core.clickAction(page, 'zoom-to-layer')
|
|
66
|
+
if (categoryId) {
|
|
67
|
+
if (!isCategoryOpened) await core.clickPaneAction(page, 'right', categoryId, 500)
|
|
68
|
+
}
|
|
69
|
+
if (!isCatalogOpened) await core.clickOpener(page, 'right')
|
|
70
|
+
await page.waitForTimeout(wait)
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
export async function saveLayer (page, tabId, layer, wait = 1000) {
|
|
56
74
|
const isCatalogOpened = await clickCatalogTab(page, tabId)
|
|
57
75
|
const layerId = getLayerId(layer)
|