I'm using the following function to generate random geo coordinates within a specified radius from a seed point:
function randomGeo(center, radius) {
var y0 = center.latitude;
var x0 = center.longitude;
var rd = radius / 111300;
var u = Math.random();
var v = Math.random();
var w = rd * Math.sqrt(u);
var t = 2 * Math.PI * v;
var x = w * Math.cos(t);
var y = w * Math.sin(t);
var xp = x / Math.cos(y0);
return {
'latitude': y + y0,
'longitude': xp + x0
};
}
I do this in a loop, several times, using a 2000m radius and the following seed point:
location: { // Oxford
latitude: 51.73213,
longitude: -1.20631
}
I'd expect all of these results to be within 2000m; instead, I'm seeing values upwards of 10000m:
[ { latitude: 51.73256540025445, longitude: -1.3358092771716716 }, // 3838.75070783092
{ latitude: 51.7214165686511, longitude: -1.1644147572878725 }, // 3652.1890457730474
{ latitude: 51.71721400063117, longitude: -1.2082082568884593 }, // 8196.861603477768
{ latitude: 51.73583824510363, longitude: -1.0940424351649711 }, // 5104.820455873758
{ latitude: 51.74017571473442, longitude: -1.3150742602532257 }, // 4112.3279147866215
{ latitude: 51.73496163915278, longitude: -1.0379454413532996 }, // 9920.01459343298
{ latitude: 51.73582333121239, longitude: -1.0939302282840453 }, // 11652.160906253064
{ latitude: 51.72145745285658, longitude: -1.2491630482776055 }, // 7599.550622138115
{ latitude: 51.73036335927129, longitude: -1.3516902043395063 }, // 8348.276271205428
{ latitude: 51.748104753808924, longitude: -1.2669212014250266 }, // 8880.760669882042
{ latitude: 51.72010719621805, longitude: -1.327161328951446 }, // 8182.466715589904
{ latitude: 51.725727610071125, longitude: -1.0691503599266818 } ] // 2026.3687763449955
Given that I (shamelessly!) plagiarized this solution from elsewhere (albeit I've seen several similar implementations), I can't seem to figure out where the math is going wrong.
(Also, in case you want it, this is how I'm calculating the distance. Pretty sure this is correct.)
function distance(lat1, lon1, lat2, lon2) {
var R = 6371000;
var a = 0.5 - Math.cos((lat2 - lat1) * Math.PI / 180) / 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * (1 - Math.cos((lon2 - lon1) * Math.PI / 180)) / 2;
return R * 2 * Math.asin(Math.sqrt(a));
}
The problem seems to stem from the fact that this is just an inaccurate calculation depending on which center point you are using. Particularly this line:
var xp = x / Math.cos(y0);
Removing this line and changing longitude to
'longitude': x + x0
Seems to keep all of the points within the specified radius, although without this line it seems the points will not completely fill out east to west in some cases.
Anyway, I found someone experiencing a similar issue here with someone elses Matlab code as a possible solution. Depends on how uniformly spread out you need the random points if you wanted to work with a different formula.
Here is a google maps visualization of what's going on with your provided formula:
<!doctype html>
<html>
<head>
<script type="text/javascript" src="//maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var distanceLimit = 2000; //in meters
var numberRandomPoints = 200;
var mapZoomLevel = 11;
var locationindex = 0;
var locations = [
{'name': 'Oxford, England', 'latitude': 51.73213, 'longitude': -1.20631},
{'name': 'Quito, Ecuador', 'latitude': -0.2333, 'longitude': -78.5167},
{'name': 'Ushuaia, Argentina', 'latitude': -54.8000, 'longitude': -68.3000},
{'name': 'McMurdo Station, Antartica', 'latitude': -77.847281, 'longitude': 166.667942},
{'name': 'Norilsk, Siberia', 'latitude': 69.3333, 'longitude': 88.2167},
{'name': 'Greenwich, England', 'latitude': 51.4800, 'longitude': 0.0000},
{'name': 'Suva, Fiji', 'latitude': -18.1416, 'longitude': 178.4419},
{'name': 'Tokyo, Japan', 'latitude': 35.6833, 'longitude': 139.6833},
{'name': 'Mumbai, India', 'latitude': 18.9750, 'longitude': 72.8258},
{'name': 'New York, USA', 'latitude': 40.7127, 'longitude': -74.0059},
{'name': 'Moscow, Russia', 'latitude': 55.7500, 'longitude': 37.6167},
{'name': 'Cape Town, South Africa', 'latitude': -33.9253, 'longitude': 18.4239},
{'name': 'Cairo, Egypt', 'latitude': 30.0500, 'longitude': 31.2333},
{'name': 'Sydney, Australia', 'latitude': -33.8650, 'longitude': 151.2094},
];
</script>
</head>
<body>
<div id="topbar">
<select id="location_switch">
<script>
for (i=0; i<locations.length; i++) {
document.write('<option value="' + i + '">' + locations[i].name + '</option>');
}
</script>
</select>
<img src="http://google.com/mapfiles/ms/micons/ylw-pushpin.png" style="height:15px;"> = Center
<img src="https://maps.gstatic.com/mapfiles/ms2/micons/red.png" style="height:15px;"> = No Longitude Adjustment
<img src="https://maps.gstatic.com/mapfiles/ms2/micons/pink.png" style="height:15px;"> = With Longitude Adjustment (var xp = x / Math.cos(y0);)
</div>
<div id="map_canvas" style="position:absolute; top:30px; left:0px; height:100%; height:calc(100% - 30px); width:100%;overflow:hidden;"></div>
<script>
var markers = [];
var currentcircle;
//Create the default map
var mapcenter = new google.maps.LatLng(locations[locationindex].latitude, locations[locationindex].longitude);
var myOptions = {
zoom: mapZoomLevel,
scaleControl: true,
center: mapcenter
};
var map = new google.maps.Map(document.getElementById('map_canvas'), myOptions);
//Draw default items
var centermarker = addCenterMarker(mapcenter, locations[locationindex].name + '<br>' + locations[locationindex].latitude + ', ' + locations[locationindex].longitude);
var mappoints = generateMapPoints(locations[locationindex], distanceLimit, numberRandomPoints);
drawRadiusCircle(map, centermarker, distanceLimit);
createRandomMapMarkers(map, mappoints);
//Create random lat/long coordinates in a specified radius around a center point
function randomGeo(center, radius) {
var y0 = center.latitude;
var x0 = center.longitude;
var rd = radius / 111300; //about 111300 meters in one degree
var u = Math.random();
var v = Math.random();
var w = rd * Math.sqrt(u);
var t = 2 * Math.PI * v;
var x = w * Math.cos(t);
var y = w * Math.sin(t);
//Adjust the x-coordinate for the shrinking of the east-west distances
var xp = x / Math.cos(y0);
var newlat = y + y0;
var newlon = x + x0;
var newlon2 = xp + x0;
return {
'latitude': newlat.toFixed(5),
'longitude': newlon.toFixed(5),
'longitude2': newlon2.toFixed(5),
'distance': distance(center.latitude, center.longitude, newlat, newlon).toFixed(2),
'distance2': distance(center.latitude, center.longitude, newlat, newlon2).toFixed(2),
};
}
//Calc the distance between 2 coordinates as the crow flies
function distance(lat1, lon1, lat2, lon2) {
var R = 6371000;
var a = 0.5 - Math.cos((lat2 - lat1) * Math.PI / 180) / 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * (1 - Math.cos((lon2 - lon1) * Math.PI / 180)) / 2;
return R * 2 * Math.asin(Math.sqrt(a));
}
//Generate a number of mappoints
function generateMapPoints(centerpoint, distance, amount) {
var mappoints = [];
for (var i=0; i<amount; i++) {
mappoints.push(randomGeo(centerpoint, distance));
}
return mappoints;
}
//Add a unique center marker
function addCenterMarker(centerposition, title) {
var infowindow = new google.maps.InfoWindow({
content: title
});
var newmarker = new google.maps.Marker({
icon: 'http://google.com/mapfiles/ms/micons/ylw-pushpin.png',
position: mapcenter,
map: map,
title: title,
zIndex: 3
});
google.maps.event.addListenerOnce(map, 'tilesloaded', function() {
infowindow.open(map,newmarker);
});
markers.push(newmarker);
return newmarker;
}
//Draw a circle on the map
function drawRadiusCircle (map, marker, distance) {
currentcircle = new google.maps.Circle({
map: map,
radius: distance
});
currentcircle.bindTo('center', marker, 'position');
}
//Create markers for the randomly generated points
function createRandomMapMarkers(map, mappoints) {
for (var i = 0; i < mappoints.length; i++) {
//Map points without the east/west adjustment
var newmappoint = new google.maps.LatLng(mappoints[i].latitude, mappoints[i].longitude);
var marker = new google.maps.Marker({
position:newmappoint,
map: map,
title: mappoints[i].latitude + ', ' + mappoints[i].longitude + ' | ' + mappoints[i].distance + 'm',
zIndex: 2
});
markers.push(marker);
//Map points with the east/west adjustment
var newmappoint = new google.maps.LatLng(mappoints[i].latitude, mappoints[i].longitude2);
var marker = new google.maps.Marker({
icon: 'https://maps.gstatic.com/mapfiles/ms2/micons/pink.png',
position:newmappoint,
map: map,
title: mappoints[i].latitude + ', ' + mappoints[i].longitude2 + ' | ' + mappoints[i].distance2 + 'm',
zIndex: 1
});
markers.push(marker);
}
}
//Destroy all markers
function clearMarkers() {
for (var i = 0; i < markers.length; i++) {
markers[i].setMap(null);
}
markers = [];
}
$('#location_switch').change(function() {
var newlocation = $(this).val();
clearMarkers();
mapcenter = new google.maps.LatLng(locations[newlocation].latitude, locations[newlocation].longitude);
map.panTo(mapcenter);
centermarker = addCenterMarker(mapcenter, locations[newlocation].name + '<br>' + locations[newlocation].latitude + ', ' + locations[newlocation].longitude);
mappoints = generateMapPoints(locations[newlocation], distanceLimit, numberRandomPoints);
//Draw default items
currentcircle.setMap(null);
drawRadiusCircle(map, centermarker, distanceLimit);
createRandomMapMarkers(map, mappoints);
});
</script>
</body>
</html>
You can generate points with a random bearing and distance from the center by moving some distance using vincenty distances (see this stackoverflow answer). In Python, for example, you could use the geopy package.
import random
from geopy import Point
from geopy.distance import geodesic
def generate_point(center: Point, radius: int) -> Point:
radius_in_kilometers = radius * 1e-3
random_distance = random.random() * radius_in_kilometers
random_bearing = random.random() * 360
return geodesic(kilometers=random_distance).destination(center, random_bearing)
radius = 2000
center = Point(51.73213, -1.20631)
points = [generate_point(center, radius) for _ in range(3000)]
Distances are confirmed with:
assert all(geodesic(center, point).meters <= radius for point in points)
Here a simple Vanilla Javascript solution that works like a charm. I want to give credits where it's due and where I found it : https://gist.github.com/fajarlabs/af9e0859fc29b2107bd1797536d2ff2d
/**
* Generates number of random geolocation points given a center and a radius.
* #param {Object} center A JS object with lat and lng attributes.
* #param {number} radius Radius in meters.
* #param {number} count Number of points to generate.
* #return {array} Array of Objects with lat and lng attributes.
*/
function generateRandomPoints(center, radius, count) {
var points = [];
for (var i=0; i<count; i++) {
points.push(generateRandomPoint(center, radius));
}
return points;
}
/**
* Generates number of random geolocation points given a center and a radius.
*
* #param {Object} center A JS object with lat and lng attributes.
* #param {number} radius Radius in meters.
* #return {Object} The generated random points as JS object with lat and lng attributes.
*/
function generateRandomPoint(center, radius) {
var x0 = center.lng;
var y0 = center.lat;
// Convert Radius from meters to degrees.
var rd = radius/111300;
var u = Math.random();
var v = Math.random();
var w = rd * Math.sqrt(u);
var t = 2 * Math.PI * v;
var x = w * Math.cos(t);
var y = w * Math.sin(t);
var xp = x/Math.cos(y0);
// Resulting point.
return {'lat': y+y0, 'lng': xp+x0};
}
// Usage Example.
// Generates 100 points that is in a 1km radius from the given lat and lng point.
var randomGeoPoints = generateRandomPoints({'lat':24.23, 'lng':23.12}, 1000, 100);
console.log(randomGeoPoints);
Related
I am making hexagon grid for my game based on Google Map v3 and got a problem.
After I click in one hexagon are showing differents values, not one the same as for marker inside of the all hexagon.
The right value is showing just in the left down corner of quarter hexagon.
The value of coord_slug is making based on coordinates lat, lng.
What I have to do hexagon and marker values being the same ?
In this way 55.3,14.8 for upper and 55.25,1485 for down hexagon.
I need those values in a game for downloading dates from database.
The part responsible for displaying the value:
function set_window(event) {
// Set Parameters
var lat = event.latLng.lat();
var lng = event.latLng.lng();
var coord_slug = (Math.round(lat * 20) / 20) + ',' + (Math.round(lng * 20) / 20);
alert(coord_slug);
}
The working part of the script here:
function round_down(n) {
if (n > 0) {
return Math.ceil(n / 0.05) * 0.05;
} else {
return 0;
}
}
var map;
var pointCount = 0;
var locations = [];
var gridWidth = 3660; // hex tile size in meters
var bounds;
var places = [
[55.3, 14.8],
[55.25, 14.85],
]
var SQRT3 = 1.73205080756887729352744634150587236;
$(document).ready(function(){
bounds = new google.maps.LatLngBounds();
map = new google.maps.Map(document.getElementById("map_canvas"), {center: {lat: 55.27, lng: 14.8}, zoom: 10});
// Adding a marker just so we can visualize where the actual data points are.
// In the end, we want to see the hex tile that contain them
places.forEach(function(place, p){
latlng = new google.maps.LatLng({lat: place[0], lng: place[1]});
marker = new google.maps.Marker({
position: latlng,
map: map})
marker.addListener('click', set_window);
// Fitting to bounds so the map is zoomed to the right place
bounds.extend(latlng);
});
// Now, we draw our hexagons! (or try to)
locations = makeBins(places);
locations.forEach(function(place, p){
drawHorizontalHexagon(map, place, gridWidth);
})
});
function drawHorizontalHexagon(map, position, radius){
var coordinates = [];
for(var angle= 0;angle < 360; angle+=60) {
coordinates.push(google.maps.geometry.spherical.computeOffset(position, radius, angle));
}
// Construct the polygon.
var polygon = new google.maps.Polygon({
paths: coordinates,
position: position,
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35,
geodesic: true
});
polygon.setMap(map);
polygon.addListener('click', set_window);
}
// Below is my attempt at porting binner.py to Javascript.
// Source: https://github.com/coryfoo/hexbins/blob/master/hexbin/binner.py
function distance(x1, y1, x2, y2){
console.log(x1, y1, x2, y2);
result = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
console.log("Distance: ", result);
return
}
function nearestCenterPoint(value, scale){
div = value / (scale/2);
console.log("div", div);
mod = value % (scale/2);
console.log("mod", mod);
if(div % 2 == 1){
increment = 1;
} else{
increment = 0;
}
rounded = scale / 2 * (div + increment);
if(div % 2 === 0){
increment = 1;
} else{
increment = 0;
}
rounded_scaled = scale / 2 * (div + increment);
result = [rounded, rounded_scaled]
console.log("nearest centerpoint to", value, result);
return result;
}
function makeBins(data){
bins = [];
data.forEach(function(place, p){
x = place[0];
y = place[1];
console.log("Original location:", x, y);
px_nearest = nearestCenterPoint(x, gridWidth);
py_nearest = nearestCenterPoint(y, gridWidth * SQRT3);
z1 = distance(x, y, px_nearest[0], py_nearest[0]);
z2 = distance(x, y, px_nearest[1], py_nearest[1]);
if(z1 > z2){
bin = new google.maps.LatLng({lat: px_nearest[0], lng: py_nearest[0]});
console.log("Final location:", px_nearest[0], py_nearest[0]);
} else {
bin = new google.maps.LatLng({lat: px_nearest[1], lng: py_nearest[1]});
console.log("Final location:", px_nearest[1], py_nearest[1]);
}
bins.push(bin);
})
return bins;
}
function set_window(event) {
// Set Parameters
var lat = event.latLng.lat();
var lng = event.latLng.lng();
var coord_slug = (Math.round(lat * 20) / 20) + ',' + (Math.round(lng * 20) / 20);
alert(coord_slug);
}
<html>
<head>
<script data-require="jquery#*" data-semver="2.2.0" src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<script data-require="bootstrap#*" data-semver="3.3.6" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<link data-require="bootstrap-css#3.3.6" data-semver="3.3.6" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry"></script>
</head>
<body>
<div id="map_canvas" style="width:100%; height:80vh;">
</div>
</body>
</html>
Additional link:
The working part of the script in Plunger
You are setting a position attribute to your Polygons, which seems to be what you want to display...
So you can replace the following
polygon.addListener('click', set_window);
By this:
polygon.addListener('click', function() {
var polyPosition = this.position.lat() + ', ' + this.position.lng();
alert(polyPosition);
});
I have a mapbox example where I do the following:1) setup a view with a pitch angle of 60 degrees2) map the canvas corner points to get the viewing frustrum as a trapezoid3) change the bearing by 30 degrees and get and draw the trapezoid again4) zoom out to see the trapezoids5) manually rotate the first trapezoid by 30 degrees6) draw the rotated trapezoid7) change the pitch to zero to look down at the imageWhen I do this, the manually rotated trapezoid (red in the image) does not match the one generated using the setBearing() call (purple in the image). It appears to be skewed improperly and I have been looking at the manual rotate code for 8 hours and cannot figure out why. Am I dealing with curvature of the earth rotated co-ordinate issues or? Can someone sort this out? Thanks!
mapboxgl.accessToken = 'pk.eyJ1IjoiZm1hY2RlZSIsImEiOiJjajJlNWMxenowNXU2MzNudmkzMndwaGI3In0.ALOYWlvpYXnlcH6sCR9MJg';
var map;
function addLayerToMap(name, points, color, width) {
map.addLayer({
"id": name,
"type": "line",
"source": {
"type": "geojson",
"data": {
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": points
}
}
},
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": color,
"line-width": width
}
});
}
function pointsRotate(points, cx, cy, angle){
var radians = (Math.PI / 180) * angle;
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var newpoints = [];
function rotate(x, y) {
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) + (-sin * (x - cx)) + cy;
return [nx, ny];
}
for(var i=0;i<points.length;i++) {
newpoints[i] = rotate(points[i][0],points[i][1]);
}
return(newpoints);
}
function convertTrapezoidToPath(trap) {
return([
[trap.Tl.lng, trap.Tl.lat], [trap.Tr.lng, trap.Tr.lat],
[trap.Br.lng, trap.Br.lat], [trap.Bl.lng, trap.Bl.lat],
[trap.Tl.lng, trap.Tl.lat] ]);
}
function getViewTrapezoid() {
var canvas = map.getCanvas();
var trap = {};
trap.Tl = map.unproject([0,0]);
trap.Tr = map.unproject([canvas.offsetWidth,0]);
trap.Br = map.unproject([canvas.offsetWidth,canvas.offsetHeight]);
trap.Bl = map.unproject([0,canvas.offsetHeight]);
return(trap);
}
map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [-122.48610019683838, 37.82880236636284],
zoom: 17,
pitch: 60
});
map.on('load', function () {
// get the center and trapezoid of the zoomed in view
var center = map.getCenter();
var trapezoid = getViewTrapezoid();
// convert the view trapezoid to a path and add it to the view
var trapezoid_path = convertTrapezoidToPath(trapezoid);
addLayerToMap("viewTrapezoid",trapezoid_path,'#888',4);
// now rotate the bearing by 30 degrees to get a second view trapezoid
map.setBearing(30);
setTimeout(function() {
var trapezoid2 = getViewTrapezoid();
var trapezoid2_path = convertTrapezoidToPath(trapezoid2);
addLayerToMap("viewTrapezoid2",trapezoid2_path,'#f0f',2);
// return to a "top down" view and zoom out to show the trapezoids
map.setBearing(0);
map.setZoom(13.5);
setTimeout(function() {
map.flyTo({ pitch: 0 });
// rotate the original view trapezoid by 30 degrees and add it to the map
var newpath = pointsRotate(trapezoid_path,center.lng,center.lat,30);
addLayerToMap("rotatedTrapezoid",newpath,'#f00',2);
}, 500);
}, 500);
});
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css' rel='stylesheet' />
<div id='map'></div>
Ok, so it turns out that because of longitude getting thinner as you approach the poles, you first need to convert all degree co-ordinates (lat and long) to Mercator co-ordinates that take into account the non-spherical nature of the earth. I've added two functions here that convert from lat lon to mercator and vice versa and put them into the code and the result is a trapezoid that is directly on top of the one provided using the setBearing() method. Problem solved!
mapboxgl.accessToken = 'pk.eyJ1IjoiZm1hY2RlZSIsImEiOiJjajJlNWMxenowNXU2MzNudmkzMndwaGI3In0.ALOYWlvpYXnlcH6sCR9MJg';
var map;
function addLayerToMap(name, points, color, width) {
map.addLayer({
"id": name,
"type": "line",
"source": {
"type": "geojson",
"data": {
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": points
}
}
},
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": color,
"line-width": width
}
});
}
function pointsRotate(points, cx, cy, angle){
var radians = (Math.PI / 180) * angle;
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var newpoints = [];
function rotate(x, y) {
nx = (cos * (x - cx)) + (sin * (y - cy)) + cx,
ny = (cos * (y - cy)) + (-sin * (x - cx)) + cy;
return [nx, ny];
}
for(var i=0;i<points.length;i++) {
newpoints[i] = rotate(points[i][0],points[i][1]);
}
return(newpoints);
}
function convertTrapezoidToPath(trap) {
return([
[trap.Tl.lng, trap.Tl.lat], [trap.Tr.lng, trap.Tr.lat],
[trap.Br.lng, trap.Br.lat], [trap.Bl.lng, trap.Bl.lat],
[trap.Tl.lng, trap.Tl.lat] ]);
}
function getViewTrapezoid() {
var canvas = map.getCanvas();
var trap = {};
trap.Tl = map.unproject([0,0]);
trap.Tr = map.unproject([canvas.offsetWidth,0]);
trap.Br = map.unproject([canvas.offsetWidth,canvas.offsetHeight]);
trap.Bl = map.unproject([0,canvas.offsetHeight]);
return(trap);
}
function Mercator2ll(mercX, mercY) {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var lon = mercX / shift * 180.0;
var lat = mercY / shift * 180.0;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
return [ lon, lat ];
}
function ll2Mercator(lon, lat) {
var rMajor = 6378137; //Equatorial Radius, WGS84
var shift = Math.PI * rMajor;
var x = lon * shift / 180;
var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
y = y * shift / 180;
return [ x, y ];
}
function convertDegrees2Meters(points) {
var newpoints = [];
for(var i=0;i<points.length;i++) {
newpoints[i] = ll2Mercator( points[i][0], points[i][1] );
}
return newpoints;
}
function convertMeters2Degrees(points) {
var newpoints = [];
for(var i=0;i<points.length;i++) {
newpoints[i] = Mercator2ll( points[i][0], points[i][1] );;
}
return newpoints;
}
map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [-122.48610019683838, 37.82880236636284],
zoom: 17,
pitch: 60
});
map.on('load', function () {
// get the center and trapezoid of the zoomed in view
var center = map.getCenter();
var trapezoid = getViewTrapezoid();
var center_meters = ll2Mercator(center.lng,center.lat);
// convert the view trapezoid to a path and add it to the view
var trapezoid_path = convertTrapezoidToPath(trapezoid);
addLayerToMap("viewTrapezoid",trapezoid_path,'#888',4);
// now rotate the bearing by 30 degrees to get a second view trapezoid
map.setBearing(30);
setTimeout(function() {
var trapezoid2 = getViewTrapezoid();
var trapezoid2_path = convertTrapezoidToPath(trapezoid2);
addLayerToMap("viewTrapezoid2",trapezoid2_path,'#f0f',2);
// return to a "top down" view and zoom out to show the trapezoids
map.setBearing(0);
map.setZoom(13.5);
setTimeout(function() {
map.flyTo({ pitch: 0 });
// rotate the original view trapezoid by 30 degrees and add it to the map
var tpath_meters = convertDegrees2Meters(trapezoid_path);
var newpath_meters = pointsRotate(tpath_meters,center_meters[0],center_meters[1],30);
var newpath = convertMeters2Degrees(newpath_meters);
addLayerToMap("rotatedTrapezoid",newpath,'#f00',2);
}, 500);
}, 500);
});
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css' rel='stylesheet' />
<div id='map'></div>
Note: The problem is not specific to Leaflet, but GIS in general.
I'm trying to draw an arc on a map. I have a function to generate the polygon points and it works on a canvas for example, but not on Lng,Lat map.
The problem is that I cannot figure out how to convert the inner/outer radius from Meters to degrees (as in lng/lat), what I tried so far looks more elliptic than circular.
How to accurately convert meters to longitude or latitude at any point on earth (except the poles)?
Here is what I tried (works) on canvas.
$(document).ready(function() {
var d_canvas = document.getElementById('canvas');
var c2 = d_canvas.getContext('2d');
c2.fillStyle = '#f00';
c2.beginPath();
var fromDeg = 0;
var toDeg = 90;
var fromRad = getAngle(fromDeg);
var toRad = getAngle(toDeg);
var segments = 100;
var step = getAngle(toDeg-fromDeg)/segments;
var x = 250;
var y = 250;
var outR = 250;
var inR = 230;
c2.moveTo(x+(Math.sin(fromRad)*inR),y-(Math.cos(fromRad)*inR));
//c2.moveTo(x,y);
for (var i = fromRad; i<=toRad; i=i+step){
c2.lineTo(x+(Math.sin(i)*inR),y-(Math.cos(i)*inR));
}
//c2.closePath();
for (var i = toRad; i>=fromRad; i=i-step){
c2.lineTo(x+(Math.sin(i)*outR),y-(Math.cos(i)*outR));
}
c2.lineTo(x+(Math.sin(fromRad)*inR),y-(Math.cos(fromRad)*inR));
//c2.closePath();
c2.stroke();
});
function getAngle(deg){
var val = 2*(deg/360);
return Math.PI*val;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="500" height="500"></canvas>
And here is what I tried ( dosn't work well ) on Leaflet map.
var osmUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png',
osmAttrib = '© OpenStreetMap contributors',
osm = L.tileLayer(osmUrl, {
maxZoom: 18,
attribution: osmAttrib
});
// initialize the map on the "map" div with a given center and zoom
var map = L.map('map').setView([59.56667, 150.80000], 12).addLayer(osm);
// Script for adding marker on map click
L.polygon(getPolygon()).addTo(map);
function getPolygon() {
var fromDeg = 0;
var toDeg = 90;
var fromRad = getAngle(fromDeg);
var toRad = getAngle(toDeg);
var segments = 100;
var step = getAngle(toDeg - fromDeg) / segments;
var y = 150.84229;
var x = 59.55416;
var outR = 0.05; // <------ should be dynamic?
var inR = 0.025; // <------ this also?
var polygon = [];
polygon.push([x + (Math.sin(fromRad) * inR), y + (Math.cos(fromRad) * inR)]);
for (var i = fromRad; i <= toRad; i = i + step) {
polygon.push([x + (Math.sin(i) * inR), y + (Math.cos(i) * inR)]);
}
//c2.closePath();
for (var i = toRad; i >= fromRad; i = i - step) {
polygon.push([x + (Math.sin(i) * outR), y + (Math.cos(i) * outR)]);
}
polygon.push([x + (Math.sin(fromRad) * inR), y + (Math.cos(fromRad) * inR)]);
return polygon;
}
function getAngle(deg) {
var val = 2 * (deg / 360);
return Math.PI * val;
}
#map {
height: 500px;
width: 80%;
}
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
<link href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" rel="stylesheet" />
<script src="http://unpkg.com/leaflet-arc/bin/leaflet-arc.min.js"></script>
<div id="map"></div>
So your original question is
How to accurately convert meters to longitude or latitude at any point on earth (except the poles)?
But my brain reads that as
Given a [lat, lng] point and a distance d in meters, how to calculate a second [lat2, lng2] point which is d meters away from the first point?
Which, if you know some GIS jargon, is the same as asking
How do I solve the direct geodesic problem?
The answer involves mathematical concepts such as ellipsoids and great circles.
But given that you're working with Javascript and Leaflet, I'll just jump to practical implementations.
If you need a super-accurate answer, you want to have a look at the JS implementation of GeographicLib, and its methods to solve the direct geodesic problem.
If you don't really care about accuracy (and specially do not care about accuracy at the poles), you want to have a look at cheap-ruler, and specifically its destination(p, dist, bearing) method.
There are more solutions, like using a equidistant map projection centered on the point, or some other implementations of the geodesic problems, or some turf.js trickery, or creating the geometries outside of JS with similar methods, or whatever.
This problem has been solved already, so I advise to use any of the existing solutions.
This solved the problem
var osmUrl = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png',
osmAttrib = '© OpenStreetMap contributors',
osm = L.tileLayer(osmUrl, {
maxZoom: 18,
attribution: osmAttrib
});
// initialize the map on the "map" div with a given center and zoom
var map = L.map('map').setView([59.56667, 150.80000], 12).addLayer(osm);
// Script for adding marker on map click
L.polygon(getPolygon()).addTo(map);
function getPolygon() {
var fromDeg = 0;
var toDeg = 120;
var lat = 59.56667;
var lon = 150.80000;
var outR = 200;
var inR = 180;
var polygon = [];
for (var i = fromDeg; i <= toDeg; i++) {
polygon.push(getPoint(lat, lon, inR, i));
}
for (var i = toDeg; i >= fromDeg; i--) {
polygon.push(getPoint(lat, lon, outR, i));
}
polygon.push(getPoint(lat, lon, inR, fromDeg));
return polygon;
}
/*************************
* The solution
*************************/
function getPoint(lat, lon, r, deg) {
lat2 = (r / 111230) * Math.cos(deg2rad(deg));
lat2 += lat;
lon2 = (r / 111230) * Math.sin(deg2rad(deg));
lon2 = lon2 * (1 / Math.cos(deg2rad(lat2)));
lon2 += lon;
return [lat2, lon2];
}
function deg2rad(deg) {
return deg * (Math.PI / 180);
}
#map {
height: 500px;
width: 80%;
}
<link href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" rel="stylesheet"/>
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
<div id="map"></div>
I am trying to check the points positions and detect whether they are in a line, then output an object containing the possible lines. Questions:
Is this the best way to do it? efficient having four loops?
I am also getting duplicates matching points within the double loop, what's the best way to remove those?
If I wanted to detect shapes e.g. square (90 degrees angles), equilateral triangle (60 degrees angles) etc, how would I extend it?
if I wanted to do advanced detection of patterns in the point data e.g. one point at 90 degrees is 1km, one point at 100 degrees is 1.5km, one point at 110km is 2km etc. The match would be: every 5 degrees the distance increases by +50km. How could I enable that?
Here is a js fiddle of where I got to:
http://jsfiddle.net/kmturley/RAQXf/1/
We know the long and lat co-ordinates of Points 1 - 5. And we want to calculate the red lines between them.
Starting point data:
var points = [
{
name: 'Point 1',
lat: 51.509440,
long: -0.126985
},
{
name: 'Point 2',
lat: 51.509453,
long: -0.126180
},
{
name: 'Point 3',
lat: 51.510076,
long: -0.124804
},
{
name: 'Point 4',
lat: 51.510327,
long: -0.124133
},
{
name: 'Point 5',
lat: 51.509440,
long: -0.124175
}
];
Here are the functions i'm using:
var utils = {
distHaversine: function (lon1, lat1, lon2, lat2) { // calculate distance between two points
var R = 6371; // earth's mean radius in km
var dLat = this.toRad(lat2 - lat1);
var dLon = this.toRad(lon2 - lon1);
lat1 = this.toRad(lat1),
lat2 = this.toRad(lat2);
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c;
return d;
},
bearing: function (lon1, lat1, lon2, lat2) { // calculate bearing between two points
lat1 = this.toRad(lat1);
lat2 = this.toRad(lat2);
var dLon = this.toRad(lon2 - lon1);
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);
return this.toBrng(Math.atan2(y, x));
},
toRad: function (val) { // convert degrees to radians
return val * Math.PI / 180;
},
toDeg: function (val) { // convert radians to degrees (signed)
return val * 180 / Math.PI;
},
toBrng: function (val) { // convert radians to degrees (as bearing: 0...360)
return (this.toDeg(val) + 360) % 360;
}
};
And this is as far as I've got:
function calculate(items) {
var i = 0,
j = 0,
accuracy = 2,
bearings = {};
// loop through the points and check the distance and bearing of each one
for (i = 0; i < items.length; i += 1) {
for (j = 0; j < items.length; j += 1) {
if (i !== j) {
var bearing = utils.bearing(items[i].long, items[i].lat, items[j].long, items[j].lat);
var distance = utils.distHaversine(items[i].long, items[i].lat, items[j].long, items[j].lat);
var key = Math.round(bearing / accuracy) * accuracy;
// push both points into the bearing array for the same line
if (!bearings[key]) { bearings[key] = {}; }
bearings[key][i] = true;
bearings[key][j] = true;
console.log(Math.round(distance * 1000) + 'm', Math.round(bearing) + '°', items[i].name + ' > ' + items[j].name);
}
}
}
return bearings;
}
function lines(bearings, items) {
var item = {},
key = '',
lines = [];
// loop though the bearings and create lines
for (item in bearings) {
if (utils.size(bearings[item]) > 2) {
var line = { name: 'Line ' + item + '°', points: [] };
for (key in bearings[item]) {
line.points.push(items[parseInt(key)]);
}
lines.push(line);
}
}
return lines;
}
var bearings = calculate(points);
var lines = lines(bearings, points);
console.log('--------');
console.log(lines);
Expected output:
var lines = [
{
name: 'Line 1',
points: [
{
name: 'Point 1',
lat: 51.509440,
long: -0.126985
},
{
name: 'Point 2',
lat: 51.509453,
long: -0.126180
},
{
name: 'Point 5',
lat: 51.509440,
long: -0.124175
}
]
},
{
name: 'Line 2',
points: [
{
name: 'Point 2',
lat: 51.509453,
long: -0.126180
},
{
name: 'Point 3',
lat: 51.510076,
long: -0.124804
},
{
name: 'Point 4',
lat: 51.510327,
long: -0.124133
}
]
}
];
Here is a js fiddle of where I got to:
http://jsfiddle.net/kmturley/RAQXf/1/
I prefer to answer this in a language independent way, as it makes the answer more useful to programmers encountering the same problem use a different language.
In the absence of any other relationship between the points (e.g. knowing the streets they're on), you must start by considering all line segments between pairs of points. There are Binomial[n, 2] segments for n points, so it would be good if you could add heuristics to avoid considering some of these segments.
One we have those line segments, we can associate each line segment with a particular vector L(S) on a plane (let's call this the L plane). Two line segments S1 and S2 will be collinear if and only if L(S1) == L(S2).
L(S) is defined as the vector from some fixed origin point O to the nearest point on the (infinite) line extending from S. If two segments are on the same line, then they'll share the same nearest point to O, and if not, they won't. So now you can use a spatial tree such as a quadtree on the L plane to see which segments are collinear.
You can compute the vector L(S) using the well-documented method of finding the nearest point on a line to another point, but here's a quick reminder.
Dirty details: Things go bad when your origin is collinear with any segment. You'll have to handle that case. I think the best way to deal with this case is to put those segments aside, move the origin, and then re-apply the algorithm to just those segments.
Also, the tolerance that you'll want to use for coincidence scales with the distance from O.
So i've managed to solve the problem by using this script:
http://www.movable-type.co.uk/scripts/latlon.js
And then the following code:
var p1 = new LatLon(item1.lat, items1.long);
var p2 = new LatLon(item2.lat, items2.long);
var p3 = new LatLon(item3.lat, items3.long);
var distance = Math.abs(Math.asin(Math.sin(p1.distanceTo(p3) / R) * Math.sin(p1.bearingTo(p3).toRad() - p1.bearingTo(p2).toRad())) * R);
I had one major issue: the bearing was in degrees, but needed to be in Radians!
p1.bearingTo(p3).toRad() - p1.bearingTo(p2).toRad()
You can see a working version here (using multiple points to find lines between them):
http://jsfiddle.net/kmturley/Cq2DV/1/
I have tried lots but could not figure out the problem. I want to draw a polygon around specific lat,lng. The polygon will consists of 13 coordinates in specific radius.
Person inter the address and radius in text box.
Geo code get lat,lng of that address
Center the map to there.
Draw the polygon around that center point with radius
The polygon should consists of 13 coordinates
Code
function showAddress(address, miles) {
var geocoder = new google.maps.Geocoder();
geocoder.geocode({
address : address
}, function(results, status) {
if(status == google.maps.GeocoderStatus.OK) {
//searchLocationsNear(results[0].geometry.location);
var cordinate = results[0].geometry.location;
//alert(cordinate);
var mapOptions = {
center : cordinate,
zoom : 8,
mapTypeId : google.maps.MapTypeId.ROADMAP,
overviewMapControl : true,
overviewMapControlOptions : {
opened : true,
position : google.maps.ControlPosition.BOTTOM_LEFT
}
};
//
//var address = document.getElementById("address").value;
var radius = 1;
var latitude = 23.1793013;
var longitude = 75.78490970000007;
//Degrees to radians
var d2r = Math.PI / 180;
// Radians to degrees
var r2d = 180 / Math.PI;
// Earth radius is 3,963 miles
var cLat = (radius / 3963) * r2d;
var cLng = cLat / Math.cos(latitude * d2r);
//Store points in array
var points = [];
alert("declare array");
var bounds = new google.maps.LatLngBounds();
// Calculate the points
// Work around 360 points on circle
for(var i = 0; i < 13; i++) {
var theta = Math.PI * (i / 180);
// Calculate next X point
circleY = longitude + (cLng * Math.cos(theta));
//console.log("CircleY:"+circleY);
// Calculate next Y point
circleX = latitude + (cLat * Math.sin(theta));
//console.log("circleX:"+circleX);
// Add point to array
var aPoint = new google.maps.LatLng(circleX, circleY);
points.push(aPoint);
bounds.extend(aPoint);
}
points.push(points[0]);
//console.log(points);
//to complete circle
var colors = ["#CD0000", "#2E6444", "#003F87"];
var Polyline_Path = new google.maps.Polyline({
path : points,
strokeColor : colors[0],
// color of the outline of the polygon
strokeOpacity : 1,
// between 0.0 and 1.0
strokeWeight : 1,
// The stroke width in pixels
fillColor : colors[1],
fillOpacity : 0
});
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
Polyline_Path.setMap(map);
} else {
alert(address + ' not found');
}
});
}
Replace i<13;i++ by
i<360;i+=360/13
this will work
thank
edit: the last point isn't needed since gmap will close it automagically
I believe that cLng should be changed to:
var cLng = cLat * Math.cos(latitude * d2r);
(to get a perfect circle, that is)