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
|
@@ -4,7 +4,13 @@ import {
|
|
|
4
4
|
FieldGroupBy,
|
|
5
5
|
FieldName,
|
|
6
6
|
FieldSort,
|
|
7
|
-
} from '
|
|
7
|
+
} from '../model'
|
|
8
|
+
|
|
9
|
+
export const GEOMETRY_COLUMN_ALIAS = '__geometry__'
|
|
10
|
+
|
|
11
|
+
function fieldToSql(name: string): string {
|
|
12
|
+
return `"${name.replace(/"/g, '""')}"` // escape double quotes in field names
|
|
13
|
+
}
|
|
8
14
|
|
|
9
15
|
function filterToSql(filter: FieldFilter): string {
|
|
10
16
|
const operator = filter[0]
|
|
@@ -20,12 +26,12 @@ function filterToSql(filter: FieldFilter): string {
|
|
|
20
26
|
case '=':
|
|
21
27
|
case '!=':
|
|
22
28
|
case 'like':
|
|
23
|
-
return
|
|
29
|
+
return `${fieldToSql(args[0] as string)} ${operator.toUpperCase()} ${valueToSql(
|
|
24
30
|
args[1] as string | number
|
|
25
31
|
)}`
|
|
26
32
|
case 'in': {
|
|
27
33
|
const values = args.slice(1) as string[] | number[]
|
|
28
|
-
return
|
|
34
|
+
return `${fieldToSql(args[0] as string)} IN (${values.map(valueToSql).join(', ')})`
|
|
29
35
|
}
|
|
30
36
|
case 'and':
|
|
31
37
|
case 'or': {
|
|
@@ -48,18 +54,19 @@ function aggregationToSql(aggregation: FieldAggregation): string {
|
|
|
48
54
|
|
|
49
55
|
switch (operation) {
|
|
50
56
|
case 'average':
|
|
51
|
-
return `AVG(
|
|
57
|
+
return `CAST(AVG(${fieldToSql(field)}) AS DOUBLE) as ${fieldToSql(`average(${field})`)}`
|
|
52
58
|
case 'sum':
|
|
53
59
|
case 'max':
|
|
54
60
|
case 'min':
|
|
55
|
-
return
|
|
61
|
+
return `CAST(${operation.toUpperCase()}(${fieldToSql(field)}) AS DOUBLE) as ${fieldToSql(`${operation}(${field})`)}`
|
|
56
62
|
case 'count':
|
|
57
|
-
return 'COUNT(*) as
|
|
63
|
+
return 'CAST(COUNT(*) AS INTEGER) as "count()"' // we don't need Bigint precision here
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/**
|
|
62
68
|
* Leave arguments at null if not used
|
|
69
|
+
* @param tableName
|
|
63
70
|
* @param selected
|
|
64
71
|
* @param filter
|
|
65
72
|
* @param sort
|
|
@@ -69,29 +76,34 @@ function aggregationToSql(aggregation: FieldAggregation): string {
|
|
|
69
76
|
* @param aggregations
|
|
70
77
|
*/
|
|
71
78
|
export function generateSqlQuery(
|
|
79
|
+
tableName: string,
|
|
72
80
|
selected: FieldName[] = null,
|
|
73
81
|
filter: FieldFilter = null,
|
|
74
82
|
sort: FieldSort[] = null,
|
|
75
83
|
startIndex: number = null,
|
|
76
84
|
count: number = null,
|
|
77
85
|
groupBy: FieldGroupBy[] = null,
|
|
78
|
-
aggregations: FieldAggregation[] = null
|
|
86
|
+
aggregations: FieldAggregation[] = null,
|
|
87
|
+
geometryColumn: string = null
|
|
79
88
|
): string {
|
|
80
89
|
let sqlSelect = 'SELECT *'
|
|
81
|
-
const sqlFrom =
|
|
90
|
+
const sqlFrom = ` FROM ${tableName}`
|
|
82
91
|
let sqlOrderBy = ''
|
|
83
92
|
let sqlWhere = ''
|
|
84
93
|
let sqlLimit = ''
|
|
85
94
|
let sqlGroupBy = ''
|
|
86
95
|
if (selected !== null) {
|
|
87
|
-
sqlSelect = `SELECT ${selected.map(
|
|
96
|
+
sqlSelect = `SELECT ${selected.map(fieldToSql).join(', ')}`
|
|
97
|
+
}
|
|
98
|
+
if (geometryColumn !== null) {
|
|
99
|
+
sqlSelect += `, ST_AsGeoJSON(${fieldToSql(geometryColumn)}) as ${GEOMETRY_COLUMN_ALIAS}`
|
|
88
100
|
}
|
|
89
101
|
if (filter !== null) {
|
|
90
102
|
sqlWhere = ` WHERE ${filterToSql(filter)}`
|
|
91
103
|
}
|
|
92
104
|
if (sort?.length) {
|
|
93
105
|
sqlOrderBy = ` ORDER BY ${sort
|
|
94
|
-
.map((sort) =>
|
|
106
|
+
.map((sort) => `${fieldToSql(sort[1])} ${sort[0].toUpperCase()}`)
|
|
95
107
|
.join(', ')}`
|
|
96
108
|
}
|
|
97
109
|
if (startIndex !== null && count !== null) {
|
|
@@ -101,15 +113,18 @@ export function generateSqlQuery(
|
|
|
101
113
|
sqlSelect = `SELECT ${aggregations.map(aggregationToSql).join(', ')}`
|
|
102
114
|
const groupedByDistinct = groupBy.filter((group) => group[0] === 'distinct')
|
|
103
115
|
const sqlGroupByFields = groupedByDistinct
|
|
104
|
-
.map((group) =>
|
|
116
|
+
.map((group) => fieldToSql(group[1]))
|
|
105
117
|
.join(', ')
|
|
106
118
|
const sqlGroupBySelect = groupedByDistinct
|
|
107
|
-
.map(
|
|
119
|
+
.map(
|
|
120
|
+
(group) =>
|
|
121
|
+
`${fieldToSql(group[1])} as ${fieldToSql(`distinct(${group[1]})`)}`
|
|
122
|
+
)
|
|
108
123
|
.join(', ')
|
|
109
124
|
if (sqlGroupByFields && sqlGroupBySelect) {
|
|
110
125
|
sqlGroupBy = ` GROUP BY ${sqlGroupByFields}`
|
|
111
126
|
sqlSelect += `, ${sqlGroupBySelect}`
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
|
-
return sqlSelect + sqlFrom + sqlGroupBy +
|
|
129
|
+
return sqlSelect + sqlFrom + sqlGroupBy + sqlWhere + sqlOrderBy + sqlLimit
|
|
115
130
|
}
|
|
@@ -97,11 +97,12 @@ export interface DatasetHeaders {
|
|
|
97
97
|
export interface PropertyInfo {
|
|
98
98
|
name: string
|
|
99
99
|
label: string
|
|
100
|
-
type: 'number' | 'date' | '
|
|
100
|
+
type: 'number' | 'date' | 'boolean' | 'string' | 'other'
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export interface DatasetInfo {
|
|
104
104
|
itemsCount: number
|
|
105
|
+
hasGeometry: boolean
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export type FieldName = string
|
|
@@ -1,65 +1,80 @@
|
|
|
1
1
|
import { DataItem, DatasetInfo, PropertyInfo } from '../model'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { generateSqlQuery } from '../engine/sql-utils'
|
|
3
|
+
import { Engine, getEngine } from '../engine/duckdb'
|
|
4
|
+
import { BaseReader } from './base'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* This reader handles file formats supported natively by DuckDB
|
|
8
|
+
*/
|
|
9
|
+
export class BaseFileReader extends BaseReader {
|
|
10
|
+
protected engine: Engine
|
|
11
|
+
protected datasetId: string
|
|
12
|
+
protected properties_: PropertyInfo[]
|
|
13
|
+
protected geometryColumn: string
|
|
14
|
+
protected rowsCount: number
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
// a table id should not exceed 63 chars
|
|
17
|
+
protected generateDatasetId() {
|
|
18
|
+
// generate a hash out of the url
|
|
19
|
+
let hash = 0
|
|
20
|
+
for (const char of this.url) {
|
|
21
|
+
hash = (hash << 5) - hash + char.charCodeAt(0)
|
|
22
|
+
hash = hash >>> 0 // make it unsigned
|
|
23
|
+
}
|
|
24
|
+
return `datafetcher_${hash.toString(16)}`
|
|
25
|
+
}
|
|
13
26
|
|
|
14
|
-
protected
|
|
27
|
+
protected async getLoadQuery(): Promise<string> {
|
|
15
28
|
throw new Error('not implemented')
|
|
16
29
|
}
|
|
17
30
|
|
|
18
|
-
load() {
|
|
19
|
-
this.
|
|
31
|
+
async load() {
|
|
32
|
+
this.datasetId = this.generateDatasetId()
|
|
33
|
+
this.loadPromise_ = getEngine()
|
|
34
|
+
.then((engine) => {
|
|
35
|
+
this.engine = engine
|
|
36
|
+
return this.getLoadQuery()
|
|
37
|
+
})
|
|
38
|
+
.then((loadQuery) =>
|
|
39
|
+
this.engine.loadFile(this.datasetId, loadQuery, !this.cacheEnabled)
|
|
40
|
+
)
|
|
41
|
+
.then((datasetInfo) => {
|
|
42
|
+
this.properties_ = datasetInfo.properties
|
|
43
|
+
this.geometryColumn = datasetInfo.geometryColumn
|
|
44
|
+
this.rowsCount = datasetInfo.rowsCount
|
|
45
|
+
// returns void for the loaded promise
|
|
46
|
+
})
|
|
20
47
|
}
|
|
21
48
|
|
|
22
49
|
get properties(): Promise<PropertyInfo[]> {
|
|
23
|
-
return this.
|
|
50
|
+
return this.isLoaded.then(() => this.properties_)
|
|
24
51
|
}
|
|
25
52
|
|
|
26
53
|
get info(): Promise<DatasetInfo> {
|
|
27
|
-
return this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}) as DatasetInfo
|
|
32
|
-
)
|
|
54
|
+
return this.isLoaded.then(() => ({
|
|
55
|
+
itemsCount: this.rowsCount,
|
|
56
|
+
hasGeometry: !!this.geometryColumn,
|
|
57
|
+
}))
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
async read(): Promise<DataItem[]> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if
|
|
39
|
-
|
|
40
|
-
this.aggregations == null &&
|
|
41
|
-
this.selected == null &&
|
|
42
|
-
this.sort == null &&
|
|
43
|
-
this.filter == null &&
|
|
44
|
-
this.startIndex == null &&
|
|
45
|
-
this.count == null
|
|
46
|
-
) {
|
|
47
|
-
return items
|
|
48
|
-
}
|
|
61
|
+
await this.isLoaded
|
|
62
|
+
|
|
63
|
+
// if only certain fields are selected, omit the geometry
|
|
64
|
+
const geometryColumn = this.selected === null ? this.geometryColumn : null
|
|
49
65
|
|
|
50
|
-
const jsonItems = getJsonDataItemsProxy(items)
|
|
51
66
|
const query = generateSqlQuery(
|
|
67
|
+
this.datasetId,
|
|
52
68
|
this.selected,
|
|
53
69
|
this.filter,
|
|
54
70
|
this.sort,
|
|
55
71
|
this.startIndex,
|
|
56
72
|
this.count,
|
|
57
73
|
this.groupedBy,
|
|
58
|
-
this.aggregations
|
|
74
|
+
this.aggregations,
|
|
75
|
+
geometryColumn
|
|
59
76
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
return result.map(jsonToGeojsonFeature)
|
|
77
|
+
|
|
78
|
+
return this.engine.queryItems(query)
|
|
64
79
|
}
|
|
65
80
|
}
|
|
@@ -17,13 +17,24 @@ export class BaseReader {
|
|
|
17
17
|
protected sort: FieldSort[] = null
|
|
18
18
|
protected startIndex: number = null
|
|
19
19
|
protected count: number = null
|
|
20
|
+
protected loadPromise_: Promise<void> = Promise.resolve()
|
|
21
|
+
|
|
22
|
+
protected cacheEnabled = false
|
|
20
23
|
|
|
21
24
|
constructor(protected url: string) {}
|
|
22
25
|
|
|
26
|
+
enableCache(enabled: boolean) {
|
|
27
|
+
this.cacheEnabled = enabled
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
load() {
|
|
24
31
|
throw new Error('not implemented')
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
get isLoaded() {
|
|
35
|
+
return this.loadPromise_
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
get properties(): Promise<PropertyInfo[]> {
|
|
28
39
|
throw new Error('not implemented')
|
|
29
40
|
}
|
|
@@ -1,52 +1,14 @@
|
|
|
1
|
-
import * as Papa from 'papaparse'
|
|
2
|
-
import { DataItem, PropertyInfo } from '../model'
|
|
3
|
-
import {
|
|
4
|
-
fetchDataAsText,
|
|
5
|
-
jsonToGeojsonFeature,
|
|
6
|
-
processItemProperties,
|
|
7
|
-
} from '../utils'
|
|
8
1
|
import { BaseFileReader } from './base-file'
|
|
9
2
|
|
|
10
|
-
export function parseCsv(text: string): {
|
|
11
|
-
items: DataItem[]
|
|
12
|
-
properties: PropertyInfo[]
|
|
13
|
-
} {
|
|
14
|
-
// first parse the header to guess the delimiter
|
|
15
|
-
// note that we do that to not rely on Papaparse logic for guessing delimiter
|
|
16
|
-
let delimiter
|
|
17
|
-
try {
|
|
18
|
-
const header = text.split('\n')[0]
|
|
19
|
-
const result = Papa.parse(header, {
|
|
20
|
-
header: false,
|
|
21
|
-
})
|
|
22
|
-
delimiter = result.meta.delimiter
|
|
23
|
-
} catch (e) {
|
|
24
|
-
throw new Error('CSV parsing failed: the delimiter could not be guessed')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const parsed = Papa.parse(text, {
|
|
28
|
-
header: true,
|
|
29
|
-
skipEmptyLines: true,
|
|
30
|
-
delimiter,
|
|
31
|
-
})
|
|
32
|
-
if (parsed.errors.length) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
'CSV parsing failed for the following reasons:\n' +
|
|
35
|
-
parsed.errors
|
|
36
|
-
.map(
|
|
37
|
-
(error) =>
|
|
38
|
-
`* ${error.message} at row ${error.row}, column ${error.index}`
|
|
39
|
-
)
|
|
40
|
-
.join('\n')
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const items = (parsed.data as any[]).map(jsonToGeojsonFeature)
|
|
45
|
-
return processItemProperties(items, true)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
3
|
export class CsvReader extends BaseFileReader {
|
|
49
|
-
|
|
50
|
-
|
|
4
|
+
async getLoadQuery() {
|
|
5
|
+
// first we get a list of columns hich have a detected type of VARCHAR
|
|
6
|
+
// then we make sure that for those columns, we don't cast an empty string to null; instead we want to keep the empty string
|
|
7
|
+
return `
|
|
8
|
+
SET VARIABLE strColumns = (SELECT list(name) FROM (SELECT unnest(Columns, recursive := true) FROM sniff_csv("${this.url}")) WHERE type = 'VARCHAR');
|
|
9
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM read_csv("${this.url}",
|
|
10
|
+
force_not_null = getvariable('strColumns'),
|
|
11
|
+
auto_type_candidates = ['NULL', 'BOOLEAN', 'INTEGER', 'DOUBLE', 'DATE', 'VARCHAR']
|
|
12
|
+
);`
|
|
51
13
|
}
|
|
52
14
|
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { DataItem, PropertyInfo } from '../model'
|
|
2
|
-
import {
|
|
3
|
-
fetchDataAsArrayBuffer,
|
|
4
|
-
jsonToGeojsonFeature,
|
|
5
|
-
processItemProperties,
|
|
6
|
-
} from '../utils'
|
|
7
1
|
import { BaseFileReader } from './base-file'
|
|
2
|
+
import { fetchDataAsArrayBuffer } from '../utils'
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
export class ExcelReader extends BaseFileReader {
|
|
5
|
+
protected async getLoadQuery(): Promise<string> {
|
|
6
|
+
// we download the file as an array buffer first, in order to be able to check if it's an XLS file
|
|
7
|
+
let buffer = await fetchDataAsArrayBuffer(this.url, this.cacheEnabled)
|
|
8
|
+
const bufferHandle = `B${this.datasetId}`
|
|
9
|
+
|
|
10
|
+
// checking against the magic number at the beginning of XLS files, see https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
11
|
+
const magicNumber = new Uint8Array(buffer, 0, 8) // first 8 bytes
|
|
12
|
+
const isXls =
|
|
13
|
+
Array.from(magicNumber)
|
|
14
|
+
.map((n) => n.toString(16).toUpperCase())
|
|
15
|
+
.join(' ') === 'D0 CF 11 E0 A1 B1 1A E1'
|
|
16
|
+
|
|
17
|
+
// uh oh, this is an XLS file (not supported by duckdb); convert it to CSV using the xlsx package
|
|
18
|
+
if (isXls) {
|
|
19
|
+
buffer = await import('xlsx').then(({ read, utils }) => {
|
|
20
|
+
const workbook = read(buffer)
|
|
21
|
+
const json = utils.sheet_to_json(
|
|
22
|
+
workbook.Sheets[workbook.SheetNames[0]]
|
|
23
|
+
)
|
|
24
|
+
return new TextEncoder().encode(JSON.stringify(json)).buffer
|
|
25
|
+
})
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
})
|
|
27
|
-
}
|
|
27
|
+
const duckDbFn = isXls ? 'read_json' : 'read_xlsx'
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
await this.engine.registerData(bufferHandle, new Uint8Array(buffer))
|
|
30
|
+
return `
|
|
31
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM ${duckDbFn}("${bufferHandle}", ignore_errors = true);`
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
import { DataItem, PropertyInfo } from '../model'
|
|
2
|
-
import { fetchDataAsText, processItemProperties } from '../utils'
|
|
3
1
|
import { BaseFileReader } from './base-file'
|
|
4
2
|
|
|
5
|
-
/**
|
|
6
|
-
* This parser supports both Geojson Feature collections or arrays
|
|
7
|
-
* of Features
|
|
8
|
-
* @param text
|
|
9
|
-
*/
|
|
10
|
-
export function parseGeojson(text: string): {
|
|
11
|
-
items: DataItem[]
|
|
12
|
-
properties: PropertyInfo[]
|
|
13
|
-
} {
|
|
14
|
-
const parsed = JSON.parse(text)
|
|
15
|
-
const features =
|
|
16
|
-
parsed.type === 'FeatureCollection' ? parsed.features : parsed
|
|
17
|
-
if (!Array.isArray(features)) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
'Could not parse GeoJSON, expected a features collection or an array of features at root level'
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
return processItemProperties(features)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
3
|
export class GeojsonReader extends BaseFileReader {
|
|
26
|
-
|
|
27
|
-
return
|
|
4
|
+
async getLoadQuery() {
|
|
5
|
+
return `
|
|
6
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM st_read("${this.url}",
|
|
7
|
+
allowed_drivers = ['GeoJSON']
|
|
8
|
+
);`
|
|
28
9
|
}
|
|
29
10
|
}
|
|
@@ -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
|
}
|