ol-mbtiles
Version:
MBTiles format reader for Openlayers
131 lines (110 loc) • 4.12 kB
text/typescript
import Protobuf from 'pbf';
import { VectorTile, VectorTileFeature } from '@mapbox/vector-tile';
import pako from 'pako';
import FeatureFormat, { ReadOptions } from 'ol/format/Feature.js';
import Projection from 'ol/proj/Projection.js';
import RenderFeature from 'ol/render/Feature.js';
import { FeatureLike } from 'ol/Feature.js';
import { get as getProjection } from 'ol/proj.js';
declare module '@mapbox/vector-tile' {
interface VectorTileFeature {
toGeoJSON(x: number, y: number, z: number, project?: (xy: [number, number]) => [number, number]): GeoJSON.Feature;
}
}
export interface Options {
layers?: string[];
featureClass?: typeof RenderFeature;
geometryName?: string;
idProperty?: string;
extent?: number;
}
export class MBTilesFormat<F extends FeatureLike = RenderFeature> extends FeatureFormat<F> {
dataProjection: Projection;
private featureClass_: typeof RenderFeature;
private geometryName_: string;
private layers_: string[] | null;
private idProperty_: string | undefined;
supportedMediaTypes: string[];
extent: number;
static MBTypes = {
mono: ['Unknown', 'Point', 'LineString', 'Polygon'],
multi: ['Unknown', 'MultiPoint', 'MultiLineString', 'Polygon']
} as const;
constructor(options?: Options) {
super();
options = options ? options : {};
this.dataProjection = new Projection({
code: '',
units: 'tile-pixels',
});
this.featureClass_ = options.featureClass ? options.featureClass : RenderFeature;
this.geometryName_ = options.geometryName ?? 'Geometry';
this.layers_ = options.layers ?? null;
this.idProperty_ = options.idProperty;
this.extent = options.extent ?? 4096;
/*
* As this is the very first time MBTiles will be distributed by HTTP
* there is still no official MIME type
*/
this.supportedMediaTypes = [
'application/vnd-mbtiles'
];
}
readFeature(source: VectorTileFeature, options?: ReadOptions): F {
const properties = source.properties;
let id: string | number;
if (!this.idProperty_) {
id = source.id;
} else {
id = properties[this.idProperty_] as string | number;
delete properties[this.idProperty_];
}
const points = source.loadGeometry();
const flatCoordinates = [] as number[];
const ends = [] as number[];
const type = MBTilesFormat.MBTypes[points.length > 1 ? 'multi' : 'mono'][source.type];
if (type === 'Unknown')
return null as unknown as F;
for (let i = 0; i < points.length; i++) {
if (points[i].length == 0)
continue;
for (let j = 0; j < points[i].length; j++) {
flatCoordinates.push(points[i][j].x, points[i][j].y);
}
ends.push(flatCoordinates.length);
}
const feature = new this.featureClass_(type, flatCoordinates, ends, 2, properties, id) as F;
if (options?.dataProjection && 'transform' in feature)
feature.transform(options?.dataProjection);
return feature;
}
readFeatures(source: ArrayBuffer, options?: ReadOptions): F[] {
const layers = this.layers_;
const features: F[] = [];
const tile = new VectorTile(new Protobuf(pako.ungzip(source)));
options = this.adaptOptions(options);
const dataProjection = getProjection(options?.dataProjection);
const extent = options?.extent;
if (!dataProjection || !options || !extent)
throw new Error('Cannot determine the projection/extent');
dataProjection.setWorldExtent(extent);
dataProjection.setExtent([0, 0, this.extent, this.extent]);
options.dataProjection = dataProjection;
for (const layerName of Object.keys(tile.layers)) {
if (layers && !layers.includes(layerName)) {
continue;
}
const l = tile.layers[layerName];
for (let idx = 0; idx < l.length; idx++) {
const vectorFeature = l.feature(idx);
const feature = this.readFeature(vectorFeature, options);
feature.getProperties().layer = layerName;
features.push(feature);
}
}
return features;
}
readProjection() {
return this.dataProjection;
}
}