geonetwork-ui 2.11.0-dev.e7b8ec331 → 2.11.0-dev.f5ee66b24
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/fesm2022/geonetwork-ui.mjs +1482 -928
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +280 -124
- package/index.d.ts.map +1 -1
- package/package.json +12 -11
- package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +43 -13
- package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +108 -34
- package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
- package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
- package/src/libs/common/domain/src/lib/model/search/filter.model.ts +20 -1
- package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
- package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
- package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
- package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
- package/src/libs/feature/map/src/index.ts +0 -1
- package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
- package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
- package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
- package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
- package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
- package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
- package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
- package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
- package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
- package/src/libs/feature/search/src/index.ts +2 -0
- package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
- package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
- package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +5 -1
- package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
- package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
- package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +18 -8
- package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +9 -1
- package/src/libs/feature/search/src/lib/utils/service/fields.ts +61 -2
- package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
- package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
- package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
- package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
- package/src/libs/ui/inputs/src/index.ts +2 -0
- package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
- package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
- package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
- package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
- package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
- package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
- package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
- package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
- package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
- package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
- package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
- package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
- package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
- package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
- package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
- package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
- package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
- package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
- package/src/libs/util/app-config/src/lib/model.ts +1 -0
- package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
- package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
- package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
- package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
- package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
- package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
- package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
- package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
- package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
- package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
- package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
- package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
- package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
- package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
- package/src/libs/util/shared/src/index.ts +1 -0
- package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
- package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
- package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
- package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
- package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
- package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
- package/tailwind.base.css +5 -0
- package/translations/de.json +17 -3
- package/translations/en.json +16 -2
- package/translations/es.json +15 -1
- package/translations/fr.json +17 -3
- package/translations/it.json +15 -1
- package/translations/nl.json +15 -1
- package/translations/pt.json +15 -1
- package/translations/sk.json +15 -1
- package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
- package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
- package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
- /package/src/libs/{feature/map/src/lib/geocoding/geocoding.component.css → ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css} +0 -0
|
@@ -1,54 +1,10 @@
|
|
|
1
1
|
import { BaseFileReader } from './base-file'
|
|
2
|
-
import { DataItem, PropertyInfo } from '../model'
|
|
3
|
-
import { fetchDataAsText, processItemProperties } from '../utils'
|
|
4
|
-
import { GeoJSON, WFS } from 'ol/format.js'
|
|
5
|
-
import { WfsVersion } from '@camptocamp/ogc-client'
|
|
6
|
-
|
|
7
|
-
export function parseGml(
|
|
8
|
-
text: string,
|
|
9
|
-
namespace: string,
|
|
10
|
-
version: string
|
|
11
|
-
): {
|
|
12
|
-
items: DataItem[]
|
|
13
|
-
properties: PropertyInfo[]
|
|
14
|
-
} {
|
|
15
|
-
const splittedNamespace = namespace.split(':')
|
|
16
|
-
const regex = new RegExp(`xmlns:${splittedNamespace[0]}=["']([^'"]*)["']`)
|
|
17
|
-
const match = regex.exec(text)
|
|
18
|
-
|
|
19
|
-
if (match && match.length >= 2) {
|
|
20
|
-
const wf = new WFS({
|
|
21
|
-
featureNS: match[1],
|
|
22
|
-
featureType: splittedNamespace[1],
|
|
23
|
-
version: version,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
let features
|
|
27
|
-
try {
|
|
28
|
-
features = wf.readFeatures(text)
|
|
29
|
-
} catch (e) {
|
|
30
|
-
throw Error("Couldn't parse WFS with GML features")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const geojsonItem = new GeoJSON().writeFeaturesObject(features)
|
|
34
|
-
return processItemProperties(geojsonItem.features, true)
|
|
35
|
-
}
|
|
36
|
-
throw Error("Couldn't retrieve namespace url")
|
|
37
|
-
}
|
|
38
2
|
|
|
39
3
|
export class GmlReader extends BaseFileReader {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
) {
|
|
46
|
-
super(url)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
protected getData() {
|
|
50
|
-
return fetchDataAsText(this.url, this.cacheActive).then((text) =>
|
|
51
|
-
parseGml(text, this.namespace, this.version)
|
|
52
|
-
)
|
|
4
|
+
async getLoadQuery() {
|
|
5
|
+
return `
|
|
6
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM st_read("${this.url}",
|
|
7
|
+
allowed_drivers = ['GML']
|
|
8
|
+
);`
|
|
53
9
|
}
|
|
54
10
|
}
|
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
import { DataItem, PropertyInfo } from '../model'
|
|
2
|
-
import {
|
|
3
|
-
fetchDataAsText,
|
|
4
|
-
jsonToGeojsonFeature,
|
|
5
|
-
processItemProperties,
|
|
6
|
-
} from '../utils'
|
|
7
1
|
import { BaseFileReader } from './base-file'
|
|
8
2
|
|
|
9
|
-
/**
|
|
10
|
-
* This parser only supports arrays of simple flat objects with properties
|
|
11
|
-
* @param text
|
|
12
|
-
*/
|
|
13
|
-
export function parseJson(text: string): {
|
|
14
|
-
items: DataItem[]
|
|
15
|
-
properties: PropertyInfo[]
|
|
16
|
-
} {
|
|
17
|
-
const parsed = JSON.parse(text) as any[]
|
|
18
|
-
if (!Array.isArray(parsed)) {
|
|
19
|
-
throw new Error('Could not parse JSON, expected an array at root level')
|
|
20
|
-
}
|
|
21
|
-
return processItemProperties(parsed.map(jsonToGeojsonFeature))
|
|
22
|
-
}
|
|
23
|
-
|
|
24
3
|
export class JsonReader extends BaseFileReader {
|
|
25
|
-
|
|
26
|
-
return
|
|
4
|
+
async getLoadQuery() {
|
|
5
|
+
return `
|
|
6
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM read_json("${this.url}",
|
|
7
|
+
maximum_object_size = 536870912 -- 500MB
|
|
8
|
+
);`
|
|
27
9
|
}
|
|
28
10
|
}
|
|
@@ -1,14 +1,58 @@
|
|
|
1
|
-
import { EndpointError, WfsEndpoint, WfsVersion } from '@camptocamp/ogc-client'
|
|
2
|
-
import { DataItem, DatasetInfo, FetchError, PropertyInfo } from '../model'
|
|
3
1
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from '
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
2
|
+
EndpointError,
|
|
3
|
+
WfsEndpoint,
|
|
4
|
+
WfsFeatureTypeFull,
|
|
5
|
+
} from '@camptocamp/ogc-client'
|
|
6
|
+
import { DataItem, DatasetInfo, FetchError, PropertyInfo } from '../model'
|
|
7
|
+
import { fetchDataAsText } from '../utils'
|
|
8
|
+
import WFS from 'ol/format/WFS'
|
|
9
|
+
import GeoJSON from 'ol/format/GeoJSON'
|
|
10
|
+
import Feature from 'ol/Feature'
|
|
11
|
+
import { GeojsonReader } from './geojson'
|
|
12
|
+
import { GmlReader } from './gml'
|
|
13
|
+
import { BaseReader } from './base'
|
|
14
|
+
|
|
15
|
+
const formatGeojson = new GeoJSON()
|
|
16
|
+
|
|
17
|
+
export function parseGeojson(text: string): DataItem[] {
|
|
18
|
+
const parsed = JSON.parse(text)
|
|
19
|
+
const features =
|
|
20
|
+
parsed.type === 'FeatureCollection' ? parsed.features : parsed
|
|
21
|
+
if (!Array.isArray(features)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Could not parse GeoJSON, expected a features collection or an array of features at root level'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return features
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseGml(
|
|
30
|
+
text: string,
|
|
31
|
+
featureType: string,
|
|
32
|
+
version: string
|
|
33
|
+
): DataItem[] {
|
|
34
|
+
const parts = featureType.split(':')
|
|
35
|
+
const regex = new RegExp(`xmlns:${parts[0]}=["']([^'"]*)["']`)
|
|
36
|
+
const match = regex.exec(text)
|
|
37
|
+
if (match && match.length >= 2) {
|
|
38
|
+
const wfs = new WFS({
|
|
39
|
+
featureNS: match[1],
|
|
40
|
+
featureType: parts[1],
|
|
41
|
+
version: version,
|
|
42
|
+
})
|
|
43
|
+
let features: Feature[]
|
|
44
|
+
try {
|
|
45
|
+
features = wfs.readFeatures(text)
|
|
46
|
+
} catch (e: unknown) {
|
|
47
|
+
throw Error(
|
|
48
|
+
`Couldn't parse WFS with GML features: ${(e as Error).message}`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
const geojsonItem = formatGeojson.writeFeaturesObject(features)
|
|
52
|
+
return geojsonItem.features
|
|
53
|
+
}
|
|
54
|
+
throw Error("Couldn't retrieve namespace url")
|
|
55
|
+
}
|
|
12
56
|
|
|
13
57
|
export async function getWfsEndpoint(wfsUrl: string): Promise<WfsEndpoint> {
|
|
14
58
|
try {
|
|
@@ -36,151 +80,163 @@ export async function getWfsEndpoint(wfsUrl: string): Promise<WfsEndpoint> {
|
|
|
36
80
|
}
|
|
37
81
|
}
|
|
38
82
|
|
|
39
|
-
export class WfsReader extends
|
|
40
|
-
endpoint: WfsEndpoint
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
83
|
+
export class WfsReader extends BaseReader {
|
|
84
|
+
endpoint: Promise<WfsEndpoint>
|
|
85
|
+
featureType: Promise<WfsFeatureTypeFull>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* This dataset reader will read the whole data as geojson or gml instead of
|
|
89
|
+
* using the WFS protocol; it is used in the following situations:
|
|
90
|
+
* - the endpoint does not support pagination
|
|
91
|
+
* - aggregations or grouping are set on the dataset (i.e. for chart view)
|
|
92
|
+
*/
|
|
93
|
+
backupReader_: Promise<BaseReader>
|
|
94
|
+
|
|
95
|
+
constructor(url: string, featureTypeName: string) {
|
|
96
|
+
super(url)
|
|
97
|
+
this.endpoint = getWfsEndpoint(url)
|
|
98
|
+
|
|
99
|
+
this.featureType = this.endpoint
|
|
100
|
+
.then((endpoint) => {
|
|
101
|
+
const featureTypes = endpoint.getFeatureTypes()
|
|
102
|
+
return endpoint.getFeatureTypeFull(
|
|
103
|
+
featureTypes.length === 1 && !featureTypeName
|
|
104
|
+
? featureTypes[0].name
|
|
105
|
+
: featureTypeName
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
.then((featureType) => {
|
|
109
|
+
if (!featureType) {
|
|
110
|
+
throw new Error('wfs.featuretype.notfound')
|
|
111
|
+
}
|
|
112
|
+
return featureType
|
|
113
|
+
})
|
|
54
114
|
}
|
|
55
115
|
|
|
56
|
-
get
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
116
|
+
get backupReader(): Promise<BaseReader> {
|
|
117
|
+
if (this.backupReader_) {
|
|
118
|
+
return this.backupReader_
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.backupReader_ = Promise.all([this.endpoint, this.featureType]).then(
|
|
122
|
+
([endpoint, featureType]) => {
|
|
123
|
+
let reader: BaseReader
|
|
124
|
+
if (endpoint.supportsJson(featureType.name)) {
|
|
125
|
+
reader = new GeojsonReader(
|
|
126
|
+
endpoint.getFeatureUrl(featureType.name, {
|
|
127
|
+
asJson: true,
|
|
128
|
+
outputCrs: 'EPSG:4326',
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
} else {
|
|
132
|
+
if (
|
|
133
|
+
featureType.outputFormats.find((f) =>
|
|
134
|
+
f.toLowerCase().includes('gml')
|
|
135
|
+
) &&
|
|
136
|
+
(featureType.defaultCrs === 'EPSG:4326' ||
|
|
137
|
+
featureType.otherCrs?.includes('EPSG:4326'))
|
|
138
|
+
) {
|
|
139
|
+
reader = new GmlReader(
|
|
140
|
+
endpoint.getFeatureUrl(featureType.name, {
|
|
141
|
+
outputFormat: featureType.outputFormats.find((f) =>
|
|
142
|
+
f.toLowerCase().includes('gml')
|
|
143
|
+
),
|
|
144
|
+
outputCrs: 'EPSG:4326',
|
|
145
|
+
})
|
|
146
|
+
)
|
|
70
147
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
148
|
+
}
|
|
149
|
+
reader.enableCache(this.cacheEnabled)
|
|
150
|
+
reader.load()
|
|
151
|
+
return reader
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
return this.backupReader_
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get properties(): Promise<PropertyInfo[]> {
|
|
158
|
+
return this.featureType.then((featureType) =>
|
|
159
|
+
Object.keys(featureType.properties).map((prop) => {
|
|
160
|
+
const originalType = featureType.properties[prop]
|
|
161
|
+
let type: PropertyInfo['type']
|
|
162
|
+
if (originalType === 'float' || originalType === 'integer') {
|
|
163
|
+
type = 'number'
|
|
164
|
+
} else if (originalType === 'boolean') {
|
|
165
|
+
type = 'string' // we don't handle booleans yet in the data fetcher
|
|
166
|
+
} else {
|
|
167
|
+
type = originalType
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
name: prop,
|
|
171
|
+
label: prop,
|
|
172
|
+
type,
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
)
|
|
73
176
|
}
|
|
74
177
|
|
|
75
178
|
get info(): Promise<DatasetInfo> {
|
|
76
|
-
return this.
|
|
179
|
+
return this.featureType.then(
|
|
77
180
|
(result) =>
|
|
78
181
|
({
|
|
79
182
|
itemsCount: result.objectCount,
|
|
183
|
+
hasGeometry: !!result.geometryName,
|
|
80
184
|
}) as DatasetInfo
|
|
81
185
|
)
|
|
82
186
|
}
|
|
83
187
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const featureType = wfsEndpoint.getFeatureTypeSummary(
|
|
88
|
-
featureTypes.length === 1 && !featureTypeName
|
|
89
|
-
? featureTypes[0].name
|
|
90
|
-
: featureTypeName
|
|
91
|
-
)
|
|
92
|
-
if (!featureType) {
|
|
93
|
-
throw new Error('wfs.featuretype.notfound')
|
|
94
|
-
}
|
|
188
|
+
load() {
|
|
189
|
+
// Nothing to load for Wfs
|
|
190
|
+
}
|
|
95
191
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
outputFormat: featureType.outputFormats.find((f) =>
|
|
116
|
-
f.toLowerCase().includes('gml')
|
|
117
|
-
),
|
|
118
|
-
outputCrs: 'EPSG:4326',
|
|
119
|
-
}),
|
|
120
|
-
featureType.name,
|
|
121
|
-
wfsEndpoint.getVersion()
|
|
122
|
-
)
|
|
192
|
+
async read(): Promise<DataItem[]> {
|
|
193
|
+
const endpoint = await this.endpoint
|
|
194
|
+
const featureType = await this.featureType
|
|
195
|
+
|
|
196
|
+
// if we can't use the WFS protocol we fall back to the backup reader
|
|
197
|
+
if (this.aggregations || this.groupedBy || !endpoint.supportsStartIndex()) {
|
|
198
|
+
const backupReader = await this.backupReader
|
|
199
|
+
backupReader.selectAll()
|
|
200
|
+
if (this.selected) {
|
|
201
|
+
backupReader.select(...this.selected)
|
|
202
|
+
}
|
|
203
|
+
if (this.aggregations) {
|
|
204
|
+
backupReader.aggregate(...this.aggregations)
|
|
205
|
+
}
|
|
206
|
+
if (this.groupedBy) {
|
|
207
|
+
backupReader.groupBy(...this.groupedBy)
|
|
208
|
+
}
|
|
209
|
+
if (this.sort) {
|
|
210
|
+
backupReader.orderBy(...this.sort)
|
|
123
211
|
}
|
|
124
|
-
|
|
212
|
+
if (this.startIndex !== null && this.count !== null) {
|
|
213
|
+
backupReader.limit(this.startIndex, this.count)
|
|
214
|
+
}
|
|
215
|
+
return backupReader.read()
|
|
125
216
|
}
|
|
126
|
-
}
|
|
127
217
|
|
|
128
|
-
|
|
129
|
-
if (aggregation || groupedBy) {
|
|
130
|
-
return { items: await this.getQueryData() }
|
|
131
|
-
}
|
|
132
|
-
const asJson = this.endpoint.supportsJson(this.featureTypeName)
|
|
218
|
+
const asJson = endpoint.supportsJson(featureType.name)
|
|
133
219
|
const attributes = this.selected ?? undefined
|
|
134
|
-
let
|
|
220
|
+
let sortBy = null
|
|
221
|
+
if (this.sort) {
|
|
222
|
+
const mapSort = (s) => [s[0] === 'desc' ? 'D' : 'A', s[1]]
|
|
223
|
+
sortBy = Array.isArray(this.sort[0])
|
|
224
|
+
? this.sort.map(mapSort)
|
|
225
|
+
: mapSort(this.sort)
|
|
226
|
+
}
|
|
227
|
+
const url = endpoint.getFeatureUrl(featureType.name, {
|
|
135
228
|
...(this.startIndex !== null && { startIndex: this.startIndex }),
|
|
136
229
|
...(this.count !== null && { maxFeatures: this.count }),
|
|
137
230
|
asJson,
|
|
138
231
|
outputCrs: 'EPSG:4326',
|
|
139
232
|
attributes,
|
|
140
|
-
|
|
233
|
+
sortBy,
|
|
141
234
|
})
|
|
142
235
|
|
|
143
|
-
|
|
144
|
-
const finalUrl = new URL(url)
|
|
145
|
-
const sorts = this.sort
|
|
146
|
-
.map(
|
|
147
|
-
(fieldSort) => `${fieldSort[1]}+${fieldSort[0] === 'asc' ? 'A' : 'D'}`
|
|
148
|
-
)
|
|
149
|
-
.join(',')
|
|
150
|
-
// Direct update on string url to prevent encoding of +A and +D
|
|
151
|
-
url = `${url}${finalUrl.search ? '&' : ''}SORTBY=${sorts}`
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return fetchDataAsText(url, this.cacheActive).then((text) =>
|
|
236
|
+
return fetchDataAsText(url, this.cacheEnabled).then((text) =>
|
|
155
237
|
asJson
|
|
156
238
|
? parseGeojson(text)
|
|
157
|
-
: parseGml(text,
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
public async getQueryData() {
|
|
162
|
-
const items = (await this.getData()).items
|
|
163
|
-
const jsonItems = getJsonDataItemsProxy(items)
|
|
164
|
-
const query = generateSqlQuery(
|
|
165
|
-
this.selected,
|
|
166
|
-
this.filter,
|
|
167
|
-
this.sort,
|
|
168
|
-
this.startIndex,
|
|
169
|
-
this.count,
|
|
170
|
-
this.groupedBy,
|
|
171
|
-
this.aggregations
|
|
172
|
-
)
|
|
173
|
-
const result: any[] = await import('alasql').then((module) =>
|
|
174
|
-
module.default(query, [jsonItems])
|
|
239
|
+
: parseGml(text, featureType.name, endpoint.getVersion())
|
|
175
240
|
)
|
|
176
|
-
return result.map(jsonToGeojsonFeature)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
load() {
|
|
180
|
-
// Nothing to load for Wfs
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async read(): Promise<DataItem[]> {
|
|
184
|
-
return (await this.getData(this.aggregations, this.groupedBy)).items
|
|
185
241
|
}
|
|
186
242
|
}
|
|
@@ -2,14 +2,11 @@ import {
|
|
|
2
2
|
DataItem,
|
|
3
3
|
DatasetHeaders,
|
|
4
4
|
FetchError,
|
|
5
|
-
PropertyInfo,
|
|
6
5
|
SupportedType,
|
|
7
6
|
SupportedTypes,
|
|
8
7
|
} from './model'
|
|
9
8
|
import { sharedFetch, useCache } from '@camptocamp/ogc-client'
|
|
10
9
|
import { parseHeaders } from './headers'
|
|
11
|
-
import { parse as parseDate } from 'date-fns/parse'
|
|
12
|
-
import { parseISO as parseIsoDate } from 'date-fns/parseISO'
|
|
13
10
|
|
|
14
11
|
export async function inferDatasetType(
|
|
15
12
|
url: string,
|
|
@@ -95,146 +92,6 @@ export function fetchDataAsArrayBuffer(
|
|
|
95
92
|
})
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
export function tryParseDate(input: unknown): Date | null {
|
|
99
|
-
if (typeof input !== 'string') return null
|
|
100
|
-
|
|
101
|
-
function tryIso(value: string) {
|
|
102
|
-
const parsed = parseIsoDate(value)
|
|
103
|
-
return isNaN(parsed.getDate()) ? null : parsed
|
|
104
|
-
}
|
|
105
|
-
function tryFormat(value: string, format: string) {
|
|
106
|
-
const parsed = parseDate(value, format, new Date())
|
|
107
|
-
return isNaN(parsed.getDate()) ? null : parsed
|
|
108
|
-
}
|
|
109
|
-
return (
|
|
110
|
-
tryIso(input) ||
|
|
111
|
-
tryFormat(input, 'dd/MM/yyyy') ||
|
|
112
|
-
tryFormat(input, 'dd.MM.yyyy') ||
|
|
113
|
-
tryFormat(input, 'MM/dd/yyyy') ||
|
|
114
|
-
null
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function tryParseNumber(input: string): number | null {
|
|
119
|
-
if (isNaN(input as any)) return null
|
|
120
|
-
const parsed = parseFloat(input)
|
|
121
|
-
return isNaN(parsed) ? null : parsed
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function jsonToGeojsonFeature(object: { [key: string]: any }): DataItem {
|
|
125
|
-
const { id, properties } = Object.keys(object)
|
|
126
|
-
.map((property) => (property ? property : 'unknown')) //prevent empty strings
|
|
127
|
-
.reduce(
|
|
128
|
-
(prev, curr) =>
|
|
129
|
-
curr.toLowerCase().endsWith('id')
|
|
130
|
-
? {
|
|
131
|
-
...prev,
|
|
132
|
-
id: object[curr],
|
|
133
|
-
}
|
|
134
|
-
: {
|
|
135
|
-
...prev,
|
|
136
|
-
properties: { ...prev.properties, [curr]: object[curr] },
|
|
137
|
-
},
|
|
138
|
-
{ id: undefined, properties: {} }
|
|
139
|
-
)
|
|
140
|
-
return {
|
|
141
|
-
type: 'Feature',
|
|
142
|
-
geometry: null,
|
|
143
|
-
properties,
|
|
144
|
-
...(id !== undefined && { id }),
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function mutateProperties(
|
|
149
|
-
items: DataItem[],
|
|
150
|
-
mutators: { [fieldName: string]: (value: unknown) => unknown }
|
|
151
|
-
): DataItem[] {
|
|
152
|
-
const mutatorKeys = Object.keys(mutators)
|
|
153
|
-
for (let i = 0, ii = items.length; i < ii; i++) {
|
|
154
|
-
const item = items[i]
|
|
155
|
-
for (const mutatorField of mutatorKeys) {
|
|
156
|
-
if (!(mutatorField in item.properties)) continue
|
|
157
|
-
item.properties[mutatorField] = mutators[mutatorField](
|
|
158
|
-
item.properties[mutatorField]
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return items
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const SAMPLE_SIZE = 20
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* This will infer field types from a list of data items and cast the values accordingly
|
|
169
|
-
* @param items
|
|
170
|
-
* @param inferTypes
|
|
171
|
-
*/
|
|
172
|
-
export function processItemProperties(
|
|
173
|
-
items: DataItem[],
|
|
174
|
-
inferTypes = false
|
|
175
|
-
): {
|
|
176
|
-
items: DataItem[]
|
|
177
|
-
properties: PropertyInfo[]
|
|
178
|
-
} {
|
|
179
|
-
const foundFields: { [name: string]: PropertyInfo } = {}
|
|
180
|
-
for (let i = 0, ii = Math.min(SAMPLE_SIZE, items.length); i < ii; i++) {
|
|
181
|
-
const item = items[i]
|
|
182
|
-
const fields = Object.keys(item.properties)
|
|
183
|
-
for (const field of fields) {
|
|
184
|
-
if (!(field in foundFields)) {
|
|
185
|
-
foundFields[field] = {
|
|
186
|
-
label: field,
|
|
187
|
-
name: field,
|
|
188
|
-
type: null,
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const value = item.properties[field]
|
|
192
|
-
const info = foundFields[field]
|
|
193
|
-
if (value === undefined || value === '' || value === null) continue
|
|
194
|
-
|
|
195
|
-
if (!inferTypes) {
|
|
196
|
-
if (info.type === null && typeof value === 'number') {
|
|
197
|
-
info.type = 'number'
|
|
198
|
-
} else if (info.type === 'number' && typeof value !== 'number') {
|
|
199
|
-
info.type = 'string'
|
|
200
|
-
}
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const parsedNumber = tryParseNumber(value)
|
|
205
|
-
if (info.type === null && parsedNumber !== null) {
|
|
206
|
-
info.type = 'number'
|
|
207
|
-
continue
|
|
208
|
-
} else if (info.type === 'number' && parsedNumber === null) {
|
|
209
|
-
info.type = 'string'
|
|
210
|
-
continue
|
|
211
|
-
}
|
|
212
|
-
const parsedDate = tryParseDate(value)
|
|
213
|
-
if (info.type === null && parsedDate !== null) {
|
|
214
|
-
info.type = 'date'
|
|
215
|
-
} else if (info.type === 'date' && parsedDate === null) {
|
|
216
|
-
info.type = 'string'
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const properties: PropertyInfo[] = []
|
|
222
|
-
const mutators = {}
|
|
223
|
-
for (const field in foundFields) {
|
|
224
|
-
const info = foundFields[field]
|
|
225
|
-
if (info.type === 'number') {
|
|
226
|
-
mutators[field] = tryParseNumber
|
|
227
|
-
} else if (info.type === 'date') {
|
|
228
|
-
mutators[field] = tryParseDate
|
|
229
|
-
}
|
|
230
|
-
properties.push({ ...info, type: info.type || 'string' })
|
|
231
|
-
}
|
|
232
|
-
if (inferTypes) {
|
|
233
|
-
mutateProperties(items, mutators)
|
|
234
|
-
}
|
|
235
|
-
return { items, properties }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
95
|
/**
|
|
239
96
|
* This creates a Proxy that allows reading and writing to the data item properties
|
|
240
97
|
* as if it was a simple array of JSON objects
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
ElementRef,
|
|
4
|
+
Injector,
|
|
5
|
+
Input,
|
|
6
|
+
afterNextRender,
|
|
7
|
+
booleanAttribute,
|
|
8
|
+
inject,
|
|
9
|
+
} from '@angular/core'
|
|
10
|
+
|
|
11
|
+
@Directive({
|
|
12
|
+
selector: '[gnUiAutofocus]',
|
|
13
|
+
standalone: true,
|
|
14
|
+
})
|
|
15
|
+
export class AutofocusDirective {
|
|
16
|
+
private el = inject<ElementRef<HTMLElement>>(ElementRef)
|
|
17
|
+
private injector = inject(Injector)
|
|
18
|
+
|
|
19
|
+
@Input({ alias: 'gnUiAutofocus', transform: booleanAttribute })
|
|
20
|
+
set autofocus(active: boolean) {
|
|
21
|
+
if (!active) return
|
|
22
|
+
afterNextRender(() => this.el.nativeElement.focus(), {
|
|
23
|
+
injector: this.injector,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|