geonetwork-ui 2.11.0-dev.e7b8ec331 → 2.11.0-dev.fb3fea50c
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 +1273 -698
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +223 -79
- 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 +77 -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 +13 -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/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
- 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/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/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 +43 -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/inputs/src/index.ts +2 -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 +1 -1
- package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
- 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/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 -2
- package/translations/en.json +16 -1
- package/translations/es.json +15 -0
- package/translations/fr.json +17 -2
- package/translations/it.json +15 -0
- package/translations/nl.json +15 -0
- package/translations/pt.json +15 -0
- package/translations/sk.json +15 -0
- package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
|
@@ -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
|
+
}
|
|
@@ -35,10 +35,10 @@ export class DateService {
|
|
|
35
35
|
return { locale, dateObj }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
async getDateFnsLocale(): Promise<Locale> {
|
|
39
39
|
const lang = this.translateService.getCurrentLang() || DEFAULT_LANGUAGE
|
|
40
40
|
const locales = await this.dateLocales
|
|
41
|
-
return locales[lang]
|
|
41
|
+
return locales[lang] ?? locales[DEFAULT_LANGUAGE]
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
formatDate(
|
|
@@ -61,7 +61,7 @@ export class DateService {
|
|
|
61
61
|
const dateObj = this.getDateObject(date)
|
|
62
62
|
|
|
63
63
|
const now = new Date()
|
|
64
|
-
const locale = await this.
|
|
64
|
+
const locale = await this.getDateFnsLocale()
|
|
65
65
|
|
|
66
66
|
return formatDistance(dateObj, now, {
|
|
67
67
|
addSuffix: true,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isFileExtensionValid(
|
|
2
|
+
fileName: string,
|
|
3
|
+
acceptedExtensions: string[]
|
|
4
|
+
): boolean {
|
|
5
|
+
return acceptedExtensions.some((ext) => fileName.toLowerCase().endsWith(ext))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function readFileAsText(file: File): Promise<string> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const reader = new FileReader()
|
|
11
|
+
reader.onload = () => resolve(reader.result as string)
|
|
12
|
+
reader.onerror = () => reject(reader.error)
|
|
13
|
+
reader.readAsText(file)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -38,6 +38,14 @@ export function getGeometryFromGeoJSON(
|
|
|
38
38
|
// FIXME: this type should be more generic across the project
|
|
39
39
|
export type BoundingBox = [number, number, number, number]
|
|
40
40
|
|
|
41
|
+
export function isBoundingBox(value: unknown): value is BoundingBox {
|
|
42
|
+
return (
|
|
43
|
+
Array.isArray(value) &&
|
|
44
|
+
value.length === 4 &&
|
|
45
|
+
value.every((item) => typeof item === 'number')
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export function getGeometryBoundingBox(geometry: Geometry): BoundingBox {
|
|
42
50
|
// use the bounding box if specified in the GeoJSON object
|
|
43
51
|
if (geometry.bbox) {
|
package/tailwind.base.css
CHANGED
|
@@ -224,6 +224,11 @@
|
|
|
224
224
|
disabled:cursor-not-allowed disabled:border-gray-100 disabled:placeholder-gray-300 disabled:text-gray-700 disabled:bg-white;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
.gn-ui-gray-outline-input {
|
|
228
|
+
@apply bg-white rounded-lg
|
|
229
|
+
border border-gray-400 hover:border-neutral-300 hover:bg-black/10;
|
|
230
|
+
}
|
|
231
|
+
|
|
227
232
|
/* GENERIC CARD */
|
|
228
233
|
.gn-ui-card {
|
|
229
234
|
@apply flex flex-row border border-gray-200 rounded-xl p-4 gap-2 w-full;
|
package/translations/de.json
CHANGED
|
@@ -65,9 +65,10 @@
|
|
|
65
65
|
"dataset.error.restrictedAccess": "",
|
|
66
66
|
"dataset.error.unknown": "Die Daten können nicht angezeigt werden: \"{ info }\"",
|
|
67
67
|
"dataset.error.unsupportedType": "Der folgende Inhaltstyp wird nicht unterstützt: \"{ info }\"",
|
|
68
|
-
"daterange.filter.
|
|
68
|
+
"daterange.filter.datePlaceholder": "tt.mm.jjjj",
|
|
69
|
+
"daterange.filter.from": "Ab dem",
|
|
69
70
|
"daterange.filter.period": "Zeitraum auswählen",
|
|
70
|
-
"daterange.filter.to": "Bis",
|
|
71
|
+
"daterange.filter.to": "Bis zum",
|
|
71
72
|
"domain.contact.role.author": "Autor",
|
|
72
73
|
"domain.contact.role.collaborator": "Mitarbeiter",
|
|
73
74
|
"domain.contact.role.contributor": "Beitragender",
|
|
@@ -550,6 +551,7 @@
|
|
|
550
551
|
"record.metadata.languages": "Sprachen",
|
|
551
552
|
"record.metadata.link.postgis.table": "Tabelle:",
|
|
552
553
|
"record.metadata.link.postgis.tooltip": "PostGIS-Ressourcen haben keinen Zugriffslink.",
|
|
554
|
+
"record.metadata.linked.associatedRecords": "",
|
|
553
555
|
"record.metadata.linked.datasets": "Verknüpfte Datensätze",
|
|
554
556
|
"record.metadata.linked.records": "Verknüpfte Inhalte",
|
|
555
557
|
"record.metadata.linked.reuses": "Verknüpfte Wiederverwendungen",
|
|
@@ -688,9 +690,22 @@
|
|
|
688
690
|
"search.filters.recordKind.reuse": "Wiederverwendungen",
|
|
689
691
|
"search.filters.recordKind.service": "Dienste",
|
|
690
692
|
"search.filters.representationType": "Repräsentationstyp",
|
|
693
|
+
"search.filters.resourceCreationRevisionDate": "Ressourcendatum",
|
|
691
694
|
"search.filters.resourceType": "Ressourcentyp",
|
|
695
|
+
"search.filters.spatialExtent": "",
|
|
696
|
+
"search.filters.spatialExtent.bboxFromFile": "",
|
|
697
|
+
"search.filters.spatialExtent.bboxFromFileDelete": "",
|
|
698
|
+
"search.filters.spatialExtent.bboxInitial": "",
|
|
699
|
+
"search.filters.spatialExtent.bboxInitialDelete": "",
|
|
700
|
+
"search.filters.spatialExtent.error.fileTooLarge": "",
|
|
701
|
+
"search.filters.spatialExtent.error.invalidFormat": "",
|
|
702
|
+
"search.filters.spatialExtent.error.noGeometry": "",
|
|
703
|
+
"search.filters.spatialExtent.error.title": "",
|
|
704
|
+
"search.filters.spatialExtent.helpText": "",
|
|
705
|
+
"search.filters.spatialExtent.import": "",
|
|
692
706
|
"search.filters.standard": "Standard",
|
|
693
707
|
"search.filters.summaryLabel.changeDate": "Geändert am: ",
|
|
708
|
+
"search.filters.summaryLabel.resourceCreationRevisionDate": "Ressourcendatum: ",
|
|
694
709
|
"search.filters.summaryLabel.user": "Geändert von: ",
|
|
695
710
|
"search.filters.title": "Ergebnisse filtern",
|
|
696
711
|
"search.filters.topic": "Themen",
|