import * as turf from "@turf/turf"; import {TileRange, Tiles} from "../Models/TileRange"; import {GeoOperations} from "./GeoOperations"; export class BBox { static global: BBox = new BBox([[-180, -90], [180, 90]]); readonly maxLat: number; readonly maxLon: number; readonly minLat: number; readonly minLon: number; /*** * 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; } static bboxAroundAll(bboxes: BBox[]): BBox { let maxLat: number = -90; let maxLon: number = -180; let minLat: number = 80; let minLon: number = 180; 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) } return new BBox([[maxLon, maxLat], [minLon, minLat]]) } /** * 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 */ 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)) } 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 } contains(lonLat: [number, number]) { return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat && this.minLon <= lonLat[0] && lonLat[0] <= this.maxLon } 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() { return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]] } asGeoJson(properties: any): any { return { type: "Feature", properties: properties, geometry: { type: "Polygon", coordinates: [[ [this.minLon, this.minLat], [this.maxLon, this.minLat], [this.maxLon, this.maxLat], [this.minLon, this.maxLat], [this.minLon, this.minLat], ]] } } } /** * 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 } } private check() { if (isNaN(this.maxLon) || isNaN(this.maxLat) || isNaN(this.minLon) || isNaN(this.minLat)) { console.trace("BBox with NaN detected:", this); throw "BBOX has NAN"; } } }