Last active
January 19, 2018 08:17
-
-
Save marinho/8a7e71b53c7da97f9579146742bb54de to your computer and use it in GitHub Desktop.
Projection of a plain polygon (with paths) into a geographic area (including rotation)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** This code was written as a solution for the following Stack Overflow entry: | |
| * https://stackoverflow.com/questions/48307362/draw-a-multi-polygon-with-metric-dimensions-over-part-of-a-sphere-using-d3-geo/48336677 | |
| * | |
| * Even though it seems to work fine, this code has at least 2 needs for improvements: | |
| * - some of the types and functions here actually exist in existing libraries and could be used from them instead | |
| * - d3-geo's mercator projection inverted should be able to do the same job more elegantly, in combination with | |
| * other d3-geo features | |
| */ | |
| import * as turf from '@turf/turf'; // https://npmjs.com/package/@turf/turf | |
| import * as LatLon from 'geodesy/latlon-spherical'; // // https://npmjs.com/package/geodesy | |
| // Example polygon - looks like an arrow pointing up | |
| export const plainPolygon = { | |
| rect: {"x":30,"y":40,"width":20,"height":10}, | |
| paths: [[[0,10],[0,0],[20,0],[20,10]],[[10,10],[5,0],[10,3],[15,0]]] | |
| }; | |
| // Example geographic areas to fit polygons into | |
| export const about20Degrees = { | |
| leftTop: [13.377948, 52.521293], | |
| leftBottom: [13.377962, 52.521250], | |
| rightBottom: [13.378174, 52.521275], | |
| rightTop: [13.378161, 52.521318] | |
| }; | |
| // Types | |
| export type Position = number[]; | |
| export interface GeoReferences { | |
| leftTop: Position, // lng / x, lat / y | |
| leftBottom: Position, | |
| rightBottom: Position, | |
| rightTop?: Position | |
| } | |
| export interface Rect { | |
| x: number; | |
| y: number; | |
| width: number; | |
| height: number: | |
| } | |
| export interface PlainInfo { | |
| rect: Rect; | |
| paths: number[][]; | |
| } | |
| // THIS IS WHAT MATTERS | |
| export function projectPlainOverGeoArea(plain: PlainInfo, geoReferences: GeoReferences): FeatureCollection<MultiPolygon> { | |
| // preparation | |
| const geoRect = getRect(geoReferences); | |
| const vertBearing = haversineBearing( | |
| geoReferences.leftBottom, | |
| geoReferences.leftTop | |
| ); | |
| // Convert to geo using left/bottom as pivot | |
| const paths = plain.paths.map(path => pathToCoords(path)); | |
| // Pack path into features | |
| const features = paths | |
| .map(p => ({ | |
| type: 'MultiPolygon', | |
| coordinates: [[p]] | |
| })) | |
| .map(p => ({ | |
| type: 'Feature', | |
| properties: {}, | |
| geometry: p | |
| })); | |
| // Rotation | |
| const collection = { type: 'FeatureCollection', features }; | |
| const rotationOptions = { pivot: geoReferences.leftBottom }; | |
| const rotated = turf.transformRotate(collection, vertBearing, rotationOptions); | |
| return { | |
| type: 'FeatureCollection', | |
| features: rotated.features | |
| }; | |
| // Nested functions | |
| function pathToCoords(path) { | |
| return path | |
| .map(point => projectToGeo(point)); | |
| } | |
| function projectToGeo(coords: Position): Position { | |
| const horzDistance = coords[0] / plain.rect.width * geoRect.width; | |
| const vertDistance = coords[1] / plain.rect.height * geoRect.height; | |
| const horzDestination = haversineDestination(geoReferences.leftBottom, horzDistance, 90); | |
| const vertDestination = haversineDestination(geoReferences.leftBottom, vertDistance, 0); | |
| return [horzDestination[0], vertDestination[1]]; | |
| } | |
| function getRect(geoReferences: GeoReferences): Rect { | |
| const width = haversineDistance( | |
| geoReferences.leftBottom, | |
| geoReferences.rightBottom | |
| ); | |
| const height = haversineDistance( | |
| geoReferences.leftBottom, | |
| geoReferences.leftTop | |
| ); | |
| return { | |
| x: 0, | |
| y: 0, | |
| width: width, | |
| height: height | |
| }; | |
| } | |
| function haversineDistance(point1: number[], point2: number[]): number { | |
| return latLongFromPair(point1).distanceTo(latLongFromPair(point2)); | |
| } | |
| function haversineBearing(point1: number[], point2: number[]): number { | |
| return latLongFromPair(point1).bearingTo(latLongFromPair(point2)); | |
| } | |
| function haversineDestination(origin: number[], distance: number, bearing: number): Position { | |
| const destination = latLongFromPair(origin).destinationPoint(distance, bearing); | |
| return [ | |
| +destination.lon.toFixed(6), | |
| +destination.lat.toFixed(6) | |
| ]; | |
| } | |
| function latLongFromPair(pair: number[]): any { | |
| return LatLon(pair[1], pair[0]); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment