UNPKG

tangram

Version:
319 lines (269 loc) 9.09 kB
// Miscellaneous geo functions var Geo; export default Geo = {}; // Projection constants Geo.default_source_max_zoom = 18; Geo.default_view_max_zoom = 20; Geo.max_style_zoom = 25; // max zoom at which styles will be evaluated Geo.tile_size = 256; Geo.half_circumference_meters = 20037508.342789244; Geo.circumference_meters = Geo.half_circumference_meters * 2; Geo.min_zoom_meters_per_pixel = Geo.circumference_meters / Geo.tile_size; // min zoom draws world as 2 tiles wide let meters_per_pixel = []; Geo.metersPerPixel = function (z) { meters_per_pixel[z] = meters_per_pixel[z] || Geo.min_zoom_meters_per_pixel / Math.pow(2, z); return meters_per_pixel[z]; }; let meters_per_tile = []; Geo.metersPerTile = function (z) { meters_per_tile[z] = meters_per_tile[z] || Geo.circumference_meters / Math.pow(2, z); return meters_per_tile[z]; }; // Conversion functions based on an defined tile scale Geo.tile_scale = 4096; // coordinates are locally scaled to the range [0, tile_scale] Geo.units_per_pixel = Geo.tile_scale / Geo.tile_size; Geo.height_scale = 16; // provides sub-meter precision for height values (16ths of a meters) let units_per_meter = []; Geo.unitsPerMeter = function (z) { units_per_meter[z] = units_per_meter[z] || Geo.tile_scale / (Geo.tile_size * Geo.metersPerPixel(z)); return units_per_meter[z]; }; // Convert tile location to mercator meters - multiply by pixels per tile, then by meters per pixel, adjust for map origin Geo.metersForTile = function (tile) { return { x: tile.x * Geo.circumference_meters / Math.pow(2, tile.z) - Geo.half_circumference_meters, y: -(tile.y * Geo.circumference_meters / Math.pow(2, tile.z) - Geo.half_circumference_meters) }; }; /** Given a point in mercator meters and a zoom level, return the tile X/Y/Z that the point lies in */ Geo.tileForMeters = function ([x, y], zoom) { return { x: Math.floor((x + Geo.half_circumference_meters) / (Geo.circumference_meters / Math.pow(2, zoom))), y: Math.floor((-y + Geo.half_circumference_meters) / (Geo.circumference_meters / Math.pow(2, zoom))), z: zoom }; }; // Wrap a tile to positive #s for zoom // Optionally specify the axes to wrap Geo.wrapTile = function({ x, y, z }, mask = { x: true, y: false }) { var m = (1 << z) - 1; if (mask.x) { x = x & m; } if (mask.y) { y = y & m; } return { x, y, z }; }; /** Convert mercator meters to lat-lng, in-place */ Geo.metersToLatLng = function (c) { c[0] /= Geo.half_circumference_meters; c[1] /= Geo.half_circumference_meters; c[1] = (2 * Math.atan(Math.exp(c[1] * Math.PI)) - (Math.PI / 2)) / Math.PI; c[0] *= 180; c[1] *= 180; return c; }; /** Convert lat-lng to mercator meters, in-place */ Geo.latLngToMeters = function (c) { // Latitude c[1] = Math.log(Math.tan(c[1] * Math.PI / 360 + Math.PI / 4)) / Math.PI; c[1] *= Geo.half_circumference_meters; // Longitude c[0] *= Geo.half_circumference_meters / 180; return c; }; // Transform from local tile coordinats to lat lng Geo.tileSpaceToLatlng = function (geometry, z, min) { const units_per_meter = Geo.unitsPerMeter(z); Geo.transformGeometry(geometry, coord => { coord[0] = (coord[0] / units_per_meter) + min.x; coord[1] = (coord[1] / units_per_meter) + min.y; Geo.metersToLatLng(coord); }); return geometry; }; // Copy GeoJSON geometry Geo.copyGeometry = function (geometry) { if (geometry == null) { return; // skip if missing geometry (valid GeoJSON) } let copy = { type: geometry.type }; if (geometry.type === 'Point') { copy.coordinates = [geometry.coordinates[0], geometry.coordinates[1]]; } else if (geometry.type === 'LineString' || geometry.type === 'MultiPoint') { copy.coordinates = geometry.coordinates.map(c => [c[0], c[1]]); } else if (geometry.type === 'Polygon' || geometry.type === 'MultiLineString') { copy.coordinates = geometry.coordinates.map(ring => ring.map(c => [c[0], c[1]])); } else if (geometry.type === 'MultiPolygon') { copy.coordinates = geometry.coordinates.map(polygon => { return polygon.map(ring => ring.map(c => [c[0], c[1]])); }); } // TODO: support GeometryCollection return copy; }; // Run an in-place transform function on each cooordinate in a GeoJSON geometry Geo.transformGeometry = function (geometry, transform) { if (geometry == null) { return; // skip if missing geometry (valid GeoJSON) } if (geometry.type === 'Point') { transform(geometry.coordinates); } else if (geometry.type === 'LineString' || geometry.type === 'MultiPoint') { geometry.coordinates.forEach(transform); } else if (geometry.type === 'Polygon' || geometry.type === 'MultiLineString') { geometry.coordinates.forEach(ring => ring.forEach(transform)); } else if (geometry.type === 'MultiPolygon') { geometry.coordinates.forEach(polygon => { polygon.forEach(ring => ring.forEach(transform)); }); } // TODO: support GeometryCollection }; Geo.boxIntersect = function (b1, b2) { return !( b2.sw.x > b1.ne.x || b2.ne.x < b1.sw.x || b2.sw.y > b1.ne.y || b2.ne.y < b1.sw.y ); }; // Finds the axis-aligned bounding box for a polygon Geo.findBoundingBox = function (polygon) { var min_x = Infinity, max_x = -Infinity, min_y = Infinity, max_y = -Infinity; // Only need to examine outer ring (polygon[0]) var num_coords = polygon[0].length; for (var c=0; c < num_coords; c++) { var coord = polygon[0][c]; if (coord[0] < min_x) { min_x = coord[0]; } if (coord[1] < min_y) { min_y = coord[1]; } if (coord[0] > max_x) { max_x = coord[0]; } if (coord[1] > max_y) { max_y = coord[1]; } } return [min_x, min_y, max_x, max_y]; }; // Convert geometry type to one of: 'point', 'line', 'polygon' Geo.geometryType = function(type) { if (type === 'Polygon' || type === 'MultiPolygon') { return 'polygon'; } else if (type === 'LineString' || type === 'MultiLineString') { return 'line'; } if (type === 'Point' || type === 'MultiPoint') { return 'point'; } }; // Geometric / weighted centroid of polygon // Adapted from https://github.com/Leaflet/Leaflet/blob/c10f405a112142b19785967ce0e142132a6095ad/src/layer/vector/Polygon.js#L57 Geo.centroid = function (polygon, relative = true) { if (!polygon || polygon.length === 0) { return; } let x = 0, y = 0, area = 0; let ring = polygon[0]; // only use first ring for now let len = ring.length; // optionally calculate relative to first coordinate to avoid precision issues w/small polygons let origin; if (relative) { origin = ring[0]; ring = ring.map(v => [v[0] - origin[0], v[1] - origin[1]]); } for (let i = 0, j = len - 1; i < len; j = i, i++) { let p0 = ring[i]; let p1 = ring[j]; let f = p0[1] * p1[0] - p1[1] * p0[0]; x += (p0[0] + p1[0]) * f; y += (p0[1] + p1[1]) * f; area += f * 3; } if (!area) { return; // skip degenerate polygons } let c = [x / area, y / area]; if (relative) { c[0] += origin[0]; c[1] += origin[1]; } return c; }; Geo.multiCentroid = function (polygons) { let n = 0; let centroid = null; for (let p=0; p < polygons.length; p++) { let c = Geo.centroid(polygons[p]); if (c) { // skip degenerate polygons centroid = centroid || [0, 0]; centroid[0] += c[0]; centroid[1] += c[1]; n++; } } if (n > 0) { centroid[0] /= n; centroid[1] /= n; } return centroid; // will return null if all polygons were degenerate }; Geo.signedPolygonRingAreaSum = function (ring) { let area = 0; let n = ring.length; for (let i = 0; i < n - 1; i++) { let p0 = ring[i]; let p1 = ring[i+1]; area += p0[0] * p1[1] - p1[0] * p0[1]; } area += ring[n - 1][0] * ring[0][1] - ring[0][0] * ring[n - 1][1]; return area; }; Geo.polygonRingArea = function (ring) { return Math.abs(Geo.signedPolygonRingAreaSum(ring)) / 2; }; // TODO: subtract inner rings Geo.polygonArea = function (polygon) { if (!polygon) { return; } return Geo.polygonRingArea(polygon[0]); }; Geo.multiPolygonArea = function (polygons) { let area = 0; for (let p=0; p < polygons.length; p++) { area += Geo.polygonArea(polygons[p]); } return area; }; Geo.ringWinding = function (ring) { let area = Geo.signedPolygonRingAreaSum(ring); if (area > 0) { return 'CW'; } else if (area < 0) { return 'CCW'; } // return undefined on zero area polygon };