mapillary-js
Version:
A WebGL interactive street imagery library
358 lines (296 loc) • 11.7 kB
text/typescript
import { VertexGeometry } from "./VertexGeometry";
import { GeometryTagError } from "../error/GeometryTagError";
import { Transform } from "../../../geo/Transform";
import { isSpherical } from "../../../geo/Geo";
/**
* @class PolygonGeometry
*
* @classdesc Represents a polygon geometry in the 2D basic image coordinate system.
* All polygons and holes provided to the constructor needs to be closed.
*
* @example
* ```js
* var basicPolygon = [[0.5, 0.3], [0.7, 0.3], [0.6, 0.5], [0.5, 0.3]];
* var polygonGeometry = new PolygonGeometry(basicPolygon);
* ```
*/
export class PolygonGeometry extends VertexGeometry {
private _polygon: number[][];
private _holes: number[][][];
/**
* Create a polygon geometry.
*
* @constructor
* @param {Array<Array<number>>} polygon - Array of polygon vertices. Must be closed.
* @param {Array<Array<Array<number>>>} [holes] - Array of arrays of hole vertices.
* Each array of holes vertices must be closed.
*
* @throws {GeometryTagError} Polygon coordinates must be valid basic coordinates.
*/
constructor(polygon: number[][], holes?: number[][][]) {
super();
let polygonLength: number = polygon.length;
if (polygonLength < 3) {
throw new GeometryTagError("A polygon must have three or more positions.");
}
if (polygon[0][0] !== polygon[polygonLength - 1][0] ||
polygon[0][1] !== polygon[polygonLength - 1][1]) {
throw new GeometryTagError("First and last positions must be equivalent.");
}
this._polygon = [];
for (let vertex of polygon) {
if (vertex[0] < 0 || vertex[0] > 1 ||
vertex[1] < 0 || vertex[1] > 1) {
throw new GeometryTagError("Basic coordinates of polygon must be on the interval [0, 1].");
}
this._polygon.push(vertex.slice());
}
this._holes = [];
if (holes == null) {
return;
}
for (let i: number = 0; i < holes.length; i++) {
let hole: number[][] = holes[i];
let holeLength: number = hole.length;
if (holeLength < 3) {
throw new GeometryTagError("A polygon hole must have three or more positions.");
}
if (hole[0][0] !== hole[holeLength - 1][0] ||
hole[0][1] !== hole[holeLength - 1][1]) {
throw new GeometryTagError("First and last positions of hole must be equivalent.");
}
this._holes.push([]);
for (let vertex of hole) {
if (vertex[0] < 0 || vertex[0] > 1 ||
vertex[1] < 0 || vertex[1] > 1) {
throw new GeometryTagError("Basic coordinates of hole must be on the interval [0, 1].");
}
this._holes[i].push(vertex.slice());
}
}
}
/**
* Get polygon property.
* @returns {Array<Array<number>>} Closed 2d polygon.
*/
public get polygon(): number[][] {
return this._polygon;
}
/**
* Get holes property.
* @returns {Array<Array<Array<number>>>} Holes of 2d polygon.
*/
public get holes(): number[][][] {
return this._holes;
}
/**
* Add a vertex to the polygon by appending it after the last vertex.
*
* @param {Array<number>} vertex - Vertex to add.
* @ignore
*/
public addVertex2d(vertex: number[]): void {
let clamped: number[] = [
Math.max(0, Math.min(1, vertex[0])),
Math.max(0, Math.min(1, vertex[1])),
];
this._polygon.splice(this._polygon.length - 1, 0, clamped);
this._notifyChanged$.next(this);
}
/**
* Get the coordinates of a vertex from the polygon representation of the geometry.
*
* @param {number} index - Vertex index.
* @returns {Array<number>} Array representing the 2D basic coordinates of the vertex.
* @ignore
*/
public getVertex2d(index: number): number[] {
return this._polygon[index].slice();
}
/**
* Remove a vertex from the polygon.
*
* @param {number} index - The index of the vertex to remove.
* @ignore
*/
public removeVertex2d(index: number): void {
if (index < 0 ||
index >= this._polygon.length ||
this._polygon.length < 4) {
throw new GeometryTagError("Index for removed vertex must be valid.");
}
if (index > 0 && index < this._polygon.length - 1) {
this._polygon.splice(index, 1);
} else {
this._polygon.splice(0, 1);
this._polygon.pop();
let closing: number[] = this._polygon[0].slice();
this._polygon.push(closing);
}
this._notifyChanged$.next(this);
}
/** @ignore */
public setVertex2d(index: number, value: number[], transform: Transform): void {
let changed: number[] = [
Math.max(0, Math.min(1, value[0])),
Math.max(0, Math.min(1, value[1])),
];
if (index === 0 || index === this._polygon.length - 1) {
this._polygon[0] = changed.slice();
this._polygon[this._polygon.length - 1] = changed.slice();
} else {
this._polygon[index] = changed.slice();
}
this._notifyChanged$.next(this);
}
/** @ignore */
public setCentroid2d(value: number[], transform: Transform): void {
let xs: number[] = this._polygon.map((point: number[]): number => { return point[0]; });
let ys: number[] = this._polygon.map((point: number[]): number => { return point[1]; });
let minX: number = Math.min.apply(Math, xs);
let maxX: number = Math.max.apply(Math, xs);
let minY: number = Math.min.apply(Math, ys);
let maxY: number = Math.max.apply(Math, ys);
let centroid: number[] = this.getCentroid2d();
let minTranslationX: number = -minX;
let maxTranslationX: number = 1 - maxX;
let minTranslationY: number = -minY;
let maxTranslationY: number = 1 - maxY;
let translationX: number = Math.max(minTranslationX, Math.min(maxTranslationX, value[0] - centroid[0]));
let translationY: number = Math.max(minTranslationY, Math.min(maxTranslationY, value[1] - centroid[1]));
for (let point of this._polygon) {
point[0] += translationX;
point[1] += translationY;
}
this._notifyChanged$.next(this);
}
/** @ignore */
public getPoints3d(transform: Transform): number[][] {
return this._getPoints3d(
this._subsample(this._polygon),
transform);
}
/** @ignore */
public getVertex3d(index: number, transform: Transform): number[] {
return transform.unprojectBasic(this._polygon[index], 200);
}
/** @ignore */
public getVertices2d(): number[][] {
return this._polygon.slice();
}
/** @ignore */
public getVertices3d(transform: Transform): number[][] {
return this._getPoints3d(this._polygon, transform);
}
/**
* Get a polygon representation of the 3D coordinates for the vertices of each hole
* of the geometry. Line segments between vertices will possibly be subsampled
* resulting in a larger number of points than the total number of vertices.
*
* @param {Transform} transform - The transform of the image related to the geometry.
* @returns {Array<Array<Array<number>>>} Array of hole polygons in 3D world coordinates
* representing the vertices of each hole of the geometry.
* @ignore
*/
public getHolePoints3d(transform: Transform): number[][][] {
return this._holes
.map(
(hole2d: number[][]): number[][] => {
return this._getPoints3d(
this._subsample(hole2d),
transform);
});
}
/**
* Get a polygon representation of the 3D coordinates for the vertices of each hole
* of the geometry.
*
* @param {Transform} transform - The transform of the image related to the geometry.
* @returns {Array<Array<Array<number>>>} Array of hole polygons in 3D world coordinates
* representing the vertices of each hole of the geometry.
* @ignore
*/
public getHoleVertices3d(transform: Transform): number[][][] {
return this._holes
.map(
(hole2d: number[][]): number[][] => {
return this._getPoints3d(hole2d, transform);
});
}
/** @ignore */
public getCentroid2d(): number[] {
let polygon: number[][] = this._polygon;
let area: number = 0;
let centroidX: number = 0;
let centroidY: number = 0;
for (let i: number = 0; i < polygon.length - 1; i++) {
let xi: number = polygon[i][0];
let yi: number = polygon[i][1];
let xi1: number = polygon[i + 1][0];
let yi1: number = polygon[i + 1][1];
let a: number = xi * yi1 - xi1 * yi;
area += a;
centroidX += (xi + xi1) * a;
centroidY += (yi + yi1) * a;
}
area /= 2;
centroidX /= 6 * area;
centroidY /= 6 * area;
return [centroidX, centroidY];
}
/** @ignore */
public getCentroid3d(transform: Transform): number[] {
let centroid2d: number[] = this.getCentroid2d();
return transform.unprojectBasic(centroid2d, 200);
}
/** @ignore */
public get3dDomainTriangles3d(transform: Transform): number[] {
return this._triangulate(
this._project(this._polygon, transform),
this.getVertices3d(transform),
this._holes
.map(
(hole2d: number[][]): number[][] => {
return this._project(hole2d, transform);
}),
this.getHoleVertices3d(transform));
}
/** @ignore */
public getTriangles3d(transform: Transform): number[] {
if (isSpherical(transform.cameraType)) {
return this._triangulateSpherical(
this._polygon.slice(),
this.holes.slice(),
transform);
}
const points2d: number[][] = this._project(this._subsample(this._polygon), transform);
const points3d: number[][] = this.getPoints3d(transform);
const holes2d: number[][][] = this._holes
.map(
(hole: number[][]): number[][] => {
return this._project(this._subsample(hole), transform);
});
const holes3d: number[][][] = this.getHolePoints3d(transform);
return this._triangulate(
points2d,
points3d,
holes2d,
holes3d);
}
/** @ignore */
public getPoleOfInaccessibility2d(): number[] {
return this._getPoleOfInaccessibility2d(this._polygon.slice());
}
/** @ignore */
public getPoleOfInaccessibility3d(transform: Transform): number[] {
let pole2d: number[] = this._getPoleOfInaccessibility2d(this._polygon.slice());
return transform.unprojectBasic(pole2d, 200);
}
private _getPoints3d(points2d: number[][], transform: Transform): number[][] {
return points2d
.map(
(point: number[]) => {
return transform.unprojectBasic(point, 200);
});
}
}