mirror of
https://github.com/mapbox/vector-tile-js.git
synced 2026-01-25 14:08:34 +00:00
301 lines
7.6 KiB
JavaScript
301 lines
7.6 KiB
JavaScript
|
|
import Point from '@mapbox/point-geometry';
|
|
|
|
export class VectorTileFeature {
|
|
constructor(pbf, end, extent, keys, values) {
|
|
// Public
|
|
this.properties = {};
|
|
this.extent = extent;
|
|
this.type = 0;
|
|
|
|
// Private
|
|
this._pbf = pbf;
|
|
this._geometry = -1;
|
|
this._keys = keys;
|
|
this._values = values;
|
|
|
|
pbf.readFields(readFeature, this, end);
|
|
}
|
|
|
|
loadGeometry() {
|
|
const pbf = this._pbf;
|
|
pbf.pos = this._geometry;
|
|
|
|
const end = pbf.readVarint() + pbf.pos;
|
|
const lines = [];
|
|
let cmd = 1,
|
|
length = 0,
|
|
x = 0,
|
|
y = 0,
|
|
line;
|
|
|
|
while (pbf.pos < end) {
|
|
if (length <= 0) {
|
|
const cmdLen = pbf.readVarint();
|
|
cmd = cmdLen & 0x7;
|
|
length = cmdLen >> 3;
|
|
}
|
|
|
|
length--;
|
|
|
|
if (cmd === 1 || cmd === 2) {
|
|
x += pbf.readSVarint();
|
|
y += pbf.readSVarint();
|
|
|
|
if (cmd === 1) { // moveTo
|
|
if (line) lines.push(line);
|
|
line = [];
|
|
}
|
|
|
|
line.push(new Point(x, y));
|
|
|
|
} else if (cmd === 7) {
|
|
|
|
// Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90
|
|
if (line) {
|
|
line.push(line[0].clone()); // closePolygon
|
|
}
|
|
|
|
} else {
|
|
throw new Error(`unknown command ${ cmd}`);
|
|
}
|
|
}
|
|
|
|
if (line) lines.push(line);
|
|
|
|
return lines;
|
|
}
|
|
|
|
bbox() {
|
|
const pbf = this._pbf;
|
|
pbf.pos = this._geometry;
|
|
|
|
const end = pbf.readVarint() + pbf.pos;
|
|
let cmd = 1,
|
|
length = 0,
|
|
x = 0,
|
|
y = 0,
|
|
x1 = Infinity,
|
|
x2 = -Infinity,
|
|
y1 = Infinity,
|
|
y2 = -Infinity;
|
|
|
|
while (pbf.pos < end) {
|
|
if (length <= 0) {
|
|
const cmdLen = pbf.readVarint();
|
|
cmd = cmdLen & 0x7;
|
|
length = cmdLen >> 3;
|
|
}
|
|
|
|
length--;
|
|
|
|
if (cmd === 1 || cmd === 2) {
|
|
x += pbf.readSVarint();
|
|
y += pbf.readSVarint();
|
|
if (x < x1) x1 = x;
|
|
if (x > x2) x2 = x;
|
|
if (y < y1) y1 = y;
|
|
if (y > y2) y2 = y;
|
|
|
|
} else if (cmd !== 7) {
|
|
throw new Error(`unknown command ${ cmd}`);
|
|
}
|
|
}
|
|
|
|
return [x1, y1, x2, y2];
|
|
}
|
|
|
|
toGeoJSON(x, y, z) {
|
|
const size = this.extent * Math.pow(2, z),
|
|
x0 = this.extent * x,
|
|
y0 = this.extent * y;
|
|
let type = VectorTileFeature.types[this.type],
|
|
coords = this.loadGeometry();
|
|
|
|
function project(line) {
|
|
for (let j = 0; j < line.length; j++) {
|
|
const p = line[j], y2 = 180 - (p.y + y0) * 360 / size;
|
|
line[j] = [
|
|
(p.x + x0) * 360 / size - 180,
|
|
360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90
|
|
];
|
|
}
|
|
}
|
|
|
|
switch (this.type) {
|
|
case 1:
|
|
const points = [];
|
|
for (let i = 0; i < coords.length; i++) {
|
|
points[i] = coords[i][0];
|
|
}
|
|
coords = points;
|
|
project(coords);
|
|
break;
|
|
|
|
case 2:
|
|
for (let i = 0; i < coords.length; i++) {
|
|
project(coords[i]);
|
|
}
|
|
break;
|
|
|
|
case 3:
|
|
coords = classifyRings(coords);
|
|
for (let i = 0; i < coords.length; i++) {
|
|
for (let j = 0; j < coords[i].length; j++) {
|
|
project(coords[i][j]);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (coords.length === 1) {
|
|
coords = coords[0];
|
|
} else {
|
|
type = `Multi${type}`;
|
|
}
|
|
|
|
const result = {
|
|
type: 'Feature',
|
|
geometry: {
|
|
type,
|
|
coordinates: coords
|
|
},
|
|
properties: this.properties
|
|
};
|
|
|
|
if ('id' in this) {
|
|
result.id = this.id;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];
|
|
|
|
function readFeature(tag, feature, pbf) {
|
|
if (tag === 1) feature.id = pbf.readVarint();
|
|
else if (tag === 2) readTag(pbf, feature);
|
|
else if (tag === 3) feature.type = pbf.readVarint();
|
|
else if (tag === 4) feature._geometry = pbf.pos;
|
|
}
|
|
|
|
function readTag(pbf, feature) {
|
|
const end = pbf.readVarint() + pbf.pos;
|
|
|
|
while (pbf.pos < end) {
|
|
const key = feature._keys[pbf.readVarint()],
|
|
value = feature._values[pbf.readVarint()];
|
|
feature.properties[key] = value;
|
|
}
|
|
}
|
|
|
|
// classifies an array of rings into polygons with outer rings and holes
|
|
export function classifyRings(rings) {
|
|
const len = rings.length;
|
|
|
|
if (len <= 1) return [rings];
|
|
|
|
const polygons = [];
|
|
let polygon, ccw;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
const area = signedArea(rings[i]);
|
|
if (area === 0) continue;
|
|
|
|
if (ccw === undefined) ccw = area < 0;
|
|
|
|
if (ccw === area < 0) {
|
|
if (polygon) polygons.push(polygon);
|
|
polygon = [rings[i]];
|
|
|
|
} else {
|
|
polygon.push(rings[i]);
|
|
}
|
|
}
|
|
if (polygon) polygons.push(polygon);
|
|
|
|
return polygons;
|
|
}
|
|
|
|
function signedArea(ring) {
|
|
let sum = 0;
|
|
for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
|
|
p1 = ring[i];
|
|
p2 = ring[j];
|
|
sum += (p2.x - p1.x) * (p1.y + p2.y);
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
export class VectorTileLayer {
|
|
constructor(pbf, end) {
|
|
// Public
|
|
this.version = 1;
|
|
this.name = null;
|
|
this.extent = 4096;
|
|
this.length = 0;
|
|
|
|
// Private
|
|
this._pbf = pbf;
|
|
this._keys = [];
|
|
this._values = [];
|
|
this._features = [];
|
|
|
|
pbf.readFields(readLayer, this, end);
|
|
|
|
this.length = this._features.length;
|
|
}
|
|
|
|
// return feature `i` from this layer as a `VectorTileFeature`
|
|
feature(i) {
|
|
if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');
|
|
|
|
this._pbf.pos = this._features[i];
|
|
|
|
const end = this._pbf.readVarint() + this._pbf.pos;
|
|
return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);
|
|
}
|
|
}
|
|
|
|
function readLayer(tag, layer, pbf) {
|
|
if (tag === 15) layer.version = pbf.readVarint();
|
|
else if (tag === 1) layer.name = pbf.readString();
|
|
else if (tag === 5) layer.extent = pbf.readVarint();
|
|
else if (tag === 2) layer._features.push(pbf.pos);
|
|
else if (tag === 3) layer._keys.push(pbf.readString());
|
|
else if (tag === 4) layer._values.push(readValueMessage(pbf));
|
|
}
|
|
|
|
function readValueMessage(pbf) {
|
|
let value = null;
|
|
const end = pbf.readVarint() + pbf.pos;
|
|
|
|
while (pbf.pos < end) {
|
|
const tag = pbf.readVarint() >> 3;
|
|
|
|
value = tag === 1 ? pbf.readString() :
|
|
tag === 2 ? pbf.readFloat() :
|
|
tag === 3 ? pbf.readDouble() :
|
|
tag === 4 ? pbf.readVarint64() :
|
|
tag === 5 ? pbf.readVarint() :
|
|
tag === 6 ? pbf.readSVarint() :
|
|
tag === 7 ? pbf.readBoolean() : null;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
export class VectorTile {
|
|
constructor(pbf, end) {
|
|
this.layers = pbf.readFields(readTile, {}, end);
|
|
}
|
|
}
|
|
|
|
function readTile(tag, layers, pbf) {
|
|
if (tag === 3) {
|
|
const layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);
|
|
if (layer.length) layers[layer.name] = layer;
|
|
}
|
|
}
|