@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
149 lines (118 loc) • 4.68 kB
text/typescript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import type { Feature } from 'ol';
import type FeatureFormat from 'ol/format/Feature';
import type { Type } from 'ol/format/Feature';
import type { Geometry } from 'ol/geom';
import GeoJSON from 'ol/format/GeoJSON';
import CoordinateSystem from '../core/geographic/CoordinateSystem';
import Fetcher from '../utils/Fetcher';
import { nonNull } from '../utils/tsutils';
import { filterByExtent, processFeatures } from './features/processor';
import { FeatureSourceBase, type GetFeatureRequest, type GetFeatureResult } from './FeatureSource';
export type Getter = (url: string, type: Type) => Promise<unknown>;
const defaultGetter: Getter = (url, type) => {
switch (type) {
case 'arraybuffer':
return Fetcher.arrayBuffer(url);
case 'json':
return Fetcher.json(url);
case 'text':
return Fetcher.text(url);
case 'xml':
return Fetcher.xml(url);
}
};
export interface FileFeatureSourceOptions {
/**
* The URL to the remote file.
*/
url: string;
/**
* The format to parse the file.
* @defaultValue {@link GeoJSON}
*/
format?: FeatureFormat;
/**
* A function to retrieve the file remotely.
* If not specified, will use standard fetch functions to download the file.
* Mostly useful for unit testing.
*/
getter?: Getter;
/**
* The coordinate system of the features in this source.
* 1. If not provided, will attempt to read it from the file.
* 2. If the file does not contain coordinate system information, will assume EPSG:4326.
*/
sourceCoordinateSystem?: CoordinateSystem;
}
/**
* Loads features from a remote file (such as GeoJSON, GPX, etc.)
*/
export default class FileFeatureSource extends FeatureSourceBase {
public readonly isFileFeatureSource = true as const;
public readonly type = 'FileFeatureSource' as const;
private readonly _format: FeatureFormat;
private _features: Feature<Geometry>[] | null = null;
private _loadFeaturePromise: Promise<Feature<Geometry>[]> | null = null;
private _getter: Getter;
private _url: string;
private _sourceCoordinateSystem?: CoordinateSystem;
public constructor(params: FileFeatureSourceOptions) {
super();
this._format = params.format ?? new GeoJSON();
this._url = params.url;
this._sourceCoordinateSystem = params.sourceCoordinateSystem;
this._getter = params.getter ?? defaultGetter;
}
private loadFeatures(): Promise<Feature[]> {
this.throwIfNotInitialized();
if (this._features != null) {
return Promise.resolve(this._features);
}
if (this._loadFeaturePromise != null) {
return this._loadFeaturePromise;
}
this._loadFeaturePromise = this.loadFeaturesOnce();
return this._loadFeaturePromise;
}
private async loadFeaturesOnce(): Promise<Feature<Geometry>[]> {
const data = await this._getter(this._url, this._format.getType());
if (!this._sourceCoordinateSystem) {
const dataProjection = this._format.readProjection(data);
if (dataProjection) {
this._sourceCoordinateSystem = CoordinateSystem.get(dataProjection?.getCode());
} else {
this._sourceCoordinateSystem = CoordinateSystem.epsg4326;
}
}
const features = this._format.readFeatures(data) as Feature[];
const targetProjection = nonNull(
this._targetCoordinateSystem,
'this source is not initialized',
);
const sourceProjection = nonNull(this._sourceCoordinateSystem);
const actualFeatures = await processFeatures(features, sourceProjection, targetProjection);
this._features = actualFeatures;
return actualFeatures;
}
public async getFeatures(request: GetFeatureRequest): Promise<GetFeatureResult> {
request.signal?.throwIfAborted();
const features = await this.loadFeatures();
request.signal?.throwIfAborted();
const filtered = await filterByExtent(features, request.extent, { signal: request.signal });
request.signal?.throwIfAborted();
return { features: filtered };
}
/**
* Deletes the already loaded features, and dispatch an event to reload the features.
*/
public reload(): void {
this._features = null;
this._loadFeaturePromise = null;
this.dispatchEvent({ type: 'updated' });
}
}