geonetwork-ui 2.11.0-dev.5c2e5295e → 2.11.0-dev.68d9eaa3a
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 +621 -492
- package/fesm2022/geonetwork-ui.mjs.map +1 -1
- package/index.d.ts +80 -48
- package/index.d.ts.map +1 -1
- package/package.json +8 -8
- 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 +1 -0
- 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/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/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/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/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/translations/de.json +2 -1
- package/translations/en.json +2 -1
- package/translations/es.json +1 -0
- package/translations/fr.json +2 -1
- package/translations/it.json +2 -1
- package/translations/nl.json +1 -0
- package/translations/pt.json +1 -0
- package/translations/sk.json +2 -1
- package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
|
@@ -6,7 +6,7 @@ import GeoJSON from 'ol/format/GeoJSON.js';
|
|
|
6
6
|
import { parse as parse$1 } from 'ol/xml.js';
|
|
7
7
|
import { format } from 'date-fns/format';
|
|
8
8
|
import { Namespace, Literal, lit, parse as parse$2, sym, BlankNode, graph } from 'rdflib';
|
|
9
|
-
import { lastValueFrom, fromEvent, startWith, map as map$2, shareReplay, filter, pairwise, of, switchMap, Subject, combineLatest, from, exhaustMap, throwError,
|
|
9
|
+
import { lastValueFrom, fromEvent, startWith, map as map$2, shareReplay, filter, pairwise, of, switchMap, Subject, forkJoin, combineLatest, from, exhaustMap, throwError, takeLast, firstValueFrom, merge, BehaviorSubject, timer, ReplaySubject, Subscription, first, distinctUntilChanged as distinctUntilChanged$1, animationFrameScheduler, debounceTime as debounceTime$1, Observable, buffer, tap as tap$2, combineLatestWith, take as take$1, catchError as catchError$1, takeUntil, EMPTY, mergeMap as mergeMap$1, withLatestFrom as withLatestFrom$1 } from 'rxjs';
|
|
10
10
|
import * as i0 from '@angular/core';
|
|
11
11
|
import { InjectionToken, inject, Injectable, NgModule, Injector, provideAppInitializer, makeEnvironmentProviders, ElementRef, HostListener, Input, Directive, Renderer2, DestroyRef, EventEmitter, Output, ViewChild, ChangeDetectionStrategy, Component, ViewEncapsulation, ChangeDetectorRef, HostBinding, ViewContainerRef, TemplateRef, ViewChildren, ContentChild, input, output, signal, viewChild, computed, ContentChildren, NgZone, ComponentFactoryResolver, afterNextRender, viewChildren } from '@angular/core';
|
|
12
12
|
import { HttpClient, HttpHeaders, HttpParams, HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi, HttpEventType } from '@angular/common/http';
|
|
@@ -79,10 +79,10 @@ import { ScrollingModule, CdkScrollable } from '@angular/cdk/scrolling';
|
|
|
79
79
|
import Duration from 'duration-relativetimeformat';
|
|
80
80
|
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
|
81
81
|
import { trigger, transition, animate, keyframes, style } from '@angular/animations';
|
|
82
|
-
import * as
|
|
83
|
-
import {
|
|
84
|
-
import
|
|
85
|
-
import
|
|
82
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
83
|
+
import { Vector, DataType } from 'apache-arrow';
|
|
84
|
+
import WFS from 'ol/format/WFS';
|
|
85
|
+
import GeoJSON$1 from 'ol/format/GeoJSON';
|
|
86
86
|
import { Chart, BarController, BarElement, CategoryScale, LinearScale, LineController, LineElement, PointElement, PieController, ArcElement, ScatterController, Tooltip, Colors, Legend } from 'chart.js';
|
|
87
87
|
import * as i2$4 from '@angular/material/sort';
|
|
88
88
|
import { MatSortModule, MatSort } from '@angular/material/sort';
|
|
@@ -90,7 +90,6 @@ import * as i1$b from '@angular/material/table';
|
|
|
90
90
|
import { MatTableModule } from '@angular/material/table';
|
|
91
91
|
import * as i3 from '@angular/material/paginator';
|
|
92
92
|
import { MatPaginatorIntl, MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
|
|
93
|
-
import { LetDirective } from '@ngrx/component';
|
|
94
93
|
import { Meta } from '@angular/platform-browser';
|
|
95
94
|
import { tablerFolderOpen } from '@ng-icons/tabler-icons';
|
|
96
95
|
import * as i3$1 from '@angular/material/radio';
|
|
@@ -18907,14 +18906,29 @@ class Gn4FieldMapper {
|
|
|
18907
18906
|
featureTypes: selectField(source, 'featureTypes'),
|
|
18908
18907
|
}, output),
|
|
18909
18908
|
related: (output, source) => {
|
|
18910
|
-
const
|
|
18909
|
+
const related = selectField(source, 'related');
|
|
18910
|
+
const fcatSource = selectField(getFirstValue(selectField(related, 'fcats')) ?? {}, '_source');
|
|
18911
18911
|
const featureCatalogIdentifier = selectField(fcatSource, 'uuid');
|
|
18912
|
-
const sourceOfLinks = getAsArray(selectField(
|
|
18912
|
+
const sourceOfLinks = getAsArray(selectField(related, 'hassources'));
|
|
18913
18913
|
const sourceOfIdentifiers = sourceOfLinks
|
|
18914
18914
|
.filter((link) => link['origin'] === 'catalog')
|
|
18915
18915
|
.map((link) => {
|
|
18916
18916
|
return selectField(selectField(link, '_source'), 'uuid');
|
|
18917
18917
|
});
|
|
18918
|
+
const siblingLinks = getAsArray(selectField(related, 'siblings'));
|
|
18919
|
+
const siblings = siblingLinks
|
|
18920
|
+
.filter((link) => link['origin'] === 'catalog')
|
|
18921
|
+
.map((link) => ({
|
|
18922
|
+
uniqueIdentifier: selectField(selectField(link, '_source'), 'uuid'),
|
|
18923
|
+
associationType: getAssociationTypeFromCode(selectField(selectField(link, 'properties'), 'associationType')),
|
|
18924
|
+
}));
|
|
18925
|
+
const associatedLinks = getAsArray(selectField(related, 'associated'));
|
|
18926
|
+
const associatedIdentifiers = associatedLinks
|
|
18927
|
+
.filter((link) => link['origin'] === 'catalog')
|
|
18928
|
+
.map((link) => {
|
|
18929
|
+
return selectField(selectField(link, '_source'), 'uuid');
|
|
18930
|
+
})
|
|
18931
|
+
.filter((uuid) => !siblings.some((sibling) => sibling.uniqueIdentifier === uuid));
|
|
18918
18932
|
const extraValues = {};
|
|
18919
18933
|
if (featureCatalogIdentifier) {
|
|
18920
18934
|
extraValues.featureCatalogIdentifier = featureCatalogIdentifier;
|
|
@@ -18922,6 +18936,12 @@ class Gn4FieldMapper {
|
|
|
18922
18936
|
if (sourceOfIdentifiers && sourceOfIdentifiers.length > 0) {
|
|
18923
18937
|
extraValues.sourceOfIdentifiers = sourceOfIdentifiers;
|
|
18924
18938
|
}
|
|
18939
|
+
if (associatedIdentifiers && associatedIdentifiers.length > 0) {
|
|
18940
|
+
extraValues.associatedIdentifiers = associatedIdentifiers;
|
|
18941
|
+
}
|
|
18942
|
+
if (siblings && siblings.length > 0) {
|
|
18943
|
+
extraValues.siblings = siblings;
|
|
18944
|
+
}
|
|
18925
18945
|
return Object.keys(extraValues).length > 0
|
|
18926
18946
|
? this.addExtra(extraValues, output)
|
|
18927
18947
|
: output;
|
|
@@ -20369,7 +20389,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
|
|
|
20369
20389
|
}] } });
|
|
20370
20390
|
|
|
20371
20391
|
var name = "geonetwork-ui";
|
|
20372
|
-
var version = "2.11.0-dev.
|
|
20392
|
+
var version = "2.11.0-dev.68d9eaa3a";
|
|
20373
20393
|
var engines = {
|
|
20374
20394
|
node: ">=24"
|
|
20375
20395
|
};
|
|
@@ -20408,11 +20428,12 @@ var peerDependencies = {
|
|
|
20408
20428
|
};
|
|
20409
20429
|
var dependencies = {
|
|
20410
20430
|
"@biesbjerg/ngx-translate-extract-marker": "~1.0.0",
|
|
20411
|
-
"@camptocamp/ogc-client": "1.3.1-dev.
|
|
20412
|
-
"@
|
|
20413
|
-
"@geospatial-sdk/
|
|
20414
|
-
"@geospatial-sdk/
|
|
20415
|
-
"@geospatial-sdk/
|
|
20431
|
+
"@camptocamp/ogc-client": "1.3.1-dev.200c02a",
|
|
20432
|
+
"@duckdb/duckdb-wasm": "~1.32.0",
|
|
20433
|
+
"@geospatial-sdk/core": "0.0.5-dev.78",
|
|
20434
|
+
"@geospatial-sdk/geocoding": "0.0.5-dev.78",
|
|
20435
|
+
"@geospatial-sdk/legend": "0.0.5-dev.78",
|
|
20436
|
+
"@geospatial-sdk/openlayers": "0.0.5-dev.78",
|
|
20416
20437
|
"@ltd/j-toml": "~1.38.0",
|
|
20417
20438
|
"@messageformat/core": "~3.4.0",
|
|
20418
20439
|
"@ng-icons/core": "~32.5.0",
|
|
@@ -20420,7 +20441,7 @@ var dependencies = {
|
|
|
20420
20441
|
"@ng-icons/material-icons": "~32.5.0",
|
|
20421
20442
|
"@ng-icons/tabler-icons": "~32.4.0",
|
|
20422
20443
|
"@rgrove/parse-xml": "4.2.0",
|
|
20423
|
-
|
|
20444
|
+
"apache-arrow": "~17.0.0",
|
|
20424
20445
|
"chart.js": "~4.5.1",
|
|
20425
20446
|
"chroma-js": "~3.2.0",
|
|
20426
20447
|
"date-fns": "4.1.0",
|
|
@@ -20433,7 +20454,6 @@ var dependencies = {
|
|
|
20433
20454
|
"ngx-dropzone": "~3.1.0",
|
|
20434
20455
|
"ngx-translate-messageformat-compiler": "~7.2.0",
|
|
20435
20456
|
ol: "~10.8.0",
|
|
20436
|
-
papaparse: "~5.5.3",
|
|
20437
20457
|
proj4: "~2.20.4",
|
|
20438
20458
|
rdflib: "~2.3.5",
|
|
20439
20459
|
semver: "~7.7.4",
|
|
@@ -20678,6 +20698,7 @@ class ElasticsearchService {
|
|
|
20678
20698
|
values: uuids,
|
|
20679
20699
|
},
|
|
20680
20700
|
},
|
|
20701
|
+
size: uuids.length,
|
|
20681
20702
|
};
|
|
20682
20703
|
}
|
|
20683
20704
|
getRelatedRecordPayload(record, size = 6, _source = [...ES_SOURCE_SUMMARY, 'createDate']) {
|
|
@@ -21229,7 +21250,7 @@ class Gn4Repository {
|
|
|
21229
21250
|
}
|
|
21230
21251
|
getRecord(uniqueIdentifier) {
|
|
21231
21252
|
return this.gn4SearchApi
|
|
21232
|
-
.search('bucket', ['fcats', 'hassources'], JSON.stringify(this.gn4SearchHelper.getMetadataByIdsPayload([uniqueIdentifier])))
|
|
21253
|
+
.search('bucket', ['fcats', 'hassources', 'siblings', 'associated'], JSON.stringify(this.gn4SearchHelper.getMetadataByIdsPayload([uniqueIdentifier])))
|
|
21233
21254
|
.pipe(map$1((results) => results.hits.hits[0]), switchMap((record) => record ? this.gn4Mapper.readRecord(record) : of(null)));
|
|
21234
21255
|
}
|
|
21235
21256
|
getMultipleRecords(uniqueIdentifiers) {
|
|
@@ -21283,19 +21304,28 @@ class Gn4Repository {
|
|
|
21283
21304
|
.search('bucket', null, JSON.stringify(this.gn4SearchHelper.getRelatedRecordPayload(similarTo, 3)))
|
|
21284
21305
|
.pipe(switchMap((results) => this.gn4Mapper.readRecords(results.hits.hits)));
|
|
21285
21306
|
}
|
|
21286
|
-
|
|
21287
|
-
const
|
|
21288
|
-
|
|
21289
|
-
|
|
21290
|
-
|
|
21291
|
-
|
|
21292
|
-
|
|
21293
|
-
|
|
21294
|
-
|
|
21295
|
-
|
|
21296
|
-
|
|
21307
|
+
getLinkedRecords(record) {
|
|
21308
|
+
const siblings = (record.extras?.['siblings'] ?? []);
|
|
21309
|
+
const relations = [
|
|
21310
|
+
['source', (record.extras?.['sourcesIdentifiers'] ?? [])],
|
|
21311
|
+
['sourceOf', (record.extras?.['sourceOfIdentifiers'] ?? [])],
|
|
21312
|
+
['sibling', siblings.map(({ uniqueIdentifier }) => uniqueIdentifier)],
|
|
21313
|
+
[
|
|
21314
|
+
'associated',
|
|
21315
|
+
(record.extras?.['associatedIdentifiers'] ?? []),
|
|
21316
|
+
],
|
|
21317
|
+
];
|
|
21318
|
+
const requested = relations.filter(([, identifiers]) => identifiers.length > 0);
|
|
21319
|
+
if (requested.length === 0) {
|
|
21320
|
+
return of([]);
|
|
21297
21321
|
}
|
|
21298
|
-
return
|
|
21322
|
+
return forkJoin(requested.map(([relation, identifiers]) => this.getMultipleRecords(identifiers).pipe(map$1((records) => (records ?? []).map((record) => ({
|
|
21323
|
+
record,
|
|
21324
|
+
relation,
|
|
21325
|
+
associationType: relation === 'sibling'
|
|
21326
|
+
? siblings.find(({ uniqueIdentifier }) => uniqueIdentifier === record.uniqueIdentifier)?.associationType
|
|
21327
|
+
: undefined,
|
|
21328
|
+
}))), catchError(() => of([]))))).pipe(map$1((groups) => groups.flat()));
|
|
21299
21329
|
}
|
|
21300
21330
|
aggregate(params) {
|
|
21301
21331
|
// if aggregations are empty, return an empty object right away
|
|
@@ -32512,8 +32542,7 @@ const closeMetadata = createAction('[Metadata view] close');
|
|
|
32512
32542
|
Related actions
|
|
32513
32543
|
*/
|
|
32514
32544
|
const setRelated = createAction('[Metadata view] Set related records', props());
|
|
32515
|
-
const
|
|
32516
|
-
const setSourceOf = createAction('[Metadata view] Set has sources', props());
|
|
32545
|
+
const setLinkedRecords = createAction('[Metadata view] Set associated records', props());
|
|
32517
32546
|
/*
|
|
32518
32547
|
ChartConfig actions
|
|
32519
32548
|
*/
|
|
@@ -32545,9 +32574,8 @@ var mdview_actions = /*#__PURE__*/Object.freeze({
|
|
|
32545
32574
|
loadUserFeedbacksSuccess: loadUserFeedbacksSuccess,
|
|
32546
32575
|
setChartConfig: setChartConfig,
|
|
32547
32576
|
setIncompleteMetadata: setIncompleteMetadata,
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
setSources: setSources
|
|
32577
|
+
setLinkedRecords: setLinkedRecords,
|
|
32578
|
+
setRelated: setRelated
|
|
32551
32579
|
});
|
|
32552
32580
|
|
|
32553
32581
|
const METADATA_VIEW_FEATURE_STATE_KEY = 'metadataView';
|
|
@@ -32590,12 +32618,9 @@ on(loadFullMetadata, (state) => ({
|
|
|
32590
32618
|
on(setRelated, (state, { related }) => ({
|
|
32591
32619
|
...state,
|
|
32592
32620
|
related,
|
|
32593
|
-
})), on(
|
|
32621
|
+
})), on(setLinkedRecords, (state, { linkedRecords }) => ({
|
|
32594
32622
|
...state,
|
|
32595
|
-
|
|
32596
|
-
})), on(setSourceOf, (state, { sourceOf }) => ({
|
|
32597
|
-
...state,
|
|
32598
|
-
sourceOf,
|
|
32623
|
+
linkedRecords,
|
|
32599
32624
|
})),
|
|
32600
32625
|
/*
|
|
32601
32626
|
ChartConfig reducers
|
|
@@ -32657,8 +32682,7 @@ const getMetadataError = createSelector(getMdViewState, (state) => state.error);
|
|
|
32657
32682
|
Related selectors
|
|
32658
32683
|
*/
|
|
32659
32684
|
const getRelated = createSelector(getMdViewState, (state) => state.related);
|
|
32660
|
-
const
|
|
32661
|
-
const getSourceOf = createSelector(getMdViewState, (state) => state.sourceOf);
|
|
32685
|
+
const getLinkedRecords = createSelector(getMdViewState, (state) => state.linkedRecords);
|
|
32662
32686
|
/*
|
|
32663
32687
|
Metadata selectors
|
|
32664
32688
|
*/
|
|
@@ -32759,212 +32783,10 @@ function parseHeaders(httpHeaders) {
|
|
|
32759
32783
|
return result;
|
|
32760
32784
|
}
|
|
32761
32785
|
|
|
32762
|
-
|
|
32763
|
-
|
|
32764
|
-
|
|
32765
|
-
? fileExtensionMatches[1].toLowerCase()
|
|
32766
|
-
: null;
|
|
32767
|
-
// 1. type hint
|
|
32768
|
-
if (typeHint)
|
|
32769
|
-
return Promise.resolve(typeHint);
|
|
32770
|
-
// 2. content-type header
|
|
32771
|
-
const headers = await fetchHeaders(url);
|
|
32772
|
-
if ('supportedType' in headers)
|
|
32773
|
-
return headers.supportedType;
|
|
32774
|
-
// 3. file extension from url
|
|
32775
|
-
else if (SupportedTypes.indexOf(fileExtension) > -1)
|
|
32776
|
-
return fileExtension;
|
|
32777
|
-
// no type inferred or hinted
|
|
32778
|
-
if ('mimeType' in headers)
|
|
32779
|
-
throw FetchError.unsupportedType(headers.mimeType);
|
|
32780
|
-
else
|
|
32781
|
-
throw FetchError.unknownType();
|
|
32782
|
-
}
|
|
32783
|
-
function fetchHeaders(url) {
|
|
32784
|
-
return sharedFetch(url, 'HEAD')
|
|
32785
|
-
.catch((error) => {
|
|
32786
|
-
throw FetchError.corsOrNetwork(error.message);
|
|
32787
|
-
})
|
|
32788
|
-
.then((response) => {
|
|
32789
|
-
if (!response.ok) {
|
|
32790
|
-
throw FetchError.http(response.status);
|
|
32791
|
-
}
|
|
32792
|
-
return parseHeaders(response.headers);
|
|
32793
|
-
});
|
|
32794
|
-
}
|
|
32795
|
-
function fetchDataAsText(url, cacheActive) {
|
|
32796
|
-
const fetchFactory = () => sharedFetch(url)
|
|
32797
|
-
.catch((error) => {
|
|
32798
|
-
throw FetchError.corsOrNetwork(error.message);
|
|
32799
|
-
})
|
|
32800
|
-
.then(async (response) => {
|
|
32801
|
-
if (!response.ok) {
|
|
32802
|
-
const clonedResponse = response.clone();
|
|
32803
|
-
throw FetchError.http(response.status, await clonedResponse.text());
|
|
32804
|
-
}
|
|
32805
|
-
const clonedResponse = response.clone();
|
|
32806
|
-
return clonedResponse.text();
|
|
32807
|
-
});
|
|
32808
|
-
return cacheActive ? useCache(fetchFactory, url, 'asText') : fetchFactory();
|
|
32809
|
-
}
|
|
32810
|
-
function fetchDataAsArrayBuffer(url, cacheActive) {
|
|
32811
|
-
const fetchFactory = () => sharedFetch(url)
|
|
32812
|
-
.catch((error) => {
|
|
32813
|
-
throw FetchError.corsOrNetwork(error.message);
|
|
32814
|
-
})
|
|
32815
|
-
.then(async (response) => {
|
|
32816
|
-
if (!response.ok) {
|
|
32817
|
-
throw FetchError.http(response.status, await response.text());
|
|
32818
|
-
}
|
|
32819
|
-
// convert to a numeric array so that we can store the response in cache
|
|
32820
|
-
return Array.from(new Uint8Array(await response.arrayBuffer()));
|
|
32821
|
-
});
|
|
32822
|
-
return (cacheActive ? useCache(fetchFactory, url, 'asArrayBuffer') : fetchFactory()).then((array) => {
|
|
32823
|
-
return new Uint8Array(array).buffer;
|
|
32824
|
-
});
|
|
32825
|
-
}
|
|
32826
|
-
function tryParseDate(input) {
|
|
32827
|
-
if (typeof input !== 'string')
|
|
32828
|
-
return null;
|
|
32829
|
-
function tryIso(value) {
|
|
32830
|
-
const parsed = parseISO(value);
|
|
32831
|
-
return isNaN(parsed.getDate()) ? null : parsed;
|
|
32832
|
-
}
|
|
32833
|
-
function tryFormat(value, format) {
|
|
32834
|
-
const parsed = parse$4(value, format, new Date());
|
|
32835
|
-
return isNaN(parsed.getDate()) ? null : parsed;
|
|
32836
|
-
}
|
|
32837
|
-
return (tryIso(input) ||
|
|
32838
|
-
tryFormat(input, 'dd/MM/yyyy') ||
|
|
32839
|
-
tryFormat(input, 'dd.MM.yyyy') ||
|
|
32840
|
-
tryFormat(input, 'MM/dd/yyyy') ||
|
|
32841
|
-
null);
|
|
32842
|
-
}
|
|
32843
|
-
function tryParseNumber(input) {
|
|
32844
|
-
if (isNaN(input))
|
|
32845
|
-
return null;
|
|
32846
|
-
const parsed = parseFloat(input);
|
|
32847
|
-
return isNaN(parsed) ? null : parsed;
|
|
32848
|
-
}
|
|
32849
|
-
function jsonToGeojsonFeature(object) {
|
|
32850
|
-
const { id, properties } = Object.keys(object)
|
|
32851
|
-
.map((property) => (property ? property : 'unknown')) //prevent empty strings
|
|
32852
|
-
.reduce((prev, curr) => curr.toLowerCase().endsWith('id')
|
|
32853
|
-
? {
|
|
32854
|
-
...prev,
|
|
32855
|
-
id: object[curr],
|
|
32856
|
-
}
|
|
32857
|
-
: {
|
|
32858
|
-
...prev,
|
|
32859
|
-
properties: { ...prev.properties, [curr]: object[curr] },
|
|
32860
|
-
}, { id: undefined, properties: {} });
|
|
32861
|
-
return {
|
|
32862
|
-
type: 'Feature',
|
|
32863
|
-
geometry: null,
|
|
32864
|
-
properties,
|
|
32865
|
-
...(id !== undefined && { id }),
|
|
32866
|
-
};
|
|
32867
|
-
}
|
|
32868
|
-
function mutateProperties(items, mutators) {
|
|
32869
|
-
const mutatorKeys = Object.keys(mutators);
|
|
32870
|
-
for (let i = 0, ii = items.length; i < ii; i++) {
|
|
32871
|
-
const item = items[i];
|
|
32872
|
-
for (const mutatorField of mutatorKeys) {
|
|
32873
|
-
if (!(mutatorField in item.properties))
|
|
32874
|
-
continue;
|
|
32875
|
-
item.properties[mutatorField] = mutators[mutatorField](item.properties[mutatorField]);
|
|
32876
|
-
}
|
|
32877
|
-
}
|
|
32878
|
-
return items;
|
|
32786
|
+
const GEOMETRY_COLUMN_ALIAS = '__geometry__';
|
|
32787
|
+
function fieldToSql(name) {
|
|
32788
|
+
return `"${name.replace(/"/g, '""')}"`; // escape double quotes in field names
|
|
32879
32789
|
}
|
|
32880
|
-
const SAMPLE_SIZE = 20;
|
|
32881
|
-
/**
|
|
32882
|
-
* This will infer field types from a list of data items and cast the values accordingly
|
|
32883
|
-
* @param items
|
|
32884
|
-
* @param inferTypes
|
|
32885
|
-
*/
|
|
32886
|
-
function processItemProperties(items, inferTypes = false) {
|
|
32887
|
-
const foundFields = {};
|
|
32888
|
-
for (let i = 0, ii = Math.min(SAMPLE_SIZE, items.length); i < ii; i++) {
|
|
32889
|
-
const item = items[i];
|
|
32890
|
-
const fields = Object.keys(item.properties);
|
|
32891
|
-
for (const field of fields) {
|
|
32892
|
-
if (!(field in foundFields)) {
|
|
32893
|
-
foundFields[field] = {
|
|
32894
|
-
label: field,
|
|
32895
|
-
name: field,
|
|
32896
|
-
type: null,
|
|
32897
|
-
};
|
|
32898
|
-
}
|
|
32899
|
-
const value = item.properties[field];
|
|
32900
|
-
const info = foundFields[field];
|
|
32901
|
-
if (value === undefined || value === '' || value === null)
|
|
32902
|
-
continue;
|
|
32903
|
-
if (!inferTypes) {
|
|
32904
|
-
if (info.type === null && typeof value === 'number') {
|
|
32905
|
-
info.type = 'number';
|
|
32906
|
-
}
|
|
32907
|
-
else if (info.type === 'number' && typeof value !== 'number') {
|
|
32908
|
-
info.type = 'string';
|
|
32909
|
-
}
|
|
32910
|
-
continue;
|
|
32911
|
-
}
|
|
32912
|
-
const parsedNumber = tryParseNumber(value);
|
|
32913
|
-
if (info.type === null && parsedNumber !== null) {
|
|
32914
|
-
info.type = 'number';
|
|
32915
|
-
continue;
|
|
32916
|
-
}
|
|
32917
|
-
else if (info.type === 'number' && parsedNumber === null) {
|
|
32918
|
-
info.type = 'string';
|
|
32919
|
-
continue;
|
|
32920
|
-
}
|
|
32921
|
-
const parsedDate = tryParseDate(value);
|
|
32922
|
-
if (info.type === null && parsedDate !== null) {
|
|
32923
|
-
info.type = 'date';
|
|
32924
|
-
}
|
|
32925
|
-
else if (info.type === 'date' && parsedDate === null) {
|
|
32926
|
-
info.type = 'string';
|
|
32927
|
-
}
|
|
32928
|
-
}
|
|
32929
|
-
}
|
|
32930
|
-
const properties = [];
|
|
32931
|
-
const mutators = {};
|
|
32932
|
-
for (const field in foundFields) {
|
|
32933
|
-
const info = foundFields[field];
|
|
32934
|
-
if (info.type === 'number') {
|
|
32935
|
-
mutators[field] = tryParseNumber;
|
|
32936
|
-
}
|
|
32937
|
-
else if (info.type === 'date') {
|
|
32938
|
-
mutators[field] = tryParseDate;
|
|
32939
|
-
}
|
|
32940
|
-
properties.push({ ...info, type: info.type || 'string' });
|
|
32941
|
-
}
|
|
32942
|
-
if (inferTypes) {
|
|
32943
|
-
mutateProperties(items, mutators);
|
|
32944
|
-
}
|
|
32945
|
-
return { items, properties };
|
|
32946
|
-
}
|
|
32947
|
-
/**
|
|
32948
|
-
* This creates a Proxy that allows reading and writing to the data item properties
|
|
32949
|
-
* as if it was a simple array of JSON objects
|
|
32950
|
-
* @param items
|
|
32951
|
-
*/
|
|
32952
|
-
function getJsonDataItemsProxy(items) {
|
|
32953
|
-
return new Proxy(items, {
|
|
32954
|
-
get(target, p) {
|
|
32955
|
-
if (typeof p === 'string' &&
|
|
32956
|
-
!Number.isNaN(parseInt(p)) &&
|
|
32957
|
-
target[p]?.properties) {
|
|
32958
|
-
return target[p].properties;
|
|
32959
|
-
}
|
|
32960
|
-
return target[p];
|
|
32961
|
-
},
|
|
32962
|
-
set() {
|
|
32963
|
-
throw new Error('This object is read-only');
|
|
32964
|
-
},
|
|
32965
|
-
});
|
|
32966
|
-
}
|
|
32967
|
-
|
|
32968
32790
|
function filterToSql(filter) {
|
|
32969
32791
|
const operator = filter[0];
|
|
32970
32792
|
const args = filter.slice(1);
|
|
@@ -32979,10 +32801,10 @@ function filterToSql(filter) {
|
|
|
32979
32801
|
case '=':
|
|
32980
32802
|
case '!=':
|
|
32981
32803
|
case 'like':
|
|
32982
|
-
return
|
|
32804
|
+
return `${fieldToSql(args[0])} ${operator.toUpperCase()} ${valueToSql(args[1])}`;
|
|
32983
32805
|
case 'in': {
|
|
32984
32806
|
const values = args.slice(1);
|
|
32985
|
-
return
|
|
32807
|
+
return `${fieldToSql(args[0])} IN (${values.map(valueToSql).join(', ')})`;
|
|
32986
32808
|
}
|
|
32987
32809
|
case 'and':
|
|
32988
32810
|
case 'or': {
|
|
@@ -33001,17 +32823,18 @@ function aggregationToSql(aggregation) {
|
|
|
33001
32823
|
const field = aggregation[1];
|
|
33002
32824
|
switch (operation) {
|
|
33003
32825
|
case 'average':
|
|
33004
|
-
return `AVG(
|
|
32826
|
+
return `CAST(AVG(${fieldToSql(field)}) AS DOUBLE) as ${fieldToSql(`average(${field})`)}`;
|
|
33005
32827
|
case 'sum':
|
|
33006
32828
|
case 'max':
|
|
33007
32829
|
case 'min':
|
|
33008
|
-
return
|
|
32830
|
+
return `CAST(${operation.toUpperCase()}(${fieldToSql(field)}) AS DOUBLE) as ${fieldToSql(`${operation}(${field})`)}`;
|
|
33009
32831
|
case 'count':
|
|
33010
|
-
return 'COUNT(*) as
|
|
32832
|
+
return 'CAST(COUNT(*) AS INTEGER) as "count()"'; // we don't need Bigint precision here
|
|
33011
32833
|
}
|
|
33012
32834
|
}
|
|
33013
32835
|
/**
|
|
33014
32836
|
* Leave arguments at null if not used
|
|
32837
|
+
* @param tableName
|
|
33015
32838
|
* @param selected
|
|
33016
32839
|
* @param filter
|
|
33017
32840
|
* @param sort
|
|
@@ -33020,22 +32843,25 @@ function aggregationToSql(aggregation) {
|
|
|
33020
32843
|
* @param groupBy
|
|
33021
32844
|
* @param aggregations
|
|
33022
32845
|
*/
|
|
33023
|
-
function generateSqlQuery(selected = null, filter = null, sort = null, startIndex = null, count = null, groupBy = null, aggregations = null) {
|
|
32846
|
+
function generateSqlQuery(tableName, selected = null, filter = null, sort = null, startIndex = null, count = null, groupBy = null, aggregations = null, geometryColumn = null) {
|
|
33024
32847
|
let sqlSelect = 'SELECT *';
|
|
33025
|
-
const sqlFrom =
|
|
32848
|
+
const sqlFrom = ` FROM ${tableName}`;
|
|
33026
32849
|
let sqlOrderBy = '';
|
|
33027
32850
|
let sqlWhere = '';
|
|
33028
32851
|
let sqlLimit = '';
|
|
33029
32852
|
let sqlGroupBy = '';
|
|
33030
32853
|
if (selected !== null) {
|
|
33031
|
-
sqlSelect = `SELECT ${selected.map(
|
|
32854
|
+
sqlSelect = `SELECT ${selected.map(fieldToSql).join(', ')}`;
|
|
32855
|
+
}
|
|
32856
|
+
if (geometryColumn !== null) {
|
|
32857
|
+
sqlSelect += `, ST_AsGeoJSON(${fieldToSql(geometryColumn)}) as ${GEOMETRY_COLUMN_ALIAS}`;
|
|
33032
32858
|
}
|
|
33033
32859
|
if (filter !== null) {
|
|
33034
32860
|
sqlWhere = ` WHERE ${filterToSql(filter)}`;
|
|
33035
32861
|
}
|
|
33036
32862
|
if (sort?.length) {
|
|
33037
32863
|
sqlOrderBy = ` ORDER BY ${sort
|
|
33038
|
-
.map((sort) =>
|
|
32864
|
+
.map((sort) => `${fieldToSql(sort[1])} ${sort[0].toUpperCase()}`)
|
|
33039
32865
|
.join(', ')}`;
|
|
33040
32866
|
}
|
|
33041
32867
|
if (startIndex !== null && count !== null) {
|
|
@@ -33045,17 +32871,212 @@ function generateSqlQuery(selected = null, filter = null, sort = null, startInde
|
|
|
33045
32871
|
sqlSelect = `SELECT ${aggregations.map(aggregationToSql).join(', ')}`;
|
|
33046
32872
|
const groupedByDistinct = groupBy.filter((group) => group[0] === 'distinct');
|
|
33047
32873
|
const sqlGroupByFields = groupedByDistinct
|
|
33048
|
-
.map((group) =>
|
|
32874
|
+
.map((group) => fieldToSql(group[1]))
|
|
33049
32875
|
.join(', ');
|
|
33050
32876
|
const sqlGroupBySelect = groupedByDistinct
|
|
33051
|
-
.map((group) =>
|
|
32877
|
+
.map((group) => `${fieldToSql(group[1])} as ${fieldToSql(`distinct(${group[1]})`)}`)
|
|
33052
32878
|
.join(', ');
|
|
33053
32879
|
if (sqlGroupByFields && sqlGroupBySelect) {
|
|
33054
32880
|
sqlGroupBy = ` GROUP BY ${sqlGroupByFields}`;
|
|
33055
32881
|
sqlSelect += `, ${sqlGroupBySelect}`;
|
|
33056
32882
|
}
|
|
33057
32883
|
}
|
|
33058
|
-
return sqlSelect + sqlFrom + sqlGroupBy +
|
|
32884
|
+
return sqlSelect + sqlFrom + sqlGroupBy + sqlWhere + sqlOrderBy + sqlLimit;
|
|
32885
|
+
}
|
|
32886
|
+
|
|
32887
|
+
function arrowTableToDataItems(table) {
|
|
32888
|
+
const fields = table.schema.fields;
|
|
32889
|
+
return table.toArray().map((row) => {
|
|
32890
|
+
const rowJson = row.toJSON();
|
|
32891
|
+
const feature = {
|
|
32892
|
+
type: 'Feature',
|
|
32893
|
+
geometry: null,
|
|
32894
|
+
properties: {},
|
|
32895
|
+
};
|
|
32896
|
+
const keys = Object.keys(rowJson);
|
|
32897
|
+
for (let i = 0; i < keys.length; i++) {
|
|
32898
|
+
const key = keys[i];
|
|
32899
|
+
const dataType = fields[i].type;
|
|
32900
|
+
let value = rowJson[key];
|
|
32901
|
+
// this might happen if we get an array inside a field
|
|
32902
|
+
if (value instanceof Vector) {
|
|
32903
|
+
value = Array.from(value);
|
|
32904
|
+
}
|
|
32905
|
+
// cast bigints to ints
|
|
32906
|
+
if (typeof value === 'bigint') {
|
|
32907
|
+
value = Number(value);
|
|
32908
|
+
}
|
|
32909
|
+
// rename columns with empty name
|
|
32910
|
+
if (!key) {
|
|
32911
|
+
feature.properties['unknown'] = value;
|
|
32912
|
+
continue;
|
|
32913
|
+
}
|
|
32914
|
+
// assign properties that look like an id to the geojson `id` field
|
|
32915
|
+
if (/^(object|feature)?_?id$/.test(key.toLowerCase()) &&
|
|
32916
|
+
(typeof value == 'string' || typeof value === 'number')) {
|
|
32917
|
+
feature.id = value;
|
|
32918
|
+
}
|
|
32919
|
+
// if a date is in timestamp (number) format, convert it to native
|
|
32920
|
+
if (typeof value === 'number' &&
|
|
32921
|
+
(DataType.isTimestamp(dataType) || DataType.isDate(dataType))) {
|
|
32922
|
+
value = new Date(value);
|
|
32923
|
+
}
|
|
32924
|
+
// if a binary field (most likely a geometry column): skip
|
|
32925
|
+
if (DataType.isBinary(dataType)) {
|
|
32926
|
+
continue;
|
|
32927
|
+
}
|
|
32928
|
+
// geometry column
|
|
32929
|
+
if (key === GEOMETRY_COLUMN_ALIAS && DataType.isUtf8(dataType)) {
|
|
32930
|
+
feature.geometry = JSON.parse(value);
|
|
32931
|
+
continue;
|
|
32932
|
+
}
|
|
32933
|
+
feature.properties[key] = value;
|
|
32934
|
+
}
|
|
32935
|
+
return feature;
|
|
32936
|
+
});
|
|
32937
|
+
}
|
|
32938
|
+
|
|
32939
|
+
// init code taken from https://github.com/duckdb/duckdb-wasm/blob/main/packages/duckdb-wasm/README.md
|
|
32940
|
+
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
|
|
32941
|
+
// from https://duckdb.org/docs/current/sql/data_types/overview
|
|
32942
|
+
const typesMapping = {
|
|
32943
|
+
INTEGER: 'number',
|
|
32944
|
+
SMALLINT: 'number',
|
|
32945
|
+
TINYINT: 'number',
|
|
32946
|
+
BIGINT: 'number',
|
|
32947
|
+
HUGEINT: 'number',
|
|
32948
|
+
UINTEGER: 'number',
|
|
32949
|
+
USMALLINT: 'number',
|
|
32950
|
+
UTINYINT: 'number',
|
|
32951
|
+
UBIGINT: 'number',
|
|
32952
|
+
UHUGEINT: 'number',
|
|
32953
|
+
DOUBLE: 'number',
|
|
32954
|
+
BIGNUM: 'number',
|
|
32955
|
+
DECIMAL: 'number',
|
|
32956
|
+
NUMERIC: 'number',
|
|
32957
|
+
FLOAT: 'number',
|
|
32958
|
+
REAL: 'number',
|
|
32959
|
+
'TIMESTAMP WITH TIME ZONE': 'date',
|
|
32960
|
+
TIMESTAMP: 'date',
|
|
32961
|
+
DATE: 'date',
|
|
32962
|
+
CHAR: 'string',
|
|
32963
|
+
VARCHAR: 'string',
|
|
32964
|
+
TEXT: 'string',
|
|
32965
|
+
UUID: 'string',
|
|
32966
|
+
BLOB: 'string',
|
|
32967
|
+
BIT: 'string',
|
|
32968
|
+
INTERVAL: 'string',
|
|
32969
|
+
LIST: 'string',
|
|
32970
|
+
BOOLEAN: 'boolean',
|
|
32971
|
+
};
|
|
32972
|
+
class Engine {
|
|
32973
|
+
async makeInit() {
|
|
32974
|
+
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
|
|
32975
|
+
let worker_url = bundle.mainWorker;
|
|
32976
|
+
// this is necessary to let browsers execute WASM code coming from a cross-origin host
|
|
32977
|
+
if (worker_url.startsWith('https://')) {
|
|
32978
|
+
worker_url = URL.createObjectURL(new Blob([`importScripts("${worker_url}");`], {
|
|
32979
|
+
type: 'text/javascript',
|
|
32980
|
+
}));
|
|
32981
|
+
}
|
|
32982
|
+
const worker = new Worker(worker_url);
|
|
32983
|
+
const logger = new duckdb.ConsoleLogger(duckdb.LogLevel.WARNING);
|
|
32984
|
+
this.db = new duckdb.AsyncDuckDB(logger, worker);
|
|
32985
|
+
await this.db.instantiate(bundle.mainModule, bundle.pthreadWorker);
|
|
32986
|
+
// setup duckdb options
|
|
32987
|
+
const conn = await this.db.connect();
|
|
32988
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
32989
|
+
await conn.query(`INSTALL spatial; LOAD spatial;`);
|
|
32990
|
+
await conn.query(`SET TimeZone = '${timezone}';`);
|
|
32991
|
+
await conn.query(`SET threads = 1;`);
|
|
32992
|
+
// await conn.query(`SET memory_limit = '2GB';`) // used for experimenting when we encounter memory issues
|
|
32993
|
+
await conn.query(`SET preserve_insertion_order = false;`); // this reduces memory usage when loading a dataset
|
|
32994
|
+
conn.close();
|
|
32995
|
+
return this;
|
|
32996
|
+
}
|
|
32997
|
+
isReady() {
|
|
32998
|
+
if (!this.init_) {
|
|
32999
|
+
this.init_ = this.makeInit();
|
|
33000
|
+
}
|
|
33001
|
+
return this.init_;
|
|
33002
|
+
}
|
|
33003
|
+
/**
|
|
33004
|
+
* Returns information about a dataset once it's loaded:
|
|
33005
|
+
* - a list of properties description
|
|
33006
|
+
* - the name of the dataset geometry column (null if no geometry present)
|
|
33007
|
+
* @param datasetId name of the table under which the dataset will be stored
|
|
33008
|
+
* @param loadQuery duckdb-specific query for creating a table out of the data
|
|
33009
|
+
* @param forceReload if true, any existing data will be dropped and redownloaded
|
|
33010
|
+
*/
|
|
33011
|
+
async loadFile(datasetId, loadQuery, forceReload = false) {
|
|
33012
|
+
const conn = await this.db.connect();
|
|
33013
|
+
let results;
|
|
33014
|
+
// either we want to recreate the table, or we keep it if it already exists
|
|
33015
|
+
const safeLoadQuery = forceReload
|
|
33016
|
+
? `DROP TABLE IF EXISTS ${datasetId};
|
|
33017
|
+
${loadQuery}`
|
|
33018
|
+
: loadQuery.replace(/CREATE TABLE(?! IF NOT EXISTS)/gi, 'CREATE TABLE IF NOT EXISTS');
|
|
33019
|
+
// create the table
|
|
33020
|
+
try {
|
|
33021
|
+
results = await conn.query(safeLoadQuery);
|
|
33022
|
+
}
|
|
33023
|
+
catch (e) {
|
|
33024
|
+
throw new FetchError('parse', `DuckDB encountered an error when loading the data: ${e.message}`);
|
|
33025
|
+
}
|
|
33026
|
+
// read rows count
|
|
33027
|
+
results = await conn.query(`SELECT count(*) FROM ${datasetId}`);
|
|
33028
|
+
const { 'count_star()': recordsCount } = results.toArray()[0].toJSON();
|
|
33029
|
+
// read columns
|
|
33030
|
+
results = await conn.query(`SELECT * FROM information_schema.columns WHERE table_name = '${datasetId}';`);
|
|
33031
|
+
let geometryColumn = null;
|
|
33032
|
+
const properties = results
|
|
33033
|
+
.toArray()
|
|
33034
|
+
.map((row) => {
|
|
33035
|
+
const rowObj = row.toJSON();
|
|
33036
|
+
if (rowObj['data_type'] === 'GEOMETRY') {
|
|
33037
|
+
// the geometry is not part of the properties
|
|
33038
|
+
// note: right now we only keep one geometry column name, but if there are multiple they will
|
|
33039
|
+
// all get discarded
|
|
33040
|
+
geometryColumn = rowObj['column_name'];
|
|
33041
|
+
return null;
|
|
33042
|
+
}
|
|
33043
|
+
return {
|
|
33044
|
+
name: rowObj['column_name'],
|
|
33045
|
+
label: rowObj['column_name'],
|
|
33046
|
+
type: typesMapping[rowObj['data_type']] ?? 'other',
|
|
33047
|
+
};
|
|
33048
|
+
})
|
|
33049
|
+
.filter((prop) => prop !== null);
|
|
33050
|
+
conn.close();
|
|
33051
|
+
return {
|
|
33052
|
+
properties,
|
|
33053
|
+
geometryColumn,
|
|
33054
|
+
rowsCount: Number(recordsCount),
|
|
33055
|
+
};
|
|
33056
|
+
}
|
|
33057
|
+
// register a Uint8 buffer using a handle in the duckdb instance
|
|
33058
|
+
async registerData(name, buffer) {
|
|
33059
|
+
return this.db.registerFileBuffer(name, buffer);
|
|
33060
|
+
}
|
|
33061
|
+
/**
|
|
33062
|
+
* @param query duckdb-specific query for fetching items
|
|
33063
|
+
*/
|
|
33064
|
+
async queryItems(query) {
|
|
33065
|
+
const conn = await this.db.connect();
|
|
33066
|
+
const results = await conn.query(query);
|
|
33067
|
+
conn.close();
|
|
33068
|
+
return arrowTableToDataItems(results);
|
|
33069
|
+
}
|
|
33070
|
+
close() {
|
|
33071
|
+
this.db?.terminate();
|
|
33072
|
+
}
|
|
33073
|
+
}
|
|
33074
|
+
let engine = null;
|
|
33075
|
+
async function getEngine() {
|
|
33076
|
+
if (!engine) {
|
|
33077
|
+
engine = new Engine();
|
|
33078
|
+
}
|
|
33079
|
+
return engine.isReady();
|
|
33059
33080
|
}
|
|
33060
33081
|
|
|
33061
33082
|
class BaseReader {
|
|
@@ -33068,10 +33089,18 @@ class BaseReader {
|
|
|
33068
33089
|
this.sort = null;
|
|
33069
33090
|
this.startIndex = null;
|
|
33070
33091
|
this.count = null;
|
|
33092
|
+
this.loadPromise_ = Promise.resolve();
|
|
33093
|
+
this.cacheEnabled = false;
|
|
33094
|
+
}
|
|
33095
|
+
enableCache(enabled) {
|
|
33096
|
+
this.cacheEnabled = enabled;
|
|
33071
33097
|
}
|
|
33072
33098
|
load() {
|
|
33073
33099
|
throw new Error('not implemented');
|
|
33074
33100
|
}
|
|
33101
|
+
get isLoaded() {
|
|
33102
|
+
return this.loadPromise_;
|
|
33103
|
+
}
|
|
33075
33104
|
get properties() {
|
|
33076
33105
|
throw new Error('not implemented');
|
|
33077
33106
|
}
|
|
@@ -33120,178 +33149,237 @@ class BaseReader {
|
|
|
33120
33149
|
}
|
|
33121
33150
|
}
|
|
33122
33151
|
|
|
33123
|
-
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
33128
|
-
|
|
33129
|
-
|
|
33130
|
-
|
|
33131
|
-
|
|
33132
|
-
|
|
33133
|
-
|
|
33134
|
-
|
|
33135
|
-
|
|
33152
|
+
/**
|
|
33153
|
+
* This reader handles file formats supported natively by DuckDB
|
|
33154
|
+
*/
|
|
33155
|
+
class BaseFileReader extends BaseReader {
|
|
33156
|
+
// a table id should not exceed 63 chars
|
|
33157
|
+
generateDatasetId() {
|
|
33158
|
+
// generate a hash out of the url
|
|
33159
|
+
let hash = 0;
|
|
33160
|
+
for (const char of this.url) {
|
|
33161
|
+
hash = (hash << 5) - hash + char.charCodeAt(0);
|
|
33162
|
+
hash = hash >>> 0; // make it unsigned
|
|
33163
|
+
}
|
|
33164
|
+
return `datafetcher_${hash.toString(16)}`;
|
|
33165
|
+
}
|
|
33166
|
+
async getLoadQuery() {
|
|
33136
33167
|
throw new Error('not implemented');
|
|
33137
33168
|
}
|
|
33138
|
-
load() {
|
|
33139
|
-
this.
|
|
33169
|
+
async load() {
|
|
33170
|
+
this.datasetId = this.generateDatasetId();
|
|
33171
|
+
this.loadPromise_ = getEngine()
|
|
33172
|
+
.then((engine) => {
|
|
33173
|
+
this.engine = engine;
|
|
33174
|
+
return this.getLoadQuery();
|
|
33175
|
+
})
|
|
33176
|
+
.then((loadQuery) => this.engine.loadFile(this.datasetId, loadQuery, !this.cacheEnabled))
|
|
33177
|
+
.then((datasetInfo) => {
|
|
33178
|
+
this.properties_ = datasetInfo.properties;
|
|
33179
|
+
this.geometryColumn = datasetInfo.geometryColumn;
|
|
33180
|
+
this.rowsCount = datasetInfo.rowsCount;
|
|
33181
|
+
// returns void for the loaded promise
|
|
33182
|
+
});
|
|
33140
33183
|
}
|
|
33141
33184
|
get properties() {
|
|
33142
|
-
return this.
|
|
33185
|
+
return this.isLoaded.then(() => this.properties_);
|
|
33143
33186
|
}
|
|
33144
33187
|
get info() {
|
|
33145
|
-
return this.
|
|
33146
|
-
itemsCount:
|
|
33188
|
+
return this.isLoaded.then(() => ({
|
|
33189
|
+
itemsCount: this.rowsCount,
|
|
33190
|
+
hasGeometry: !!this.geometryColumn,
|
|
33147
33191
|
}));
|
|
33148
33192
|
}
|
|
33149
33193
|
async read() {
|
|
33150
|
-
|
|
33151
|
-
//
|
|
33152
|
-
|
|
33153
|
-
|
|
33154
|
-
|
|
33155
|
-
this.sort == null &&
|
|
33156
|
-
this.filter == null &&
|
|
33157
|
-
this.startIndex == null &&
|
|
33158
|
-
this.count == null) {
|
|
33159
|
-
return items;
|
|
33160
|
-
}
|
|
33161
|
-
const jsonItems = getJsonDataItemsProxy(items);
|
|
33162
|
-
const query = generateSqlQuery(this.selected, this.filter, this.sort, this.startIndex, this.count, this.groupedBy, this.aggregations);
|
|
33163
|
-
const result = await import('alasql').then((module) => module.default(query, [jsonItems]));
|
|
33164
|
-
return result.map(jsonToGeojsonFeature);
|
|
33165
|
-
}
|
|
33166
|
-
}
|
|
33167
|
-
|
|
33168
|
-
function parseCsv(text) {
|
|
33169
|
-
// first parse the header to guess the delimiter
|
|
33170
|
-
// note that we do that to not rely on Papaparse logic for guessing delimiter
|
|
33171
|
-
let delimiter;
|
|
33172
|
-
try {
|
|
33173
|
-
const header = text.split('\n')[0];
|
|
33174
|
-
const result = Papa.parse(header, {
|
|
33175
|
-
header: false,
|
|
33176
|
-
});
|
|
33177
|
-
delimiter = result.meta.delimiter;
|
|
33178
|
-
}
|
|
33179
|
-
catch (e) {
|
|
33180
|
-
throw new Error('CSV parsing failed: the delimiter could not be guessed');
|
|
33181
|
-
}
|
|
33182
|
-
const parsed = Papa.parse(text, {
|
|
33183
|
-
header: true,
|
|
33184
|
-
skipEmptyLines: true,
|
|
33185
|
-
delimiter,
|
|
33186
|
-
});
|
|
33187
|
-
if (parsed.errors.length) {
|
|
33188
|
-
throw new Error('CSV parsing failed for the following reasons:\n' +
|
|
33189
|
-
parsed.errors
|
|
33190
|
-
.map((error) => `* ${error.message} at row ${error.row}, column ${error.index}`)
|
|
33191
|
-
.join('\n'));
|
|
33194
|
+
await this.isLoaded;
|
|
33195
|
+
// if only certain fields are selected, omit the geometry
|
|
33196
|
+
const geometryColumn = this.selected === null ? this.geometryColumn : null;
|
|
33197
|
+
const query = generateSqlQuery(this.datasetId, this.selected, this.filter, this.sort, this.startIndex, this.count, this.groupedBy, this.aggregations, geometryColumn);
|
|
33198
|
+
return this.engine.queryItems(query);
|
|
33192
33199
|
}
|
|
33193
|
-
const items = parsed.data.map(jsonToGeojsonFeature);
|
|
33194
|
-
return processItemProperties(items, true);
|
|
33195
33200
|
}
|
|
33201
|
+
|
|
33196
33202
|
class CsvReader extends BaseFileReader {
|
|
33197
|
-
|
|
33198
|
-
|
|
33203
|
+
async getLoadQuery() {
|
|
33204
|
+
// first we get a list of columns hich have a detected type of VARCHAR
|
|
33205
|
+
// 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
|
|
33206
|
+
return `
|
|
33207
|
+
SET VARIABLE strColumns = (SELECT list(name) FROM (SELECT unnest(Columns, recursive := true) FROM sniff_csv("${this.url}")) WHERE type = 'VARCHAR');
|
|
33208
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM read_csv("${this.url}",
|
|
33209
|
+
force_not_null = getvariable('strColumns'),
|
|
33210
|
+
auto_type_candidates = ['NULL', 'BOOLEAN', 'INTEGER', 'DOUBLE', 'DATE', 'VARCHAR']
|
|
33211
|
+
);`;
|
|
33199
33212
|
}
|
|
33200
33213
|
}
|
|
33201
33214
|
|
|
33202
|
-
/**
|
|
33203
|
-
* This parser only supports arrays of simple flat objects with properties
|
|
33204
|
-
* @param text
|
|
33205
|
-
*/
|
|
33206
|
-
function parseJson(text) {
|
|
33207
|
-
const parsed = JSON.parse(text);
|
|
33208
|
-
if (!Array.isArray(parsed)) {
|
|
33209
|
-
throw new Error('Could not parse JSON, expected an array at root level');
|
|
33210
|
-
}
|
|
33211
|
-
return processItemProperties(parsed.map(jsonToGeojsonFeature));
|
|
33212
|
-
}
|
|
33213
33215
|
class JsonReader extends BaseFileReader {
|
|
33214
|
-
|
|
33215
|
-
return
|
|
33216
|
+
async getLoadQuery() {
|
|
33217
|
+
return `
|
|
33218
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM read_json("${this.url}",
|
|
33219
|
+
maximum_object_size = 536870912 -- 500MB
|
|
33220
|
+
);`;
|
|
33216
33221
|
}
|
|
33217
33222
|
}
|
|
33218
33223
|
|
|
33219
|
-
/**
|
|
33220
|
-
* This parser supports both Geojson Feature collections or arrays
|
|
33221
|
-
* of Features
|
|
33222
|
-
* @param text
|
|
33223
|
-
*/
|
|
33224
|
-
function parseGeojson(text) {
|
|
33225
|
-
const parsed = JSON.parse(text);
|
|
33226
|
-
const features = parsed.type === 'FeatureCollection' ? parsed.features : parsed;
|
|
33227
|
-
if (!Array.isArray(features)) {
|
|
33228
|
-
throw new Error('Could not parse GeoJSON, expected a features collection or an array of features at root level');
|
|
33229
|
-
}
|
|
33230
|
-
return processItemProperties(features);
|
|
33231
|
-
}
|
|
33232
33224
|
class GeojsonReader extends BaseFileReader {
|
|
33233
|
-
|
|
33234
|
-
return
|
|
33225
|
+
async getLoadQuery() {
|
|
33226
|
+
return `
|
|
33227
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM st_read("${this.url}",
|
|
33228
|
+
allowed_drivers = ['GeoJSON']
|
|
33229
|
+
);`;
|
|
33235
33230
|
}
|
|
33236
33231
|
}
|
|
33237
33232
|
|
|
33233
|
+
async function inferDatasetType(url, typeHint) {
|
|
33234
|
+
const fileExtensionMatches = new URL(url, typeof window !== 'undefined' ? window.location.toString() : undefined).pathname.match(/\.(.+)$/);
|
|
33235
|
+
const fileExtension = fileExtensionMatches && fileExtensionMatches.length
|
|
33236
|
+
? fileExtensionMatches[1].toLowerCase()
|
|
33237
|
+
: null;
|
|
33238
|
+
// 1. type hint
|
|
33239
|
+
if (typeHint)
|
|
33240
|
+
return Promise.resolve(typeHint);
|
|
33241
|
+
// 2. content-type header
|
|
33242
|
+
const headers = await fetchHeaders(url);
|
|
33243
|
+
if ('supportedType' in headers)
|
|
33244
|
+
return headers.supportedType;
|
|
33245
|
+
// 3. file extension from url
|
|
33246
|
+
else if (SupportedTypes.indexOf(fileExtension) > -1)
|
|
33247
|
+
return fileExtension;
|
|
33248
|
+
// no type inferred or hinted
|
|
33249
|
+
if ('mimeType' in headers)
|
|
33250
|
+
throw FetchError.unsupportedType(headers.mimeType);
|
|
33251
|
+
else
|
|
33252
|
+
throw FetchError.unknownType();
|
|
33253
|
+
}
|
|
33254
|
+
function fetchHeaders(url) {
|
|
33255
|
+
return sharedFetch(url, 'HEAD')
|
|
33256
|
+
.catch((error) => {
|
|
33257
|
+
throw FetchError.corsOrNetwork(error.message);
|
|
33258
|
+
})
|
|
33259
|
+
.then((response) => {
|
|
33260
|
+
if (!response.ok) {
|
|
33261
|
+
throw FetchError.http(response.status);
|
|
33262
|
+
}
|
|
33263
|
+
return parseHeaders(response.headers);
|
|
33264
|
+
});
|
|
33265
|
+
}
|
|
33266
|
+
function fetchDataAsText(url, cacheActive) {
|
|
33267
|
+
const fetchFactory = () => sharedFetch(url)
|
|
33268
|
+
.catch((error) => {
|
|
33269
|
+
throw FetchError.corsOrNetwork(error.message);
|
|
33270
|
+
})
|
|
33271
|
+
.then(async (response) => {
|
|
33272
|
+
if (!response.ok) {
|
|
33273
|
+
const clonedResponse = response.clone();
|
|
33274
|
+
throw FetchError.http(response.status, await clonedResponse.text());
|
|
33275
|
+
}
|
|
33276
|
+
const clonedResponse = response.clone();
|
|
33277
|
+
return clonedResponse.text();
|
|
33278
|
+
});
|
|
33279
|
+
return cacheActive ? useCache(fetchFactory, url, 'asText') : fetchFactory();
|
|
33280
|
+
}
|
|
33281
|
+
function fetchDataAsArrayBuffer(url, cacheActive) {
|
|
33282
|
+
const fetchFactory = () => sharedFetch(url)
|
|
33283
|
+
.catch((error) => {
|
|
33284
|
+
throw FetchError.corsOrNetwork(error.message);
|
|
33285
|
+
})
|
|
33286
|
+
.then(async (response) => {
|
|
33287
|
+
if (!response.ok) {
|
|
33288
|
+
throw FetchError.http(response.status, await response.text());
|
|
33289
|
+
}
|
|
33290
|
+
// convert to a numeric array so that we can store the response in cache
|
|
33291
|
+
return Array.from(new Uint8Array(await response.arrayBuffer()));
|
|
33292
|
+
});
|
|
33293
|
+
return (cacheActive ? useCache(fetchFactory, url, 'asArrayBuffer') : fetchFactory()).then((array) => {
|
|
33294
|
+
return new Uint8Array(array).buffer;
|
|
33295
|
+
});
|
|
33296
|
+
}
|
|
33238
33297
|
/**
|
|
33239
|
-
* This
|
|
33240
|
-
*
|
|
33241
|
-
* @param
|
|
33298
|
+
* This creates a Proxy that allows reading and writing to the data item properties
|
|
33299
|
+
* as if it was a simple array of JSON objects
|
|
33300
|
+
* @param items
|
|
33242
33301
|
*/
|
|
33243
|
-
function
|
|
33244
|
-
return
|
|
33245
|
-
|
|
33246
|
-
|
|
33247
|
-
|
|
33248
|
-
|
|
33249
|
-
|
|
33250
|
-
|
|
33251
|
-
|
|
33302
|
+
function getJsonDataItemsProxy(items) {
|
|
33303
|
+
return new Proxy(items, {
|
|
33304
|
+
get(target, p) {
|
|
33305
|
+
if (typeof p === 'string' &&
|
|
33306
|
+
!Number.isNaN(parseInt(p)) &&
|
|
33307
|
+
target[p]?.properties) {
|
|
33308
|
+
return target[p].properties;
|
|
33309
|
+
}
|
|
33310
|
+
return target[p];
|
|
33311
|
+
},
|
|
33312
|
+
set() {
|
|
33313
|
+
throw new Error('This object is read-only');
|
|
33314
|
+
},
|
|
33252
33315
|
});
|
|
33253
33316
|
}
|
|
33317
|
+
|
|
33254
33318
|
class ExcelReader extends BaseFileReader {
|
|
33255
|
-
|
|
33256
|
-
|
|
33319
|
+
async getLoadQuery() {
|
|
33320
|
+
// we download the file as an array buffer first, in order to be able to check if it's an XLS file
|
|
33321
|
+
let buffer = await fetchDataAsArrayBuffer(this.url, this.cacheEnabled);
|
|
33322
|
+
const bufferHandle = `B${this.datasetId}`;
|
|
33323
|
+
// checking against the magic number at the beginning of XLS files, see https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
33324
|
+
const magicNumber = new Uint8Array(buffer, 0, 8); // first 8 bytes
|
|
33325
|
+
const isXls = Array.from(magicNumber)
|
|
33326
|
+
.map((n) => n.toString(16).toUpperCase())
|
|
33327
|
+
.join(' ') === 'D0 CF 11 E0 A1 B1 1A E1';
|
|
33328
|
+
// uh oh, this is an XLS file (not supported by duckdb); convert it to CSV using the xlsx package
|
|
33329
|
+
if (isXls) {
|
|
33330
|
+
buffer = await import('xlsx').then(({ read, utils }) => {
|
|
33331
|
+
const workbook = read(buffer);
|
|
33332
|
+
const json = utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
|
|
33333
|
+
return new TextEncoder().encode(JSON.stringify(json)).buffer;
|
|
33334
|
+
});
|
|
33335
|
+
}
|
|
33336
|
+
const duckDbFn = isXls ? 'read_json' : 'read_xlsx';
|
|
33337
|
+
await this.engine.registerData(bufferHandle, new Uint8Array(buffer));
|
|
33338
|
+
return `
|
|
33339
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM ${duckDbFn}("${bufferHandle}", ignore_errors = true);`;
|
|
33340
|
+
}
|
|
33341
|
+
}
|
|
33342
|
+
|
|
33343
|
+
class GmlReader extends BaseFileReader {
|
|
33344
|
+
async getLoadQuery() {
|
|
33345
|
+
return `
|
|
33346
|
+
CREATE TABLE ${this.datasetId} AS SELECT * FROM st_read("${this.url}",
|
|
33347
|
+
allowed_drivers = ['GML']
|
|
33348
|
+
);`;
|
|
33257
33349
|
}
|
|
33258
33350
|
}
|
|
33259
33351
|
|
|
33260
|
-
|
|
33261
|
-
|
|
33262
|
-
const
|
|
33352
|
+
const formatGeojson = new GeoJSON$1();
|
|
33353
|
+
function parseGeojson(text) {
|
|
33354
|
+
const parsed = JSON.parse(text);
|
|
33355
|
+
const features = parsed.type === 'FeatureCollection' ? parsed.features : parsed;
|
|
33356
|
+
if (!Array.isArray(features)) {
|
|
33357
|
+
throw new Error('Could not parse GeoJSON, expected a features collection or an array of features at root level');
|
|
33358
|
+
}
|
|
33359
|
+
return features;
|
|
33360
|
+
}
|
|
33361
|
+
function parseGml(text, featureType, version) {
|
|
33362
|
+
const parts = featureType.split(':');
|
|
33363
|
+
const regex = new RegExp(`xmlns:${parts[0]}=["']([^'"]*)["']`);
|
|
33263
33364
|
const match = regex.exec(text);
|
|
33264
33365
|
if (match && match.length >= 2) {
|
|
33265
|
-
const
|
|
33366
|
+
const wfs = new WFS({
|
|
33266
33367
|
featureNS: match[1],
|
|
33267
|
-
featureType:
|
|
33368
|
+
featureType: parts[1],
|
|
33268
33369
|
version: version,
|
|
33269
33370
|
});
|
|
33270
33371
|
let features;
|
|
33271
33372
|
try {
|
|
33272
|
-
features =
|
|
33373
|
+
features = wfs.readFeatures(text);
|
|
33273
33374
|
}
|
|
33274
33375
|
catch (e) {
|
|
33275
|
-
throw Error(
|
|
33376
|
+
throw Error(`Couldn't parse WFS with GML features: ${e.message}`);
|
|
33276
33377
|
}
|
|
33277
|
-
const geojsonItem =
|
|
33278
|
-
return
|
|
33378
|
+
const geojsonItem = formatGeojson.writeFeaturesObject(features);
|
|
33379
|
+
return geojsonItem.features;
|
|
33279
33380
|
}
|
|
33280
33381
|
throw Error("Couldn't retrieve namespace url");
|
|
33281
33382
|
}
|
|
33282
|
-
class GmlReader extends BaseFileReader {
|
|
33283
|
-
constructor(url, namespace, version, cacheActive = true) {
|
|
33284
|
-
super(url);
|
|
33285
|
-
this.url = url;
|
|
33286
|
-
this.namespace = namespace;
|
|
33287
|
-
this.version = version;
|
|
33288
|
-
this.cacheActive = cacheActive;
|
|
33289
|
-
}
|
|
33290
|
-
getData() {
|
|
33291
|
-
return fetchDataAsText(this.url, this.cacheActive).then((text) => parseGml(text, this.namespace, this.version));
|
|
33292
|
-
}
|
|
33293
|
-
}
|
|
33294
|
-
|
|
33295
33383
|
async function getWfsEndpoint(wfsUrl) {
|
|
33296
33384
|
try {
|
|
33297
33385
|
return await new WfsEndpoint(wfsUrl).isReady();
|
|
@@ -33319,21 +33407,65 @@ async function getWfsEndpoint(wfsUrl) {
|
|
|
33319
33407
|
}
|
|
33320
33408
|
}
|
|
33321
33409
|
}
|
|
33322
|
-
class WfsReader extends
|
|
33323
|
-
constructor(url,
|
|
33324
|
-
super(url
|
|
33325
|
-
this.endpoint =
|
|
33326
|
-
this.
|
|
33327
|
-
|
|
33410
|
+
class WfsReader extends BaseReader {
|
|
33411
|
+
constructor(url, featureTypeName) {
|
|
33412
|
+
super(url);
|
|
33413
|
+
this.endpoint = getWfsEndpoint(url);
|
|
33414
|
+
this.featureType = this.endpoint
|
|
33415
|
+
.then((endpoint) => {
|
|
33416
|
+
const featureTypes = endpoint.getFeatureTypes();
|
|
33417
|
+
return endpoint.getFeatureTypeFull(featureTypes.length === 1 && !featureTypeName
|
|
33418
|
+
? featureTypes[0].name
|
|
33419
|
+
: featureTypeName);
|
|
33420
|
+
})
|
|
33421
|
+
.then((featureType) => {
|
|
33422
|
+
if (!featureType) {
|
|
33423
|
+
throw new Error('wfs.featuretype.notfound');
|
|
33424
|
+
}
|
|
33425
|
+
return featureType;
|
|
33426
|
+
});
|
|
33427
|
+
}
|
|
33428
|
+
get backupReader() {
|
|
33429
|
+
if (this.backupReader_) {
|
|
33430
|
+
return this.backupReader_;
|
|
33431
|
+
}
|
|
33432
|
+
this.backupReader_ = Promise.all([this.endpoint, this.featureType]).then(([endpoint, featureType]) => {
|
|
33433
|
+
let reader;
|
|
33434
|
+
if (endpoint.supportsJson(featureType.name)) {
|
|
33435
|
+
reader = new GeojsonReader(endpoint.getFeatureUrl(featureType.name, {
|
|
33436
|
+
asJson: true,
|
|
33437
|
+
outputCrs: 'EPSG:4326',
|
|
33438
|
+
}));
|
|
33439
|
+
}
|
|
33440
|
+
else {
|
|
33441
|
+
if (featureType.outputFormats.find((f) => f.toLowerCase().includes('gml')) &&
|
|
33442
|
+
(featureType.defaultCrs === 'EPSG:4326' ||
|
|
33443
|
+
featureType.otherCrs?.includes('EPSG:4326'))) {
|
|
33444
|
+
reader = new GmlReader(endpoint.getFeatureUrl(featureType.name, {
|
|
33445
|
+
outputFormat: featureType.outputFormats.find((f) => f.toLowerCase().includes('gml')),
|
|
33446
|
+
outputCrs: 'EPSG:4326',
|
|
33447
|
+
}));
|
|
33448
|
+
}
|
|
33449
|
+
}
|
|
33450
|
+
reader.enableCache(this.cacheEnabled);
|
|
33451
|
+
reader.load();
|
|
33452
|
+
return reader;
|
|
33453
|
+
});
|
|
33454
|
+
return this.backupReader_;
|
|
33328
33455
|
}
|
|
33329
33456
|
get properties() {
|
|
33330
|
-
return this.
|
|
33331
|
-
.getFeatureTypeFull(this.featureTypeName)
|
|
33332
|
-
.then((featureType) => Object.keys(featureType.properties).map((prop) => {
|
|
33457
|
+
return this.featureType.then((featureType) => Object.keys(featureType.properties).map((prop) => {
|
|
33333
33458
|
const originalType = featureType.properties[prop];
|
|
33334
|
-
|
|
33335
|
-
|
|
33336
|
-
|
|
33459
|
+
let type;
|
|
33460
|
+
if (originalType === 'float' || originalType === 'integer') {
|
|
33461
|
+
type = 'number';
|
|
33462
|
+
}
|
|
33463
|
+
else if (originalType === 'boolean') {
|
|
33464
|
+
type = 'string'; // we don't handle booleans yet in the data fetcher
|
|
33465
|
+
}
|
|
33466
|
+
else {
|
|
33467
|
+
type = originalType;
|
|
33468
|
+
}
|
|
33337
33469
|
return {
|
|
33338
33470
|
name: prop,
|
|
33339
33471
|
label: prop,
|
|
@@ -33342,83 +33474,63 @@ class WfsReader extends BaseCacheReader {
|
|
|
33342
33474
|
}));
|
|
33343
33475
|
}
|
|
33344
33476
|
get info() {
|
|
33345
|
-
return this.
|
|
33477
|
+
return this.featureType.then((result) => ({
|
|
33346
33478
|
itemsCount: result.objectCount,
|
|
33479
|
+
hasGeometry: !!result.geometryName,
|
|
33347
33480
|
}));
|
|
33348
33481
|
}
|
|
33349
|
-
|
|
33350
|
-
|
|
33351
|
-
const featureTypes = wfsEndpoint.getFeatureTypes();
|
|
33352
|
-
const featureType = wfsEndpoint.getFeatureTypeSummary(featureTypes.length === 1 && !featureTypeName
|
|
33353
|
-
? featureTypes[0].name
|
|
33354
|
-
: featureTypeName);
|
|
33355
|
-
if (!featureType) {
|
|
33356
|
-
throw new Error('wfs.featuretype.notfound');
|
|
33357
|
-
}
|
|
33358
|
-
if (wfsEndpoint.supportsStartIndex()) {
|
|
33359
|
-
return new WfsReader(wfsUrlEndpoint, wfsEndpoint, featureType.name);
|
|
33360
|
-
}
|
|
33361
|
-
else if (wfsEndpoint.supportsJson(featureType.name)) {
|
|
33362
|
-
return new GeojsonReader(wfsEndpoint.getFeatureUrl(featureType.name, {
|
|
33363
|
-
asJson: true,
|
|
33364
|
-
outputCrs: 'EPSG:4326',
|
|
33365
|
-
}));
|
|
33366
|
-
}
|
|
33367
|
-
else {
|
|
33368
|
-
if (featureType.outputFormats.find((f) => f.toLowerCase().includes('gml')) &&
|
|
33369
|
-
(featureType.defaultCrs === 'EPSG:4326' ||
|
|
33370
|
-
featureType.otherCrs?.includes('EPSG:4326'))) {
|
|
33371
|
-
return new GmlReader(wfsEndpoint.getFeatureUrl(featureType.name, {
|
|
33372
|
-
outputFormat: featureType.outputFormats.find((f) => f.toLowerCase().includes('gml')),
|
|
33373
|
-
outputCrs: 'EPSG:4326',
|
|
33374
|
-
}), featureType.name, wfsEndpoint.getVersion());
|
|
33375
|
-
}
|
|
33376
|
-
throw new Error('wfs.geojsongml.notsupported');
|
|
33377
|
-
}
|
|
33482
|
+
load() {
|
|
33483
|
+
// Nothing to load for Wfs
|
|
33378
33484
|
}
|
|
33379
|
-
async
|
|
33380
|
-
|
|
33381
|
-
|
|
33485
|
+
async read() {
|
|
33486
|
+
const endpoint = await this.endpoint;
|
|
33487
|
+
const featureType = await this.featureType;
|
|
33488
|
+
// if we can't use the WFS protocol we fall back to the backup reader
|
|
33489
|
+
if (this.aggregations || this.groupedBy || !endpoint.supportsStartIndex()) {
|
|
33490
|
+
const backupReader = await this.backupReader;
|
|
33491
|
+
backupReader.selectAll();
|
|
33492
|
+
if (this.selected) {
|
|
33493
|
+
backupReader.select(...this.selected);
|
|
33494
|
+
}
|
|
33495
|
+
if (this.aggregations) {
|
|
33496
|
+
backupReader.aggregate(...this.aggregations);
|
|
33497
|
+
}
|
|
33498
|
+
if (this.groupedBy) {
|
|
33499
|
+
backupReader.groupBy(...this.groupedBy);
|
|
33500
|
+
}
|
|
33501
|
+
if (this.sort) {
|
|
33502
|
+
backupReader.orderBy(...this.sort);
|
|
33503
|
+
}
|
|
33504
|
+
if (this.startIndex !== null && this.count !== null) {
|
|
33505
|
+
backupReader.limit(this.startIndex, this.count);
|
|
33506
|
+
}
|
|
33507
|
+
return backupReader.read();
|
|
33382
33508
|
}
|
|
33383
|
-
const asJson =
|
|
33509
|
+
const asJson = endpoint.supportsJson(featureType.name);
|
|
33384
33510
|
const attributes = this.selected ?? undefined;
|
|
33385
|
-
let
|
|
33511
|
+
let sortBy = null;
|
|
33512
|
+
if (this.sort) {
|
|
33513
|
+
const mapSort = (s) => [s[0] === 'desc' ? 'D' : 'A', s[1]];
|
|
33514
|
+
sortBy = Array.isArray(this.sort[0])
|
|
33515
|
+
? this.sort.map(mapSort)
|
|
33516
|
+
: mapSort(this.sort);
|
|
33517
|
+
}
|
|
33518
|
+
const url = endpoint.getFeatureUrl(featureType.name, {
|
|
33386
33519
|
...(this.startIndex !== null && { startIndex: this.startIndex }),
|
|
33387
33520
|
...(this.count !== null && { maxFeatures: this.count }),
|
|
33388
33521
|
asJson,
|
|
33389
33522
|
outputCrs: 'EPSG:4326',
|
|
33390
33523
|
attributes,
|
|
33391
|
-
|
|
33524
|
+
sortBy,
|
|
33392
33525
|
});
|
|
33393
|
-
|
|
33394
|
-
const finalUrl = new URL(url);
|
|
33395
|
-
const sorts = this.sort
|
|
33396
|
-
.map((fieldSort) => `${fieldSort[1]}+${fieldSort[0] === 'asc' ? 'A' : 'D'}`)
|
|
33397
|
-
.join(',');
|
|
33398
|
-
// Direct update on string url to prevent encoding of +A and +D
|
|
33399
|
-
url = `${url}${finalUrl.search ? '&' : ''}SORTBY=${sorts}`;
|
|
33400
|
-
}
|
|
33401
|
-
return fetchDataAsText(url, this.cacheActive).then((text) => asJson
|
|
33526
|
+
return fetchDataAsText(url, this.cacheEnabled).then((text) => asJson
|
|
33402
33527
|
? parseGeojson(text)
|
|
33403
|
-
: parseGml(text,
|
|
33404
|
-
}
|
|
33405
|
-
async getQueryData() {
|
|
33406
|
-
const items = (await this.getData()).items;
|
|
33407
|
-
const jsonItems = getJsonDataItemsProxy(items);
|
|
33408
|
-
const query = generateSqlQuery(this.selected, this.filter, this.sort, this.startIndex, this.count, this.groupedBy, this.aggregations);
|
|
33409
|
-
const result = await import('alasql').then((module) => module.default(query, [jsonItems]));
|
|
33410
|
-
return result.map(jsonToGeojsonFeature);
|
|
33411
|
-
}
|
|
33412
|
-
load() {
|
|
33413
|
-
// Nothing to load for Wfs
|
|
33414
|
-
}
|
|
33415
|
-
async read() {
|
|
33416
|
-
return (await this.getData(this.aggregations, this.groupedBy)).items;
|
|
33528
|
+
: parseGml(text, featureType.name, endpoint.getVersion()));
|
|
33417
33529
|
}
|
|
33418
33530
|
}
|
|
33419
33531
|
|
|
33420
|
-
async function openDataset(url,
|
|
33421
|
-
const fileType = await inferDatasetType(url, typeHint);
|
|
33532
|
+
async function openDataset(url, options) {
|
|
33533
|
+
const fileType = await inferDatasetType(url, options?.typeHint);
|
|
33422
33534
|
let reader;
|
|
33423
33535
|
try {
|
|
33424
33536
|
switch (fileType) {
|
|
@@ -33435,18 +33547,18 @@ async function openDataset(url, typeHint, options, cacheActive) {
|
|
|
33435
33547
|
reader = new ExcelReader(url);
|
|
33436
33548
|
break;
|
|
33437
33549
|
case 'gml':
|
|
33438
|
-
reader = new GmlReader(url
|
|
33550
|
+
reader = new GmlReader(url);
|
|
33439
33551
|
break;
|
|
33440
33552
|
case 'wfs':
|
|
33441
|
-
reader =
|
|
33553
|
+
reader = new WfsReader(url, options?.wfsFeatureType);
|
|
33442
33554
|
break;
|
|
33443
33555
|
}
|
|
33444
|
-
reader.
|
|
33556
|
+
reader.enableCache(options?.enableCache ?? true);
|
|
33445
33557
|
reader.load();
|
|
33446
33558
|
return reader;
|
|
33447
33559
|
}
|
|
33448
33560
|
catch (e) {
|
|
33449
|
-
//WfsReader may already raise a FetchError
|
|
33561
|
+
// WfsReader may already raise a FetchError
|
|
33450
33562
|
if (e instanceof FetchError)
|
|
33451
33563
|
throw e;
|
|
33452
33564
|
else
|
|
@@ -33457,13 +33569,13 @@ async function openDataset(url, typeHint, options, cacheActive) {
|
|
|
33457
33569
|
* This fetches the full dataset at the given URL and parses it according to its mime type.
|
|
33458
33570
|
* All items in the dataset are converted to GeoJSON features, even if they do not bear any spatial geometry.
|
|
33459
33571
|
* File type can be either inferred (from the HTTP headers or the URL), or hinted using the 2nd argument
|
|
33460
|
-
* File type is determined
|
|
33572
|
+
* File type is determined like so:
|
|
33461
33573
|
* 1. if a type hint is given, use it
|
|
33462
33574
|
* 2. otherwise, look for a Content-Type header in the response with a supported mime type
|
|
33463
33575
|
* 3. if no valid mime type was found, look for an explicit file extension in the url (.csv, .geojson etc.)
|
|
33464
33576
|
*/
|
|
33465
|
-
async function readDataset(url,
|
|
33466
|
-
const reader = await openDataset(url,
|
|
33577
|
+
async function readDataset(url, options) {
|
|
33578
|
+
const reader = await openDataset(url, options);
|
|
33467
33579
|
try {
|
|
33468
33580
|
return await reader.read();
|
|
33469
33581
|
}
|
|
@@ -33694,9 +33806,11 @@ class DataService {
|
|
|
33694
33806
|
getDataset(link, cacheActive) {
|
|
33695
33807
|
if (link.type === 'service' && link.accessServiceProtocol === 'wfs') {
|
|
33696
33808
|
const wfsUrlEndpoint = this.proxy.getProxiedUrl(link.url.toString());
|
|
33697
|
-
return from(openDataset(wfsUrlEndpoint,
|
|
33809
|
+
return from(openDataset(wfsUrlEndpoint, {
|
|
33810
|
+
typeHint: 'wfs',
|
|
33698
33811
|
wfsFeatureType: link.name,
|
|
33699
|
-
|
|
33812
|
+
enableCache: cacheActive,
|
|
33813
|
+
}));
|
|
33700
33814
|
}
|
|
33701
33815
|
else if (link.type === 'download') {
|
|
33702
33816
|
const linkProxifiedUrl = this.proxy.getProxiedUrl(link.url.toString());
|
|
@@ -33704,12 +33818,15 @@ class DataService {
|
|
|
33704
33818
|
const supportedType = SupportedTypes.indexOf(format) > -1
|
|
33705
33819
|
? format
|
|
33706
33820
|
: undefined;
|
|
33707
|
-
return from(openDataset(linkProxifiedUrl,
|
|
33821
|
+
return from(openDataset(linkProxifiedUrl, {
|
|
33822
|
+
typeHint: supportedType,
|
|
33823
|
+
enableCache: cacheActive,
|
|
33824
|
+
})).pipe();
|
|
33708
33825
|
}
|
|
33709
33826
|
else if (link.type === 'service' &&
|
|
33710
33827
|
link.accessServiceProtocol === 'esriRest') {
|
|
33711
33828
|
const url = this.getDownloadUrlFromEsriRest(link.url.toString(), 'geojson');
|
|
33712
|
-
return from(openDataset(url, 'geojson',
|
|
33829
|
+
return from(openDataset(url, { typeHint: 'geojson', enableCache: cacheActive })).pipe();
|
|
33713
33830
|
}
|
|
33714
33831
|
else if (link.type === 'service' &&
|
|
33715
33832
|
link.accessServiceProtocol === 'ogcFeatures') {
|
|
@@ -33723,7 +33840,10 @@ class DataService {
|
|
|
33723
33840
|
}
|
|
33724
33841
|
const urlWithoutLimit = new URL(geojsonUrl);
|
|
33725
33842
|
urlWithoutLimit.searchParams.delete('limit');
|
|
33726
|
-
return openDataset(urlWithoutLimit.toString(),
|
|
33843
|
+
return openDataset(urlWithoutLimit.toString(), {
|
|
33844
|
+
typeHint: 'geojson',
|
|
33845
|
+
enableCache: cacheActive,
|
|
33846
|
+
});
|
|
33727
33847
|
}));
|
|
33728
33848
|
}
|
|
33729
33849
|
return throwError(() => 'protocol not supported');
|
|
@@ -33975,20 +34095,31 @@ class DataTableComponent {
|
|
|
33975
34095
|
this.eltRef = inject(ElementRef);
|
|
33976
34096
|
this.cdr = inject(ChangeDetectorRef);
|
|
33977
34097
|
this.translateService = inject(TranslateService);
|
|
33978
|
-
this.
|
|
34098
|
+
this.columnsFromFeatureCatalog = null;
|
|
34099
|
+
this.columnsFromDataset = [];
|
|
33979
34100
|
this.selected = new EventEmitter();
|
|
33980
|
-
this.properties$ = new BehaviorSubject(null);
|
|
33981
34101
|
this.loading$ = new BehaviorSubject(false);
|
|
33982
34102
|
this.error = null;
|
|
33983
34103
|
}
|
|
33984
34104
|
set featureAttributes(value) {
|
|
33985
|
-
this.
|
|
33986
|
-
|
|
34105
|
+
this.columnsFromFeatureCatalog = value.map((attrs) => ({
|
|
34106
|
+
name: attrs.value,
|
|
34107
|
+
label: attrs.label,
|
|
34108
|
+
}));
|
|
33987
34109
|
}
|
|
33988
34110
|
set dataset(value) {
|
|
33989
34111
|
this.dataset_ = value;
|
|
33990
34112
|
this.dataset_.load();
|
|
33991
|
-
this.dataset_.info.then((info) =>
|
|
34113
|
+
this.dataset_.info.then((info) => {
|
|
34114
|
+
this.count = info.itemsCount;
|
|
34115
|
+
this.cdr.detectChanges();
|
|
34116
|
+
});
|
|
34117
|
+
}
|
|
34118
|
+
get columns() {
|
|
34119
|
+
return this.columnsFromFeatureCatalog ?? this.columnsFromDataset;
|
|
34120
|
+
}
|
|
34121
|
+
get columnNames() {
|
|
34122
|
+
return this.columns.map((c) => c.name);
|
|
33992
34123
|
}
|
|
33993
34124
|
ngOnInit() {
|
|
33994
34125
|
this.dataSource = new DataTableDataSource();
|
|
@@ -34023,11 +34154,12 @@ class DataTableComponent {
|
|
|
34023
34154
|
}
|
|
34024
34155
|
async readData() {
|
|
34025
34156
|
this.loading$.next(true);
|
|
34026
|
-
// wait for properties to be read
|
|
34027
|
-
const properties = await firstValueFrom(this.properties$.pipe(filter((p) => !!p)));
|
|
34028
|
-
const propsWithoutGeom = properties.filter((p) => !p.toLowerCase().startsWith('geom'));
|
|
34029
|
-
this.dataset_.select(...propsWithoutGeom);
|
|
34030
34157
|
try {
|
|
34158
|
+
// wait for properties to be read
|
|
34159
|
+
if (!this.columnsFromFeatureCatalog) {
|
|
34160
|
+
this.columnsFromDataset = await this.dataset_.properties;
|
|
34161
|
+
}
|
|
34162
|
+
this.dataset_.select(...this.columnNames);
|
|
34031
34163
|
await this.dataSource.showData(this.dataset_.read());
|
|
34032
34164
|
this.error = null;
|
|
34033
34165
|
}
|
|
@@ -34057,7 +34189,7 @@ class DataTableComponent {
|
|
|
34057
34189
|
}
|
|
34058
34190
|
}
|
|
34059
34191
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DataTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
34060
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: DataTableComponent, isStandalone: true, selector: "gn-ui-data-table", inputs: { featureAttributes: "featureAttributes", dataset: "dataset", activeId: "activeId" }, outputs: { selected: "selected" }, providers: [{ provide: MatPaginatorIntl, useClass: CustomMatPaginatorIntl }], viewQueries: [{ propertyName: "sort", first: true, predicate: MatSort, descendants: true }, { propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"flex flex-col border border-gray-300 rounded-lg bg-white h-full\">\n <div class=\"flex-1 overflow-y-hidden overflow-x-auto rounded-lg relative\">\n <table\n mat-table\n [dataSource]=\"dataSource\"\n matSort\n (matSortChange)=\"setSort($event)\"\n [matSortDisableClear]=\"true\"\n
|
|
34192
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.19", type: DataTableComponent, isStandalone: true, selector: "gn-ui-data-table", inputs: { featureAttributes: "featureAttributes", dataset: "dataset", activeId: "activeId" }, outputs: { selected: "selected" }, providers: [{ provide: MatPaginatorIntl, useClass: CustomMatPaginatorIntl }], viewQueries: [{ propertyName: "sort", first: true, predicate: MatSort, descendants: true }, { propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"flex flex-col border border-gray-300 rounded-lg bg-white h-full\">\n <div class=\"flex-1 overflow-y-hidden overflow-x-auto rounded-lg relative\">\n <table\n mat-table\n [dataSource]=\"dataSource\"\n matSort\n (matSortChange)=\"setSort($event)\"\n [matSortDisableClear]=\"true\"\n >\n @for (column of columns; track column) {\n <ng-container [matColumnDef]=\"column.name\">\n <th\n mat-header-cell\n *matHeaderCellDef\n mat-sort-header\n class=\"text-sm text-black bg-white\"\n >\n {{ column.label }}\n </th>\n <td\n mat-cell\n *matCellDef=\"let element\"\n class=\"whitespace-nowrap pr-1 truncate\"\n >\n {{ element[column.name] }}\n </td>\n </ng-container>\n }\n\n <tr mat-header-row *matHeaderRowDef=\"columnNames; sticky: true\"></tr>\n <tr\n [id]=\"getRowEltId(row.id)\"\n mat-row\n *matRowDef=\"let row; columns: columnNames\"\n (click)=\"selected.emit(row)\"\n [class.active]=\"\n activeId !== undefined && activeId !== null && row.id === activeId\n \"\n ></tr>\n </table>\n @if (loading$ | async) {\n <gn-ui-loading-mask\n class=\"sticky inset-0\"\n [message]=\"'table.loading.data' | translate\"\n ></gn-ui-loading-mask>\n }\n @if (error) {\n <gn-ui-popup-alert\n type=\"warning\"\n icon=\"matErrorOutlineOutline\"\n class=\"absolute m-2 inset-0 z-[100]\"\n >\n <span translate>{{ error }}</span>\n </gn-ui-popup-alert>\n }\n </div>\n <div class=\"flex justify-between items-center overflow-hidden\">\n <div class=\"text-gray-900 px-4 py-2 text-sm\">\n <span class=\"count font-extrabold text-primary\">{{ count }}</span\n > <span translate>table.object.count</span>.\n </div>\n\n <mat-paginator\n class=\"my-[-16px]\"\n (page)=\"setPagination()\"\n [length]=\"count\"\n [pageSize]=\"10\"\n [showFirstLastButtons]=\"true\"\n [hidePageSize]=\"true\"\n ></mat-paginator>\n </div>\n</div>\n", styles: ["table{width:100%;background:#fff}th.mat-mdc-header-cell,td.mat-mdc-cell,td.mat-mdc-footer-cell{padding-right:20px}tr.mat-mdc-row,tr.mat-mdc-footer-row{height:36px}tr:hover{background:#f5f5f5}tr.mat-mdc-header-row{height:48px}[mat-header-cell]{color:#0000008a;font-size:12px;font-weight:500}tr{cursor:pointer}.active .mat-mdc-cell{color:var(--color-primary)}.mat-mdc-paginator{background:none}\n"], dependencies: [{ kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i1$b.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i1$b.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i1$b.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i1$b.MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: i1$b.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i1$b.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i1$b.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i1$b.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i1$b.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i1$b.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i2$4.MatSort, selector: "[matSort]", inputs: ["matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear", "matSortDisabled"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i2$4.MatSortHeader, selector: "[mat-sort-header]", inputs: ["mat-sort-header", "arrowPosition", "start", "disabled", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i3.MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: LoadingMaskComponent, selector: "gn-ui-loading-mask", inputs: ["message"] }, { kind: "component", type: PopupAlertComponent, selector: "gn-ui-popup-alert", inputs: ["icon", "type", "position"] }, { kind: "directive", type: TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
34061
34193
|
}
|
|
34062
34194
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: DataTableComponent, decorators: [{
|
|
34063
34195
|
type: Component,
|
|
@@ -34069,10 +34201,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImpo
|
|
|
34069
34201
|
CommonModule,
|
|
34070
34202
|
LoadingMaskComponent,
|
|
34071
34203
|
PopupAlertComponent,
|
|
34072
|
-
LetDirective,
|
|
34073
34204
|
TranslatePipe,
|
|
34074
34205
|
TranslateDirective,
|
|
34075
|
-
], providers: [{ provide: MatPaginatorIntl, useClass: CustomMatPaginatorIntl }], selector: 'gn-ui-data-table', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col border border-gray-300 rounded-lg bg-white h-full\">\n <div class=\"flex-1 overflow-y-hidden overflow-x-auto rounded-lg relative\">\n <table\n mat-table\n [dataSource]=\"dataSource\"\n matSort\n (matSortChange)=\"setSort($event)\"\n [matSortDisableClear]=\"true\"\n
|
|
34206
|
+
], providers: [{ provide: MatPaginatorIntl, useClass: CustomMatPaginatorIntl }], selector: 'gn-ui-data-table', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col border border-gray-300 rounded-lg bg-white h-full\">\n <div class=\"flex-1 overflow-y-hidden overflow-x-auto rounded-lg relative\">\n <table\n mat-table\n [dataSource]=\"dataSource\"\n matSort\n (matSortChange)=\"setSort($event)\"\n [matSortDisableClear]=\"true\"\n >\n @for (column of columns; track column) {\n <ng-container [matColumnDef]=\"column.name\">\n <th\n mat-header-cell\n *matHeaderCellDef\n mat-sort-header\n class=\"text-sm text-black bg-white\"\n >\n {{ column.label }}\n </th>\n <td\n mat-cell\n *matCellDef=\"let element\"\n class=\"whitespace-nowrap pr-1 truncate\"\n >\n {{ element[column.name] }}\n </td>\n </ng-container>\n }\n\n <tr mat-header-row *matHeaderRowDef=\"columnNames; sticky: true\"></tr>\n <tr\n [id]=\"getRowEltId(row.id)\"\n mat-row\n *matRowDef=\"let row; columns: columnNames\"\n (click)=\"selected.emit(row)\"\n [class.active]=\"\n activeId !== undefined && activeId !== null && row.id === activeId\n \"\n ></tr>\n </table>\n @if (loading$ | async) {\n <gn-ui-loading-mask\n class=\"sticky inset-0\"\n [message]=\"'table.loading.data' | translate\"\n ></gn-ui-loading-mask>\n }\n @if (error) {\n <gn-ui-popup-alert\n type=\"warning\"\n icon=\"matErrorOutlineOutline\"\n class=\"absolute m-2 inset-0 z-[100]\"\n >\n <span translate>{{ error }}</span>\n </gn-ui-popup-alert>\n }\n </div>\n <div class=\"flex justify-between items-center overflow-hidden\">\n <div class=\"text-gray-900 px-4 py-2 text-sm\">\n <span class=\"count font-extrabold text-primary\">{{ count }}</span\n > <span translate>table.object.count</span>.\n </div>\n\n <mat-paginator\n class=\"my-[-16px]\"\n (page)=\"setPagination()\"\n [length]=\"count\"\n [pageSize]=\"10\"\n [showFirstLastButtons]=\"true\"\n [hidePageSize]=\"true\"\n ></mat-paginator>\n </div>\n</div>\n", styles: ["table{width:100%;background:#fff}th.mat-mdc-header-cell,td.mat-mdc-cell,td.mat-mdc-footer-cell{padding-right:20px}tr.mat-mdc-row,tr.mat-mdc-footer-row{height:36px}tr:hover{background:#f5f5f5}tr.mat-mdc-header-row{height:48px}[mat-header-cell]{color:#0000008a;font-size:12px;font-weight:500}tr{cursor:pointer}.active .mat-mdc-cell{color:var(--color-primary)}.mat-mdc-paginator{background:none}\n"] }]
|
|
34076
34207
|
}], propDecorators: { featureAttributes: [{
|
|
34077
34208
|
type: Input
|
|
34078
34209
|
}], dataset: [{
|
|
@@ -34175,7 +34306,7 @@ class ChartViewComponent {
|
|
|
34175
34306
|
}), shareReplay$1(1));
|
|
34176
34307
|
this.properties$ = combineLatest([this.dataset$, this.featureCatalog$]).pipe(switchMap$1(([dataset, catalog]) => this.setProperties(dataset, catalog)), shareReplay$1(1));
|
|
34177
34308
|
this.yChoices$ = this.properties$.pipe(map$1((properties) => properties
|
|
34178
|
-
.filter((prop) => prop.type === 'number'
|
|
34309
|
+
.filter((prop) => prop.type === 'number')
|
|
34179
34310
|
.map((prop) => ({ value: prop.name, label: prop.label || prop.name }))), tap$1((choices) => {
|
|
34180
34311
|
if (!choices.find((choice) => choice.value === this.yProperty$.value)) {
|
|
34181
34312
|
const newProp = choices[0]?.value || '';
|
|
@@ -34202,6 +34333,7 @@ class ChartViewComponent {
|
|
|
34202
34333
|
this.aggregation$,
|
|
34203
34334
|
]).pipe(filter$1(([_, x, y]) => !!x || !!y), switchMap$1(([dataset, xProp, yProp, aggregation]) => {
|
|
34204
34335
|
const fieldAgg = aggregation === 'count' ? ['count'] : [aggregation, yProp];
|
|
34336
|
+
this.loading = true;
|
|
34205
34337
|
return dataset
|
|
34206
34338
|
.groupBy(['distinct', xProp])
|
|
34207
34339
|
.aggregate(fieldAgg)
|
|
@@ -34443,6 +34575,7 @@ class GeoTableViewComponent {
|
|
|
34443
34575
|
}));
|
|
34444
34576
|
}
|
|
34445
34577
|
async initMapContext() {
|
|
34578
|
+
this.dataset.load();
|
|
34446
34579
|
this.dataset.selectAll();
|
|
34447
34580
|
return {
|
|
34448
34581
|
layers: [
|
|
@@ -34616,8 +34749,7 @@ class MdViewFacade {
|
|
|
34616
34749
|
}));
|
|
34617
34750
|
this.error$ = this.store.pipe(select(getMetadataError));
|
|
34618
34751
|
this.related$ = this.store.pipe(select(getRelated));
|
|
34619
|
-
this.
|
|
34620
|
-
this.sourceOf$ = this.store.pipe(select(getSourceOf));
|
|
34752
|
+
this.linkedRecords$ = this.store.pipe(select(getLinkedRecords));
|
|
34621
34753
|
this.chartConfig$ = this.store.pipe(select(getChartConfig));
|
|
34622
34754
|
this.allLinks$ = this.metadata$.pipe(map$1((record) => 'onlineResources' in record ? record.onlineResources : []), shareReplay$1(1));
|
|
34623
34755
|
this.resourceDoi$ = this.metadata$.pipe(map$1((record) => {
|
|
@@ -34736,12 +34868,9 @@ class MdViewEffects {
|
|
|
34736
34868
|
this.loadRelatedRecords$ = createEffect(() => this.actions$.pipe(ofType(loadFullMetadataSuccess), switchMap$1(({ full }) => this.recordsRepository.getSimilarRecords(full)), map$1((related) => {
|
|
34737
34869
|
return setRelated({ related });
|
|
34738
34870
|
}), catchError(() => of(setRelated({ related: null })))));
|
|
34739
|
-
this.
|
|
34740
|
-
return
|
|
34741
|
-
}), catchError(() => of(
|
|
34742
|
-
this.loadSourceOf$ = createEffect(() => this.actions$.pipe(ofType(loadFullMetadataSuccess), switchMap$1(({ full }) => this.recordsRepository.getSourceOf(full)), map$1((sourceOf) => {
|
|
34743
|
-
return setSourceOf({ sourceOf });
|
|
34744
|
-
}), catchError(() => of(setSourceOf({ sourceOf: null })))));
|
|
34871
|
+
this.loadLinkedRecords$ = createEffect(() => this.actions$.pipe(ofType(loadFullMetadataSuccess), switchMap$1(({ full }) => this.recordsRepository.getLinkedRecords(full)), map$1((linkedRecords) => {
|
|
34872
|
+
return setLinkedRecords({ linkedRecords });
|
|
34873
|
+
}), catchError(() => of(setLinkedRecords({ linkedRecords: null })))));
|
|
34745
34874
|
/*
|
|
34746
34875
|
UserFeedback effects
|
|
34747
34876
|
*/
|