mapcomplete/Logic/BBox.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

238 lines
6.8 KiB
TypeScript
Raw Normal View History

import * as turf from "@turf/turf"
import { TileRange, Tiles } from "../Models/TileRange"
import { GeoOperations } from "./GeoOperations"
export class BBox {
2021-11-07 16:34:51 +01:00
static global: BBox = new BBox([
[-180, -90],
[180, 90],
])
readonly maxLat: number
readonly maxLon: number
readonly minLat: number
readonly minLon: number
2021-12-07 17:46:57 +01:00
/***
* Coordinates should be [[lon, lat],[lon, lat]]
* @param coordinates
*/
constructor(coordinates) {
this.maxLat = -90
this.maxLon = -180
this.minLat = 90
this.minLon = 180
for (const coordinate of coordinates) {
this.maxLon = Math.max(this.maxLon, coordinate[0])
this.maxLat = Math.max(this.maxLat, coordinate[1])
this.minLon = Math.min(this.minLon, coordinate[0])
this.minLat = Math.min(this.minLat, coordinate[1])
}
this.maxLon = Math.min(this.maxLon, 180)
this.maxLat = Math.min(this.maxLat, 90)
this.minLon = Math.max(this.minLon, -180)
this.minLat = Math.max(this.minLat, -90)
this.check()
}
static fromLeafletBounds(bounds) {
return new BBox([
[bounds.getWest(), bounds.getNorth()],
[bounds.getEast(), bounds.getSouth()],
])
}
static get(feature): BBox {
if (feature.bbox?.overlapsWith === undefined) {
const turfBbox: number[] = turf.bbox(feature)
feature.bbox = new BBox([
[turfBbox[0], turfBbox[1]],
[turfBbox[2], turfBbox[3]],
])
}
return feature.bbox
}
2022-01-26 20:47:08 +01:00
static bboxAroundAll(bboxes: BBox[]): BBox {
2021-12-07 17:46:57 +01:00
let maxLat: number = -90
2022-01-26 20:47:08 +01:00
let maxLon: number = -180
let minLat: number = 80
let minLon: number = 180
2021-12-07 17:46:57 +01:00
for (const bbox of bboxes) {
maxLat = Math.max(maxLat, bbox.maxLat)
maxLon = Math.max(maxLon, bbox.maxLon)
minLat = Math.min(minLat, bbox.minLat)
minLon = Math.min(minLon, bbox.minLon)
}
2022-01-26 20:47:08 +01:00
return new BBox([
[maxLon, maxLat],
[minLon, minLat],
])
2021-12-07 17:46:57 +01:00
}
2022-03-23 19:48:06 +01:00
/**
* Calculates the BBox based on a slippy map tile number
*
* const bbox = BBox.fromTile(16, 32754, 21785)
* bbox.minLon // => -0.076904296875
* bbox.maxLon // => -0.0714111328125
* bbox.minLat // => 51.5292513551899
* bbox.maxLat // => 51.53266860674158
*/
2021-11-07 16:34:51 +01:00
static fromTile(z: number, x: number, y: number): BBox {
return new BBox(Tiles.tile_bounds_lon_lat(z, x, y))
}
static fromTileIndex(i: number): BBox {
if (i === 0) {
return BBox.global
}
return BBox.fromTile(...Tiles.tile_from_index(i))
}
2022-01-26 20:47:08 +01:00
public unionWith(other: BBox) {
return new BBox([
[Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)],
[Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)],
])
}
/**
* Constructs a tilerange which fully contains this bbox (thus might be a bit larger)
* @param zoomlevel
*/
public containingTileRange(zoomlevel): TileRange {
return Tiles.TileRangeBetween(zoomlevel, this.minLat, this.minLon, this.maxLat, this.maxLon)
}
public overlapsWith(other: BBox) {
if (this.maxLon < other.minLon) {
return false
}
if (this.maxLat < other.minLat) {
return false
}
if (this.minLon > other.maxLon) {
return false
}
return this.minLat <= other.maxLat
}
public isContainedIn(other: BBox) {
if (this.maxLon > other.maxLon) {
return false
}
if (this.maxLat > other.maxLat) {
return false
}
if (this.minLon < other.minLon) {
return false
}
if (this.minLat < other.minLat) {
return false
}
return true
}
getEast() {
return this.maxLon
}
getNorth() {
return this.maxLat
}
getWest() {
return this.minLon
}
getSouth() {
return this.minLat
}
2021-11-07 16:34:51 +01:00
contains(lonLat: [number, number]) {
return (
this.minLat <= lonLat[1] &&
lonLat[1] <= this.maxLat &&
2021-11-07 16:34:51 +01:00
this.minLon <= lonLat[0] &&
lonLat[0] <= this.maxLon
2022-09-08 21:40:48 +02:00
)
}
pad(factor: number, maxIncrease = 2): BBox {
const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor)
const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
return new BBox([
[this.minLon - lonDiff, this.minLat - latDiff],
[this.maxLon + lonDiff, this.maxLat + latDiff],
])
}
padAbsolute(degrees: number): BBox {
return new BBox([
[this.minLon - degrees, this.minLat - degrees],
[this.maxLon + degrees, this.maxLat + degrees],
])
}
toLeaflet(): [[number, number], [number, number]] {
return [
[this.minLat, this.minLon],
[this.maxLat, this.maxLon],
]
}
asGeoJson(properties: any): any {
return {
type: "Feature",
properties: properties,
geometry: {
type: "Polygon",
coordinates: [
2022-09-08 21:40:48 +02:00
[
[this.minLon, this.minLat],
[this.maxLon, this.minLat],
[this.maxLon, this.maxLat],
[this.minLon, this.maxLat],
[this.minLon, this.minLat],
2022-09-08 21:40:48 +02:00
],
],
},
}
}
/**
* Expands the BBOx so that it contains complete tiles for the given zoomlevel
* @param zoomlevel
*/
expandToTileBounds(zoomlevel: number): BBox {
const ul = Tiles.embedded_tile(this.minLat, this.minLon, zoomlevel)
const lr = Tiles.embedded_tile(this.maxLat, this.maxLon, zoomlevel)
const boundsul = Tiles.tile_bounds_lon_lat(ul.z, ul.x, ul.y)
const boundslr = Tiles.tile_bounds_lon_lat(lr.z, lr.x, lr.y)
return new BBox([].concat(boundsul, boundslr))
}
toMercator(): { minLat: number; maxLat: number; minLon: number; maxLon: number } {
const [minLon, minLat] = GeoOperations.ConvertWgs84To900913([this.minLon, this.minLat])
const [maxLon, maxLat] = GeoOperations.ConvertWgs84To900913([this.maxLon, this.maxLat])
return {
minLon,
maxLon,
minLat,
maxLat,
}
}
2021-11-07 16:34:51 +01:00
2022-02-24 03:09:30 +01:00
private check() {
2021-11-07 16:34:51 +01:00
if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) {
2022-02-24 03:09:30 +01:00
console.trace("BBox with NaN detected:", this)
2021-11-07 16:34:51 +01:00
throw "BBOX has NAN"
}
}
}