Rotating polygon coordinates on a map (Google Map API) - javascript

I have a set of latitude and longitude coordinates in an array. When plotted as points on a Polygon in Google Maps, they form roughly an irregular oval pointing from west to east.
I would like to rotate the polygon to arbitrary degrees with a point near the far left (western) axis as the origin point.
What's important to me is that the overall distances are preserved — the total length and width of the polygon in miles should be preserved even though size of the polygon in pixels will obviously vary because of the map projection.
I've spent many hours Googling and searching on this site for an answer to this question but haven't been able to find one. Strictly speaking this is not a Google Maps issue — it's just a geometry issue related to rotating polygons on a map projection.
Here is more or less (some things dropped/renamed for simplicity) what I've been playing with so far:
function transpose_poly() {
//the polygon I'm transposing; these are actually in lng,lat not lat,lng
var poly = [
[165.2467094000077,11.90511591102683],[165.1960646350092,11.8776472999036],[165.163749733589,11.84385698549516],[165.1260503052001,11.79105209544025],[165.1174590975139,11.73721064669002],[165.1239723221977,11.68443896599666],[165.1455712453083,11.62322111902237],[165.1628352566873,11.60212814234246],[165.2801792409835,11.57112282455308],[165.3469838984795,11.55321856612457],[165.4267372080734,11.49929306693537],[165.565122175408,11.43334434041074],[165.7036021721537,11.37198352732909],[165.7982543390455,11.32757670668951],[165.942446703552,11.2660302522167],[166.0435044916007,11.23207374453692],[166.1628753311194,11.19161490039798],[166.3468035041342,11.13537294275959],[166.432786256031,11.11077339082378],[166.6460225244011,11.09603091173615],[166.9054486129032,11.10353634871533],[167.0953310801652,11.11920326808891],[167.2738338244123,11.14546202299651],[167.5299835821322,11.20017441185735],[167.7587090824888,11.25007287877568],[168.0532186132958,11.32987818697488],[168.3030699093596,11.40339603540862],[168.592055474493,11.49329084618948],[168.8894586866613,11.59767488596071],[169.1097084341002,11.70426500697907],[169.3388671138959,11.8464629880637],[169.47335151263,11.96284699062962],[169.4987805640997,12.00051052731504],[169.5113979458664,12.04997756596092],[169.496674063518,12.07975001861134],[169.4439862794831,12.10473302818016],[169.3792705121883,12.11718325976015],[169.2053586392944,12.12366910168141],[169.0210976722354,12.12171866909852],[168.7390558752391,12.08703266811138],[168.4733370821476,12.04764814638675],[168.1055698159765,12.00021651042535],[167.8745488025422,11.97152786285725],[167.5955303201492,11.94724207538445],[167.1571321566584,11.94152529858467],[166.8673995936747,11.95771709621411],[166.6698153277294,11.98911065050636],[166.4432968316392,12.03361885637251],[166.2604579582592,12.0693923391982],[166.0834945953367,12.09610600014998],[165.9206278637858,12.10688793842689],[165.7421018997999,12.1070118835482],[165.6244752224984,12.09489163127243],[165.4939687494391,12.0642833194958],[165.4055155587918,12.02521842289156],[165.3386147079753,11.97769336095395],[165.278157772496,11.92168821285675],[165.2467094000077,11.90511591102683],
];
var pos = marker.getPosition(); //where it transposes it to
var marker_lat = pos.lat();
var marker_lng = pos.lng();
var angle = document.getElementById("poly_angle").value; //rotational angle
var original_lat = 11.697222; //the original lat/lng of the polygon coordinates above
var original_lng = 165.27194399999996; //used to move the general polygon to a new set of coords as an offset
var new_poly = []; //the transformed polygon
//iterate over polygon array, create new array of Google Maps LatLng objects that are transposed and rotated
for(var i=0; i<poly.length; i++) {
new_poly.push( rotateLatLng( (poly[i][1]-original_lat)+marker_lat,(poly[i][0].lng()-original_lng)+marker_lng,angle));
}
// plot the polygon on the map
poly_obj = new google.maps.Polygon({
paths: poly_new,
strokeColor: "#ff763b",
strokeOpacity: 1,
strokeWeight: 1,
fillColor: "#ff763b",
fillOpacity: 0.25,
map: map,
visible: true
});
}
//this rotating function is cobbled together from code I found... it doesn't really work. It somewhat rotates it but distorts it terribly and gives really bizarre results
function rotateLatLng (pointLat,pointLng,angle) {
var pos = marker.getPosition();
var theX = pointLat;
var theY = pointLng;
var rotationTheta = angle;
var rotationThetaRad = rotationTheta*(Math.PI/180);
var rotationOriginX = pos.lat();
var rotationOriginY = pos.lng();
var newX;
var newY;
if (rotationOriginX == 0 && rotationOriginY == 0) {
newX = theX * Math.cos(rotationThetaRad) - Math.sin(rotationThetaRad) * theY;
newY = theX * Math.sin(rotationThetaRad) + Math.cos(rotationThetaRad) * theY;
} else {
newX = (theX - rotationOriginX) * Math.cos(rotationThetaRad) - (theY - rotationOriginY) * Math.sin(rotationTheta) + rotationOriginX;
newY = (theX - rotationOriginX) * Math.sin(rotationThetaRad) + (theY - rotationOriginY) * Math.cos(rotationTheta) + rotationOriginY;
}
return new google.maps.LatLng(newX,newY);
}
I don't think the above is necessarily the right way to do it at all. Any pointers would be helpful.
A much more simplified version of this problem would be to say that if I had an origin point of lat1,lng1, and another target point of lat2,lng2, how do I calculate lat3,lng3 which is defined as being the same distance between lat1,lng1 and lat2,lng2 but at an arbitrary angle? Because if I knew how to do that, applying that to the entire polygon should be a snap.

Related

add padding to google maps bounds.contains()

I have a sidebar which shows the names of the markers in the current map view of a google map. The sidebar contents change as the map gets moved:
google.maps.event.addListener(map, 'bounds_changed', function() {
document.getElementById("list").innerHTML = "";
var mklen = mkrs.length,
a = 0,
bnds = map.getBounds();
for (a; a < mklen; a++) {
var themk = mkrs[a];
if (bnds.contains(themk.getPosition())) {
var myli = document.createElement("li");
myli.innerHTML = themk.title;
document.getElementById("list").appendChild(myli);
}
}
});
That's working OK, but the thing is that the bounds.contains() is very strict - if just the bottom tip of the marker is on the map (ie, you can't see 99% of it) it gets listed on the sidebar. What I'd like is to have just the markers that are completely shown pass that test.
There are a couple of approaches that I can think of and I can't believe that nobody else has come up against this problem, so I'm wondering if there is a preference out of the following:
take the bounds and recalculate them to be smaller than the actual bounds and use those new bounds for the bounds.contains() test
calculate where the edges of the marker icons are (I guess using fromDivPixelToLatLng) then check that both the ne AND sw corners are within the bounds and if so, list the item
Before you ask, I haven't tried either of those - I'm more looking for advice on which would be best or even possible, or if there is another way to do this. Here's a fiddle demonstrating the issue, in case it clarifies
In case anybody finds this later, I ended up recalculating the bounds - it seemed to be the approach that involved the least overhead. Here's the function:
function paddedBounds(npad, spad, epad, wpad) {
var SW = map.getBounds().getSouthWest();
var NE = map.getBounds().getNorthEast();
var topRight = map.getProjection().fromLatLngToPoint(NE);
var bottomLeft = map.getProjection().fromLatLngToPoint(SW);
var scale = Math.pow(2, map.getZoom());
var SWtopoint = map.getProjection().fromLatLngToPoint(SW);
var SWpoint = new google.maps.Point(((SWtopoint.x - bottomLeft.x) * scale) + wpad, ((SWtopoint.y - topRight.y) * scale) - spad);
var SWworld = new google.maps.Point(SWpoint.x / scale + bottomLeft.x, SWpoint.y / scale + topRight.y);
var pt1 = map.getProjection().fromPointToLatLng(SWworld);
var NEtopoint = map.getProjection().fromLatLngToPoint(NE);
var NEpoint = new google.maps.Point(((NEtopoint.x - bottomLeft.x) * scale) - epad, ((NEtopoint.y - topRight.y) * scale) + npad);
var NEworld = new google.maps.Point(NEpoint.x / scale + bottomLeft.x, NEpoint.y / scale + topRight.y);
var pt2 = map.getProjection().fromPointToLatLng(NEworld);
return new google.maps.LatLngBounds(pt1, pt2);
}
and you call it like this:
var padbnds = paddedBounds(50, 70, 100, 30);
specifying how much padding you want on the north, south, east and west edges of the map respectively

Parametric equation to place a leaflet marker on the circumference of a circle is not precise?

I am working on an application where I have the center of a circle and the radius and I am plotting the circle with the help of Leaflet.
I placed a marker on the north most end of the circumference and made it draggable.
var circle = L.circle(coords, radius).addTo(map);
convertRadiusToLatitude = parseInt(response.radius)/111111;
var coordsOnRadius = [parseFloat(response.lat) + convertRadiusToLatitude, parseFloat(response.long)];
var markerOnRadius = L.marker(coordsOnRadius, {draggable: true}).addTo(map);
Now, this adds the marker to the circumference and now I wanted it to be draggable only on the circumference itself for which I used the parametric equation.
Parametric equation
x = Xc + R * cos(theta)
y = Yc + R * sin(theta)
Code for dragging
markerOnRadius.on('drag', function(e){
bearing = marker.getLatLng().bearingTo(markerOnRadius.getLatLng());
var markerOnRadiusX = parseFloat(response.lat) + ((0.000009 * parseFloat(response.radius)) * Math.cos( toRad(bearing) ));
var markerOnRadiusY = parseFloat(response.long) + ((0.000009 * parseFloat(response.radius)) * Math.sin( toRad(bearing) ));
markerOnRadius.setLatLng([markerOnRadiusX, markerOnRadiusY]);
});
The bearingTo method:
L.LatLng.prototype.bearingTo = function(other) {
var d2r = L.LatLng.DEG_TO_RAD;
var r2d = L.LatLng.RAD_TO_DEG;
var lat1 = this.lat * d2r;
var lat2 = other.lat * d2r;
var dLon = (other.lng-this.lng) * d2r;
var y = Math.sin(dLon) * Math.cos(lat2);
var x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
var brng = Math.atan2(y, x);
brng = parseInt( brng * r2d );
brng = (brng + 360) % 360;
return brng;
};
Issue
When I start dragging the marker, this code is working fine and brings it back to the circumference at the bearing at which the marker is dragged to. But there is one problem, the coords on the circumference are slightly off and in terms of longitude. When the bearing is 0 (north), the coords are perfect, but when it is 90 (east), the longitude is slightly less that it should for the marker to be at the circumference.
Again at 180 (south), coords are perfect, but at 270 (west), the longitude calculated is slightly less and the marker tends towards the radius again.
So basically if you visualize the marker being dragged, it starts perfectly on the north end and starts coming inside the circle slightly increasing with the bearing till it reacher 90 and then starts going towards the circumference again till 180 when it is perfect again.
It forms more like a ellipse if you get the gist of it.
Could anyone tell me why is longitude coming a little off and why the marker moves in an elliptical path. Has it something to do with the world coordinates and window coordinates. Or are my equations slightly off somewhere?
It does look like a projection issue. In your dragging code you are basically doing
lat = a + r cos(baring)
long = b + r sin(baring)
giving a circle in the Lat-Long coordinates. This would work fine if you were at the equator with Mercator projection. You will get more distortion as you move further towards the polls.
Assume you are using the defaults for Leaflet reference doc You have the EPSG3857 Web Mercator coordinates.
If you want to ensure you have a exact circle it will be better to work using screen coordinates. You can get these using methods on the ICRS objects. First get the coordinate system L.CRS.EPSG3857 and use the latLngToPoint and pointToLatLng methods.
var crs = L.CRS.EPSG3857;
var zoom = ...; // how you calculate your zoom factor
markerOnRadius.on('drag', function(e){
var markerLL = marker.getLatLng()
var morLL = markerOnRadius.getLatLng();
var markerP = crs.latLngToPoint(markerLL,zoom);
var morP = crs.latLngToPoint(morLL,zoom);
// get the distance between the two points
var dist = markerP.distanceTo(morP);
// Get the vector from center to point
var A = morP.subtract(markerP);
// scale so its of the desired length
var B = A. multiplyBy( factor / dist);
// Add on the center
var C = markerP.add(B);
// Convert back to LatLong
var D = crs.pointToLatLng(C,zoom);
markerOnRadius.setLatLong(D);
});

How to get shortest distance between two shapes on Google Maps, using JavaScript?

I am using the Google Maps API in my application (ASP.NET with MVC).
I have an array of coordinates (each consisting of latitude and longitude), let us call it "origin" (this can be a polygon, polyline or marker) and another array of coordinates, let us call that "destination" (can be a polygon, polyline or marker either).
I want to calculate the shortest distance between "origin" and "destination". How can I do that?
Well, looking from it at a mathematical standpoint:
your problem is to find the shortest distance between a point in space and a vektor line or plane.
So if you have your coords in arrays like [a1,a2,a3] and [b1,b2,b3] the distance between this 2 points in 3 dimensional space is like the Pythagorean theorem with three elements:
sqrt[(a1-b1)²+(a2-b2)²+(a3-b3)²]=d(a,b).
I know this does not take the curvature of the earth into account but for "short" distances this is is not important.
If you understand some math the wikipedia article might help you as well. http://en.wikipedia.org/wiki/Euclidean_distance#Three_dimensions
Edit 12.08.14:
To take the curvature of the earth into account, you can do a simple calculation:
(1) You already know the distance of the earth
(2) you know the approx. radius of the earth
With your start point(A) and your destination(B) known, you now build a triangle with the center of the earth(C). You do this to now calculate the angle at (C) (sin/cos/tan). With that angle you can now get the length of the earth (including the curvature).
([boundary of earth]/360°)*[angle at (C)] = the dinstance from (A) to (B) over the curvature of the earth.
I recommend that you use the Spherical Law of Cosines to calculate the distance between the points. If you have an array of latitude and longitude for the origins, and an array of latitude and longitude coordinates for destinations, then you can do something like this:
var origins = [{lat: "35.5", lon: "-80.0"}, ...]; // Array of origin coordinates
var destinations = [{lat: "34.5", lon: "-80.0"}, ...]; // Array of destination coordinates
var shortestDistance = null;
var shortestPair = [];
for (i = 0; i < origins.length; i++) {
for (j = 0; j < destinations.length; j++) {
var lat1 = origins[i].lat.toRadians();
var lat2 = destinations[j].lat.toRadians();
var lon = (destinations[j].lon - origins[i].lon).toRadians();
var R = 6371; // gives distance in kilometers
var calcDistance = Math.acos(Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon)) * R;
if (shortestDistance === null || calcDistance < shortestDistance) {
shortestPair[0] = origins[i]; // Store the origin coordinates
shortestPair[1] = destinations[j]; // Store the destination coordinates
shortestDistance = calcDistance; // Update the shortest distance
}
}
}
/* After this runs, you'll have the array indexes for the origin and
destination with the shortest distance as well as the actual distance (kilometers)
in the array shortestPair and the variable shortestDistance respectively.
For miles, divide shortestDistance by 1.609344
For nautical miles, divide shortestDistance by 1.852
*/
This seems to be a simpler approach than attempting to use the Maps API for the distance calculations. The above formula was sourced from http://www.movable-type.co.uk/scripts/latlong.html. You could also use the haversine formula if you need the calculations to be more accurate; it's detailed on the page that I linked.
One solution would be to take one of the options found here and calculate the distance from each point in the origin to each point in the destination. The smallest distance would be the distance between your two shapes.
Code might look like this (untested):
var minDistance = Number.POSITIVE_INFINITY;
for (var i=0; i<origin.length; i++){
for (var j=0; j<destination.length; j++){
var dist = google.maps.geometry.spherical.computeDistanceBetween(origin[i], destination[j]);
if (dist < minDistance)
minDistance = dist;
}
}
This could likely be optimized if performance is an issue. For more information on that, I would look at this question and its answers which deal with the same issue, although from a purely mathematical perspective.
function moveAlongPath(points, distance, index) {
index = index || 0;
if (index < points.length && typeof points[index +1] !="undefined") {
var polyline = new google.maps.Polyline({
path: [points[index], points[index + 1]],
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2
});
var distanceToNextPoint = polyline.Distance();
if (distance <= distanceToNextPoint) {
return polyline_des(points[index],points[index + 1], distance);
}
else {
return moveAlongPath(points,
distance - distanceToNextPoint,
index + 1);
}
}
else {
return null;
}
}

Compute a distance including elevation/terrain data for a KmlLinestring

The Google Earth Desktop Application shows the both the map length and ground length of a line.
In the Google Earth plugin I want to do a similar thing, that is I wish to determine the ground length of a tessellated KmlLineString taking the terrain into account.
Can I do this, and if so, how?
You can certainly get the length pretty easily if you use the earth-api-utility-library. Using that you can do.
var length = (new geo.Path(linestring)).distance();
Granted this method does not take the terrain into account - but there are a number of caveats you should be aware of before trying calculate distances using an elevation gradient.
Firstly any differences between topographic and direct distance are minimal in most cases. Indeed many quality GPS receivers simply don't take any changes in elevation into account when calculating distances.
Secondly ground altitude is one of the most unreliable pieces data. Using a gradient based on elevation to determine distance will often produce greater inaccuracy in distance measurements than using a simple 'as the crow flies' measure.
Bearing that in mind, if you still wanted to do it then one way would be something like the following.
Sample the line string at certain points (say every 10 meters).
Get the ground altitude at each point.
Convert each point to Cartesian coordinates
Calculate the angular distances between each Cartesian point in sequence.
You can improve your precision of this kind of method in two ways, either by increasing the sampling rate (say every meter) or by applying a smoothing procedure to the results.
For a rougher version, you could just loop over the coordinates in the the KmlLinestring itself, rather than resampling at some set distance. You would use the latitude, longitude of the coordinate to get the ground altitude at each point. Then you would construct a Cartesian coordinate from this data (latitude, longitude, elevation => X,Y,Z) and work out the angular distance between it and the next point...and so on.
something like the following idea should work - although it is written here and untested!
var EARTH_RADIUS = 6378135; // approximate in meters
var degreestoRadians = function(degrees) {
return degrees * Math.PI / 180;
}
var arcLength = function(point1 , point2) {
var length = Math.sqrt(Math.pow(point1.X-point2.X, 2)
+ Math.pow(point1.Y-point2.Y, 2)
+ Math.pow(point1.Z-point2.Z, 2));
var angle = 2 * Math.asin(length/2/EARTH_RADIUS);
return EARTH_RADIUS * angle;
}
var sphericalToCartesian = function(latitude, longitude, altitude) {
var phi = degreestoRadians(latitude);
var theta = degreestoRadians(longitude);
var rho = EARTH_RADIUS + altitude;
return {
X: Math.cos(phi) * Math.cos(theta) * rho,
Y: Math.cos(phi) * Math.sin(theta) * rho,
Z: Math.sin(phi) * rho
}
}
var topographicDistance = function(linestring) {
var coordinates = linestring.getCoordinates(); //KmlCoordArray
var last = null;
var distance = 0;
for(var i = 0; i < coordinates.length; i++) {
var coord = coordinates.get(i); //KmlCoord
var lat = coord.getLatitude();
var lng = coord.getLongitude();
var alt = ge.getGlobe().getGroundAltitude(lat, lng);
var latest = sphericalToCartesian(lat, lng, alt);
if(last != null) {
distance += arcLength(last, latest);
}
last = latest;
}
return distance;
}
You would use it like so...
var distance = topographicDistance(yourLinestring);

drawing a line in google maps perpendicular to two points

I have two coordinates for which I would like to draw a perpendicular line of equal length. Is there either a simple google maps offset for this or a clean javascript approach by which I might accomplish this? What would that be?
Here is what I have thus far. As you can see, I plot the two points as markers and then attempt to draw a line between them, except I need to get that line perpendicular to the line between the two coordinates.
var locations = [
['', position.coords.latitude, position.coords.longitude, 1],
['', llat, llng, 2]
];
var marker, i;
for ( var i = 0; i < locations.length; i++ )
{
marker = new google.maps.Marker({
position: new google.maps.LatLng(locations[i][1], locations[i][2]),
map: map
});
}
var borderPlanCoordinates = [
new google.maps.LatLng(llat, position.coords.longitude),
new google.maps.LatLng(position.coords.latitude,llng)
];
var borderPath = new google.maps.Polyline({
path: borderPlanCoordinates,
strokeColor: "#FF0000",
strokeOpacity: 1.0,
strokeWeight: 10,
map: map
});
borderPath.setMap(map);
So you have two points with coordinates (x1,y1) and (x2, y2) and you want to draw a line segment whose length is the distance between them, which is the perpendicular bisector of the segment connecting them, and which is bisected by said segment?
Probably the simplest way is to set cx = (x1 + x2)/2, cy = (y1+y2)/2), dx = (x2-x1)/2, dy = (y2-y1)/2 and then draw a line from (cx-dy, cy+dx) to (cx+dy, cy-dx).
This works because (cx, cy) is the midpoint of the segment you want, and then you're just taking the vector from that midpoint to (x2,y2) and rotating it by plus and minus 90 degrees to find the endpoints of the segment you want to draw.
I tried this solution, the middle of the segment is ok BUT it doesn't look perpendicular on google maps, I suspect because of spherical projection (not orthonormal).
Here is a solution that works :
spherical = google.maps.geometry.spherical;
var F = new google.maps.LatLng(latF, longF);
var T = new google.maps.LatLng(latT, longT);
// M is the middle of [FT]
var latM = (latF + latT)/2;
var longM = (longF + longT)/2;
var M = new google.maps.LatLng(latM, longM);
// Get direction of the segment
var heading = spherical.computeHeading(F, T);
var dist = 200; // distance in meters
// Place point A that is oriented at 90° in a distance of dist from M
var A = spherical.computeOffset(M, dist, heading+90);
var perpendicularCoordinates = [M, A ];
var perpendicular = new google.maps.Polyline({
path: perpendicularCoordinates,
geodesic: true,
strokeColor: '#FF0000',
strokeOpacity: 1.0,
strokeWeight: 2,
map: map
});
Here is the related fiddle : http://jsfiddle.net/8m7vm650/15/
Please note that you need to use an optional google maps library called geometry which is done by adding libraries=geometry to query string when loading maps : http://maps.googleapis.com/maps/api/js?libraries=geometry&sensor=false

Categories

Resources