@walkthru-earth/objex 1.1.0 → 1.2.1
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/README.md +3 -1
- package/dist/components/browser/FileBrowser.svelte +25 -14
- package/dist/components/browser/FileTreeSidebar.svelte +43 -7
- package/dist/components/layout/ConnectionDialog.svelte +100 -1
- package/dist/components/layout/Sidebar.svelte +70 -25
- package/dist/components/viewers/ArchiveViewer.svelte +4 -4
- package/dist/components/viewers/CodeViewer.svelte +44 -5
- package/dist/components/viewers/CogControls.svelte +208 -0
- package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
- package/dist/components/viewers/CogViewer.svelte +373 -1162
- package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
- package/dist/components/viewers/CopcViewer.svelte +20 -2
- package/dist/components/viewers/DatabaseViewer.svelte +345 -37
- package/dist/components/viewers/FlatGeobufViewer.svelte +15 -9
- package/dist/components/viewers/MarkdownViewer.svelte +1 -1
- package/dist/components/viewers/PmtilesViewer.svelte +2 -2
- package/dist/components/viewers/StacMapViewer.svelte +25 -9
- package/dist/components/viewers/TableViewer.svelte +162 -51
- package/dist/components/viewers/ZarrMapViewer.svelte +33 -4
- package/dist/components/viewers/ZarrViewer.svelte +3 -6
- package/dist/components/viewers/pmtiles/PmtilesMapView.svelte +0 -1
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +6 -2
- package/dist/file-icons/index.d.ts +1 -1
- package/dist/file-icons/index.js +12 -2
- package/dist/i18n/ar.js +25 -0
- package/dist/i18n/en.js +25 -0
- package/dist/i18n/index.svelte.d.ts +0 -1
- package/dist/i18n/index.svelte.js +0 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/query/engine.d.ts +20 -4
- package/dist/query/index.d.ts +2 -1
- package/dist/query/index.js +1 -0
- package/dist/query/source.d.ts +42 -0
- package/dist/query/source.js +54 -0
- package/dist/query/wasm.d.ts +7 -5
- package/dist/query/wasm.js +267 -107
- package/dist/storage/adapter.d.ts +9 -0
- package/dist/storage/adapter.js +13 -1
- package/dist/storage/browser-azure.d.ts +1 -1
- package/dist/storage/browser-azure.js +4 -0
- package/dist/storage/browser-cloud.d.ts +1 -1
- package/dist/storage/browser-cloud.js +7 -0
- package/dist/storage/presign.d.ts +13 -0
- package/dist/storage/presign.js +55 -0
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +171 -0
- package/dist/stores/browser.svelte.d.ts +2 -0
- package/dist/stores/browser.svelte.js +17 -1
- package/dist/stores/files.svelte.d.ts +1 -2
- package/dist/stores/files.svelte.js +1 -2
- package/dist/stores/tabs.svelte.d.ts +9 -2
- package/dist/stores/tabs.svelte.js +11 -2
- package/dist/types.d.ts +11 -0
- package/dist/utils/cog.d.ts +244 -0
- package/dist/utils/cog.js +1039 -0
- package/dist/utils/deck.d.ts +0 -18
- package/dist/utils/deck.js +0 -36
- package/dist/utils/geometry-type.d.ts +52 -0
- package/dist/utils/geometry-type.js +76 -0
- package/dist/utils/markdown-sql.d.ts +1 -1
- package/dist/utils/markdown-sql.js +3 -4
- package/dist/utils/pmtiles-tile.d.ts +0 -2
- package/dist/utils/pmtiles-tile.js +0 -8
- package/dist/utils/url-state.d.ts +6 -0
- package/dist/utils/url-state.js +34 -26
- package/dist/utils/url.d.ts +26 -9
- package/dist/utils/url.js +52 -25
- package/dist/utils/wkb.js +22 -8
- package/dist/utils/zarr-tab.d.ts +22 -0
- package/dist/utils/zarr-tab.js +30 -0
- package/dist/utils/zarr.d.ts +0 -2
- package/dist/utils/zarr.js +73 -44
- package/package.json +47 -43
- package/dist/components/ui/tabs/index.d.ts +0 -5
- package/dist/components/ui/tabs/index.js +0 -7
- package/dist/components/ui/tabs/tabs-content.svelte +0 -17
- package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-list.svelte +0 -16
- package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
- package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
- package/dist/components/ui/tabs/tabs.svelte +0 -19
- package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
- package/dist/components/viewers/MapViewer.svelte +0 -234
- package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
- package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
- package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
package/dist/i18n/en.js
CHANGED
|
@@ -30,6 +30,14 @@ export const en = {
|
|
|
30
30
|
'connection.accessKey': 'Access Key',
|
|
31
31
|
'connection.secretKey': 'Secret Key',
|
|
32
32
|
'connection.credentialNotice': "Your credentials are stored only in your browser's password manager if you choose to save them. They are never sent to any external server or stored in local storage.",
|
|
33
|
+
'connection.corsTitle': 'CORS Configuration',
|
|
34
|
+
'connection.corsRequired': 'Browser access requires CORS to be enabled on your bucket. Without it, requests will be blocked.',
|
|
35
|
+
'connection.corsDefault': 'CORS is enabled by default. No configuration needed.',
|
|
36
|
+
'connection.corsDocs': 'Official CORS docs',
|
|
37
|
+
'connection.corsCliTitle': 'Enable CORS via CLI',
|
|
38
|
+
'connection.readOnlyTitle': 'Read-Only Access',
|
|
39
|
+
'connection.readOnlyDocs': 'Official permissions docs',
|
|
40
|
+
'connection.readOnlyCliTitle': 'Restrict via CLI',
|
|
33
41
|
'connection.testSuccess': 'Connection successful',
|
|
34
42
|
'connection.testFail': 'Connection failed. Check your settings and try again.',
|
|
35
43
|
'connection.testButton': 'Test Connection',
|
|
@@ -194,6 +202,12 @@ export const en = {
|
|
|
194
202
|
'database.tablesHeader': 'Tables',
|
|
195
203
|
'database.loadingTable': 'Loading table...',
|
|
196
204
|
'database.selectTable': 'Select a table to browse',
|
|
205
|
+
// DuckLake
|
|
206
|
+
'ducklake.loading': 'Attaching DuckLake catalog...',
|
|
207
|
+
'ducklake.snapshot': 'Snapshot',
|
|
208
|
+
'ducklake.snapshots': 'snapshots',
|
|
209
|
+
'ducklake.noTables': 'No tables in this schema',
|
|
210
|
+
'ducklake.extensionHint': 'The DuckLake extension may not be available for this DuckDB-WASM version.',
|
|
197
211
|
// PDF Viewer
|
|
198
212
|
'pdf.badge': 'PDF',
|
|
199
213
|
'pdf.loading': 'Loading PDF...',
|
|
@@ -317,6 +331,7 @@ export const en = {
|
|
|
317
331
|
'map.loadingFgb': 'Loading FlatGeobuf...',
|
|
318
332
|
'map.loadingCog': 'Loading COG...',
|
|
319
333
|
'map.loadingZarr': 'Loading Zarr data...',
|
|
334
|
+
'map.zarrTooLarge': 'Array too large for map view ({shape}, ~{tiles} tiles). This dataset needs a multiscale pyramid for tiled rendering.',
|
|
320
335
|
'map.features': 'features',
|
|
321
336
|
'map.limit': '(limit)',
|
|
322
337
|
'map.of': 'of',
|
|
@@ -327,6 +342,7 @@ export const en = {
|
|
|
327
342
|
'map.flatgeobufInfo': 'FlatGeobuf Info',
|
|
328
343
|
'map.cogInfo': 'COG Info',
|
|
329
344
|
'map.cogCorsError': 'Cannot load COG: the server does not allow cross-origin requests (CORS). The file must be hosted with CORS headers enabled.',
|
|
345
|
+
'map.cogInvalidTiff': 'This file is not a valid TIFF. The server advertises image/tiff but the bytes do not match the TIFF signature, the file may be corrupt, encrypted, or mislabeled.',
|
|
330
346
|
'map.cogUnsupportedFormat': 'This COG uses {{type}} format which is not supported for map rendering. Only RGB COGs can be displayed.',
|
|
331
347
|
'map.noGeoColumn': 'No geometry column detected in schema',
|
|
332
348
|
'map.noData': 'No data available for map view',
|
|
@@ -366,6 +382,15 @@ export const en = {
|
|
|
366
382
|
'mapInfo.columns': 'Columns',
|
|
367
383
|
'mapInfo.size': 'Size',
|
|
368
384
|
'mapInfo.bands': 'Bands',
|
|
385
|
+
// COG Controls
|
|
386
|
+
'cog.style': 'Style',
|
|
387
|
+
'cog.band': 'Band',
|
|
388
|
+
'cog.singleBand': 'Single',
|
|
389
|
+
'cog.colorRamp': 'Color ramp',
|
|
390
|
+
'cog.pixelValue': 'Pixel Value',
|
|
391
|
+
'cog.reading': 'Reading pixel...',
|
|
392
|
+
'cog.rescale': 'Rescale',
|
|
393
|
+
'cog.rescaleReset': 'Reset',
|
|
369
394
|
// PMTiles Viewer
|
|
370
395
|
'pmtiles.mapView': 'Map',
|
|
371
396
|
'pmtiles.archiveView': 'Archive',
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { UrlAdapter } from './storage/url-adapter.js';
|
|
|
10
10
|
export type { Connection, ConnectionConfig, FileEntry, Tab, Theme, WriteResult } from './types.js';
|
|
11
11
|
export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
|
|
12
12
|
export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
|
|
13
|
+
export type { CogInfo, GeoBounds } from './utils/cog.js';
|
|
14
|
+
export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './utils/cog.js';
|
|
13
15
|
export type { TypeCategory } from './utils/column-types.js';
|
|
14
16
|
export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
|
|
15
17
|
export { handleLoadError } from './utils/error.js';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { UrlAdapter } from './storage/url-adapter.js';
|
|
|
10
10
|
export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
|
|
11
11
|
// Cloud URL resolution
|
|
12
12
|
export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
|
|
13
|
+
export { buildDataTypeLabel, clampBounds, SF_LABELS, safeClamp } from './utils/cog.js';
|
|
13
14
|
export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
|
|
14
15
|
// Error handling
|
|
15
16
|
export { handleLoadError } from './utils/error.js';
|
package/dist/query/engine.d.ts
CHANGED
|
@@ -35,15 +35,27 @@ export interface SchemaField {
|
|
|
35
35
|
type: string;
|
|
36
36
|
nullable: boolean;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Abstraction over a DuckDB query source. Decouples schema / CRS / count
|
|
40
|
+
* helpers from assuming a file-backed path. `ref` is the FROM-clause target
|
|
41
|
+
* inserted into generated SQL (e.g. `read_parquet('url')` for files, or
|
|
42
|
+
* `attached_db."schema"."table"` for attached databases). `filePath` is
|
|
43
|
+
* optional and only used as a shortcut for Parquet file-level metadata
|
|
44
|
+
* queries (`parquet_kv_metadata`, `parquet_file_metadata`), not for SQL.
|
|
45
|
+
*/
|
|
46
|
+
export interface QuerySource {
|
|
47
|
+
ref: string;
|
|
48
|
+
filePath?: string;
|
|
49
|
+
}
|
|
38
50
|
export interface QueryEngine {
|
|
39
51
|
query(connId: string, sql: string): Promise<QueryResult>;
|
|
40
52
|
queryForMap(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): Promise<MapQueryResult>;
|
|
41
|
-
getSchema(connId: string,
|
|
42
|
-
getRowCount(connId: string,
|
|
53
|
+
getSchema(connId: string, source: QuerySource): Promise<SchemaField[]>;
|
|
54
|
+
getRowCount(connId: string, source: QuerySource): Promise<number>;
|
|
43
55
|
/** Detect CRS from GeoParquet metadata. Returns e.g. 'EPSG:27700' or null if WGS84/unknown. */
|
|
44
|
-
detectCrs(connId: string,
|
|
56
|
+
detectCrs(connId: string, source: QuerySource, geomCol: string): Promise<string | null>;
|
|
45
57
|
/** Combined schema + CRS detection in a single connection (fewer web worker round-trips). */
|
|
46
|
-
getSchemaAndCrs?(connId: string,
|
|
58
|
+
getSchemaAndCrs?(connId: string, source: QuerySource, findGeoCol: (schema: SchemaField[]) => string | null): Promise<{
|
|
47
59
|
schema: SchemaField[];
|
|
48
60
|
geomCol: string | null;
|
|
49
61
|
crs: string | null;
|
|
@@ -51,6 +63,10 @@ export interface QueryEngine {
|
|
|
51
63
|
queryCancellable?(connId: string, sql: string): QueryHandle;
|
|
52
64
|
queryForMapCancellable?(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): MapQueryHandle;
|
|
53
65
|
forceCancel?(): Promise<void>;
|
|
66
|
+
/** Register a file buffer in DuckDB-WASM's virtual filesystem for ATTACH. */
|
|
67
|
+
registerFileBuffer?(name: string, buffer: Uint8Array): Promise<void>;
|
|
68
|
+
/** Drop a previously registered file from DuckDB-WASM's virtual filesystem. */
|
|
69
|
+
dropFile?(name: string): Promise<void>;
|
|
54
70
|
releaseMemory(): Promise<void>;
|
|
55
71
|
dispose(): Promise<void>;
|
|
56
72
|
}
|
package/dist/query/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QueryEngine } from './engine';
|
|
2
2
|
export declare function getQueryEngine(): Promise<QueryEngine>;
|
|
3
|
-
export type { MapQueryHandle, MapQueryResult, QueryEngine, QueryHandle, QueryResult, SchemaField } from './engine';
|
|
3
|
+
export type { MapQueryHandle, MapQueryResult, QueryEngine, QueryHandle, QueryResult, QuerySource, SchemaField } from './engine';
|
|
4
4
|
export { QueryCancelledError } from './engine';
|
|
5
|
+
export { type ResolvedTableSource, resolveTableSource, resolveTableSourceAsync } from './source.js';
|
package/dist/query/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a Tab to a QuerySource — the abstraction every engine helper
|
|
3
|
+
* consumes. A tab is either file-backed (path → `read_parquet('url')`) or
|
|
4
|
+
* SQL-backed (`tab.sourceRef` is a pre-built FROM-clause target, such as an
|
|
5
|
+
* attached DuckLake table).
|
|
6
|
+
*
|
|
7
|
+
* This module is the only place that knows how to map a Tab to both a SQL
|
|
8
|
+
* FROM target AND a resolved file URL, so TableViewer and DatabaseViewer
|
|
9
|
+
* stay free of ad-hoc branching.
|
|
10
|
+
*/
|
|
11
|
+
import type { Tab } from '../types.js';
|
|
12
|
+
import type { QuerySource } from './engine.js';
|
|
13
|
+
/**
|
|
14
|
+
* True when a source ref points at a self-authenticating HTTPS URL (e.g. a
|
|
15
|
+
* presigned `read_parquet('https://...?X-Amz-Signature=...')`). Used to decide
|
|
16
|
+
* whether DuckDB needs S3 credential config — presigned URLs don't.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isHttpsSourceRef(ref: string): boolean;
|
|
19
|
+
export interface ResolvedTableSource extends QuerySource {
|
|
20
|
+
/**
|
|
21
|
+
* True when the tab is file-backed and hyparquet / parquet metadata
|
|
22
|
+
* shortcuts apply. False for SQL-backed sources like attached DuckLake
|
|
23
|
+
* tables.
|
|
24
|
+
*/
|
|
25
|
+
isFileSource: boolean;
|
|
26
|
+
/** File URL used for hyparquet metadata fetches. Null for SQL-backed sources. */
|
|
27
|
+
fileUrl: string | null;
|
|
28
|
+
/** Display label, typically the tab name. */
|
|
29
|
+
label: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a tab to its QuerySource. Must be called lazily (inside reactive
|
|
33
|
+
* expressions or functions) because `tab.sourceRef` and `tab.path` can change
|
|
34
|
+
* over a tab's lifetime.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveTableSource(tab: Tab): ResolvedTableSource;
|
|
37
|
+
/**
|
|
38
|
+
* Async counterpart of `resolveTableSource`. Returns a presigned HTTPS URL
|
|
39
|
+
* for `signed-s3` connections so DuckDB httpfs can fetch without the
|
|
40
|
+
* `Authorization` header preflight.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveTableSourceAsync(tab: Tab): Promise<ResolvedTableSource>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a Tab to a QuerySource — the abstraction every engine helper
|
|
3
|
+
* consumes. A tab is either file-backed (path → `read_parquet('url')`) or
|
|
4
|
+
* SQL-backed (`tab.sourceRef` is a pre-built FROM-clause target, such as an
|
|
5
|
+
* attached DuckLake table).
|
|
6
|
+
*
|
|
7
|
+
* This module is the only place that knows how to map a Tab to both a SQL
|
|
8
|
+
* FROM target AND a resolved file URL, so TableViewer and DatabaseViewer
|
|
9
|
+
* stay free of ad-hoc branching.
|
|
10
|
+
*/
|
|
11
|
+
import { buildDuckDbSource } from '../file-icons/index.js';
|
|
12
|
+
import { buildDuckDbUrl, buildDuckDbUrlAsync } from '../utils/url.js';
|
|
13
|
+
/**
|
|
14
|
+
* True when a source ref points at a self-authenticating HTTPS URL (e.g. a
|
|
15
|
+
* presigned `read_parquet('https://...?X-Amz-Signature=...')`). Used to decide
|
|
16
|
+
* whether DuckDB needs S3 credential config — presigned URLs don't.
|
|
17
|
+
*/
|
|
18
|
+
export function isHttpsSourceRef(ref) {
|
|
19
|
+
return /(?:^|\(\s*['"])https:\/\//.test(ref);
|
|
20
|
+
}
|
|
21
|
+
function toResolved(tab, fileUrl) {
|
|
22
|
+
if (tab.sourceRef) {
|
|
23
|
+
return {
|
|
24
|
+
ref: tab.sourceRef,
|
|
25
|
+
filePath: undefined,
|
|
26
|
+
isFileSource: false,
|
|
27
|
+
fileUrl: null,
|
|
28
|
+
label: tab.name
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ref: buildDuckDbSource(tab.path, fileUrl ?? ''),
|
|
33
|
+
filePath: tab.path,
|
|
34
|
+
isFileSource: true,
|
|
35
|
+
fileUrl,
|
|
36
|
+
label: tab.name
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a tab to its QuerySource. Must be called lazily (inside reactive
|
|
41
|
+
* expressions or functions) because `tab.sourceRef` and `tab.path` can change
|
|
42
|
+
* over a tab's lifetime.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveTableSource(tab) {
|
|
45
|
+
return toResolved(tab, tab.sourceRef ? null : buildDuckDbUrl(tab));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Async counterpart of `resolveTableSource`. Returns a presigned HTTPS URL
|
|
49
|
+
* for `signed-s3` connections so DuckDB httpfs can fetch without the
|
|
50
|
+
* `Authorization` header preflight.
|
|
51
|
+
*/
|
|
52
|
+
export async function resolveTableSourceAsync(tab) {
|
|
53
|
+
return toResolved(tab, tab.sourceRef ? null : await buildDuckDbUrlAsync(tab));
|
|
54
|
+
}
|
package/dist/query/wasm.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { type MapQueryHandle, type MapQueryResult, type QueryEngine, type QueryHandle, type QueryResult, type SchemaField } from './engine';
|
|
1
|
+
import { type MapQueryHandle, type MapQueryResult, type QueryEngine, type QueryHandle, type QueryResult, type QuerySource, type SchemaField } from './engine';
|
|
2
2
|
export declare class WasmQueryEngine implements QueryEngine {
|
|
3
3
|
query(connId: string, sql: string): Promise<QueryResult>;
|
|
4
4
|
queryForMap(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): Promise<MapQueryResult>;
|
|
5
|
-
getSchema(connId: string,
|
|
6
|
-
getRowCount(connId: string,
|
|
7
|
-
getSchemaAndCrs(connId: string,
|
|
5
|
+
getSchema(connId: string, source: QuerySource): Promise<SchemaField[]>;
|
|
6
|
+
getRowCount(connId: string, source: QuerySource): Promise<number>;
|
|
7
|
+
getSchemaAndCrs(connId: string, source: QuerySource, findGeoCol: (schema: SchemaField[]) => string | null): Promise<{
|
|
8
8
|
schema: SchemaField[];
|
|
9
9
|
geomCol: string | null;
|
|
10
10
|
crs: string | null;
|
|
11
11
|
}>;
|
|
12
12
|
private configureStorage;
|
|
13
|
-
detectCrs(connId: string,
|
|
13
|
+
detectCrs(connId: string, source: QuerySource, geomCol: string): Promise<string | null>;
|
|
14
14
|
private detectCrsWithConn;
|
|
15
15
|
queryCancellable(connId: string, sql: string): QueryHandle;
|
|
16
16
|
queryForMapCancellable(connId: string, sql: string, geomCol: string, geomColType: string, sourceCrs?: string | null): MapQueryHandle;
|
|
17
17
|
forceCancel(): Promise<void>;
|
|
18
|
+
registerFileBuffer(name: string, buffer: Uint8Array): Promise<void>;
|
|
19
|
+
dropFile(name: string): Promise<void>;
|
|
18
20
|
releaseMemory(): Promise<void>;
|
|
19
21
|
dispose(): Promise<void>;
|
|
20
22
|
}
|