Trying to create a static map with a circle like this
In the path parameter I do not understand how to obtain the enc part. This appears to be some encoded path that includes the lat/long and size of the circle.
https://maps.googleapis.com/maps/api/staticmap?
center=51.44208,5.47308&
zoom=14&
size=693x648&
path=color:blue|fillcolor:0x00d2c196|weight:1|
enc%3Aad_yHofn%60%40JyFh%40sF%60AcFxAmElBqD~BoCnCiBtC_AzCUzCTvC~%40lChB~BnCnBpDxAlE%60AbFf%40rFLxFMxFg%40rFaAbFyAlEoBpD_CnCmChBwC~%40%7BCT%7BCUuC_AoCiB_CoCmBqDyAmEaAcFi%40sFKyF%3F%3F
Link to Google's documentation
**EDIT: Found these:
Drawing a circle Google Static Maps
Encoded Polyline Algorithm Format
Here's what I came up with:
function funcStaticMapWithCircle($lat, $lng) {
//$lat & $lng are center of circle
$mapCentLat = $lat + 0.002;
$mapCentLng = $lng - 0.011;
$mapW = 600;
$mapH = 600;
$zoom = 14;
$circRadius = 0.75; //Radius in km
$circRadiusThick = 1;
$circFill = '00BFFF';
$circFillOpacity = '60';
$circBorder = 'red';
$encString = GMapCircle($lat,$lng,$circRadius); //Encoded polyline string
$src = 'https://maps.googleapis.com/maps/api/staticmap?';
$src .= 'center=' .$mapCentLat. ',' .$mapCentLng. '&';
$src .= 'zoom=' .$zoom. '&';
$src .= 'size=' .$mapW. 'x' .$mapH.'&';
$src .= 'maptype=roadmap&';
$src .= 'style=feature:water|element:geometry.fill|color:0x9bd3ff&';
$src .= 'path=';
$src .= 'color:0x' .$circBorder. '00|';
$src .= 'fillcolor:0x' .$circFill.$circFillOpacity. '|';
$src .= 'weight:' .$circRadiusThick. '|';
$src .= 'enc:' .$encString;
return $src;
}
GMapCircle function is from: Drawing a circle Google Static Maps
I have rewritten it in javascript (as an Angular Service).
import { Injectable } from "#angular/core";
import { HttpClient } from "#angular/common/http";
import { environment } from "environments/environment";
#Injectable({
providedIn: "root",
})
export class StaticMapService {
constructor(private httpClient: HttpClient) {}
getStaticMapBase64(
lat: number,
lng: number,
radius: string,
zoom: number
): Promise<string> {
return new Promise((resolve) => {
this.httpClient
.get(`https://maps.googleapis.com/maps/api/staticmap`, {
params: {
key: environment.googleMapsApiKey,
center: `${lat},${lng}`,
size: `640x480`,
zoom: `${zoom}`,
path: `fillcolor:0xff00002D|color:0xf96332ff|enc:${this.drawCirclePath(
lat,
lng,
radius
)}`,
},
responseType: "blob",
})
.toPromise()
.then((imgBlob) => {
const reader = new FileReader();
reader.readAsDataURL(imgBlob);
reader.onloadend = function () {
resolve(reader.result.toString());
};
});
});
}
private drawCirclePath(lat, lng, radius, detail = 8) {
let R = 6371;
let pi = Math.PI;
lat = (lat * pi) / 180;
lng = (lng * pi) / 180;
let d = radius / R;
let points: any = [];
let i = 0;
for (i = 0; i <= 360; i += detail) {
let brng = (i * pi) / 180;
let plat = Math.asin(
Math.sin(lat) * Math.cos(d) +
Math.cos(lat) * Math.sin(d) * Math.cos(brng)
);
let plng =
((lng +
Math.atan2(
Math.sin(brng) * Math.sin(d) * Math.cos(lat),
Math.cos(d) - Math.sin(lat) * Math.sin(plat)
)) *
180) /
pi;
plat = (plat * 180) / pi;
let currentPoints: any = [plat, plng];
points.push(currentPoints);
}
return this.createEncodings(points);
}
private createEncodings(coords) {
var i = 0;
var plat = 0;
var plng = 0;
var encoded_points = "";
for (i = 0; i < coords.length; ++i) {
var lat = coords[i][0];
var lng = coords[i][1];
encoded_points += this.encodePoint(plat, plng, lat, lng);
plat = lat;
plng = lng;
}
return encoded_points;
}
private encodePoint(plat, plng, lat, lng) {
var dlng = 0;
var dlat = 0;
var late5 = Math.round(lat * 1e5);
var plate5 = Math.round(plat * 1e5);
var lnge5 = Math.round(lng * 1e5);
var plnge5 = Math.round(plng * 1e5);
dlng = lnge5 - plnge5;
dlat = late5 - plate5;
return this.encodeSignedNumber(dlat) + this.encodeSignedNumber(dlng);
}
private encodeSignedNumber(num) {
var sgn_num = num << 1;
if (num < 0) {
sgn_num = ~sgn_num;
}
return this.encodeNumber(sgn_num);
}
private encodeNumber(num) {
var encodeString = "";
while (num >= 0x20) {
encodeString += String.fromCharCode((0x20 | (num & 0x1f)) + 63);
num >>= 5;
}
encodeString += String.fromCharCode(num + 63);
return encodeString;
}
}
Related
I am trying to create a draggable marker that is confined to a polyline. I have read this post (Confine dragging of Google Maps V3 Marker to Polyline), but I do not want to create the points that the marker can move along. Are there other ways to do this without having to create a points array for the marker? If anyone can point me in the right direction, it is much appreciated.
From what I understand, you have to load the polyline points into an array. It seems that there is no way around this. I am not sure how the the directions api snaps to roads, but I am assuming that it is based on this concept (loading points into an array).
I have found an older maps v2 library that updates the marker based on mouse movement events, which loads the line data on zoom end. I have updated the code to work with api v3 and replaced the mouse events with drag events.
To use this library, initialize like this:
var snapToRoute = new SnapToRoute(map_instance, initial_marker, polyline);
The library can be found here: SnapToRoute
** Update ** example fiddle
Here is my modified version:
function SnapToRoute(map, marker, polyline) {
this.routePixels_ = [];
this.normalProj_ = map.getProjection();
this.map_ = map;
this.marker_ = marker;
this.polyline_ = polyline;
this.init_();
}
SnapToRoute.prototype.init_ = function () {
this.loadLineData_();
this.loadMapListener_();
};
SnapToRoute.prototype.updateTargets = function (marker, polyline) {
this.marker_ = marker || this.marker_;
this.polyline_ = polyline || this.polyline_;
this.loadLineData_();
};
SnapToRoute.prototype.loadMapListener_ = function () {
var me = this;
google.maps.event.addListener(me.marker_, "dragend", function (evt) {
me.updateMarkerLocation_(evt.latLng);
});
google.maps.event.addListener(me.marker_, "drag", function (evt) {
me.updateMarkerLocation_(evt.latLng);
});
google.maps.event.addListener(me.map_, "zoomend", function (evt) {
me.loadLineData_();
});
};
SnapToRoute.prototype.loadLineData_ = function () {
var zoom = this.map_.getZoom();
this.routePixels_ = [];
var path = this.polyline_.getPath();
for (var i = 0; i < path.getLength() ; i++) {
var Px = this.normalProj_.fromLatLngToPoint(path.getAt(i));
this.routePixels_.push(Px);
}
};
SnapToRoute.prototype.updateMarkerLocation_ = function (mouseLatLng) {
var markerLatLng = this.getClosestLatLng(mouseLatLng);
this.marker_.setPosition(markerLatLng);
};
SnapToRoute.prototype.getClosestLatLng = function (latlng) {
var r = this.distanceToLines_(latlng);
return this.normalProj_.fromPointToLatLng(new google.maps.Point(r.x, r.y));
};
SnapToRoute.prototype.getDistAlongRoute = function (latlng) {
if (typeof (opt_latlng) === 'undefined') {
latlng = this.marker_.getLatLng();
}
var r = this.distanceToLines_(latlng);
return this.getDistToLine_(r.i, r.to);
};
SnapToRoute.prototype.distanceToLines_ = function (mouseLatLng) {
var zoom = this.map_.getZoom();
var mousePx = this.normalProj_.fromLatLngToPoint(mouseLatLng);
var routePixels_ = this.routePixels_;
return this.getClosestPointOnLines_(mousePx, routePixels_);
};
SnapToRoute.prototype.getDistToLine_ = function (line, to) {
var routeOverlay = this.polyline_;
var d = 0;
for (var n = 1; n < line; n++) {
d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(n - 1), routeOverlay.getAt(n));
}
d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(line - 1), routeOverlay.getAt(line)) * to;
return d;
};
SnapToRoute.prototype.getClosestPointOnLines_ = function (pXy, aXys) {
var minDist;
var to;
var from;
var x;
var y;
var i;
var dist;
if (aXys.length > 1) {
for (var n = 1; n < aXys.length ; n++) {
if (aXys[n].x !== aXys[n - 1].x) {
var a = (aXys[n].y - aXys[n - 1].y) / (aXys[n].x - aXys[n - 1].x);
var b = aXys[n].y - a * aXys[n].x;
dist = Math.abs(a * pXy.x + b - pXy.y) / Math.sqrt(a * a + 1);
} else {
dist = Math.abs(pXy.x - aXys[n].x);
}
var rl2 = Math.pow(aXys[n].y - aXys[n - 1].y, 2) + Math.pow(aXys[n].x - aXys[n - 1].x, 2);
var ln2 = Math.pow(aXys[n].y - pXy.y, 2) + Math.pow(aXys[n].x - pXy.x, 2);
var lnm12 = Math.pow(aXys[n - 1].y - pXy.y, 2) + Math.pow(aXys[n - 1].x - pXy.x, 2);
var dist2 = Math.pow(dist, 2);
var calcrl2 = ln2 - dist2 + lnm12 - dist2;
if (calcrl2 > rl2) {
dist = Math.sqrt(Math.min(ln2, lnm12));
}
if ((minDist == null) || (minDist > dist)) {
to = Math.sqrt(lnm12 - dist2) / Math.sqrt(rl2);
from = Math.sqrt(ln2 - dist2) / Math.sqrt(rl2);
minDist = dist;
i = n;
}
}
if (to > 1) {
to = 1;
}
if (from > 1) {
to = 0;
from = 1;
}
var dx = aXys[i - 1].x - aXys[i].x;
var dy = aXys[i - 1].y - aXys[i].y;
x = aXys[i - 1].x - (dx * to);
y = aXys[i - 1].y - (dy * to);
}
return { 'x': x, 'y': y, 'i': i, 'to': to, 'from': from };
};
example fiddle
code snippet:
var geocoder;
var directionsDisplay;
var directionsService = new google.maps.DirectionsService();
var map;
var polyline = new google.maps.Polyline({
path: [],
strokeColor: '#FF0000',
strokeWeight: 3
});
var marker;
function initialize() {
directionsDisplay = new google.maps.DirectionsRenderer();
map = new google.maps.Map(
document.getElementById("map_canvas"), {
center: new google.maps.LatLng(37.4419, -122.1419),
zoom: 13,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
calcRoute("New York, NY", "Baltimore, MD");
directionsDisplay.setMap(map);
}
google.maps.event.addDomListener(window, "load", initialize);
function calcRoute(start, end) {
var request = {
origin: start,
destination: end,
travelMode: google.maps.TravelMode.DRIVING
};
directionsService.route(request, function(response, status) {
if (status == google.maps.DirectionsStatus.OK) {
// directionsDisplay.setDirections(response);
renderRoute(response);
}
});
}
function renderRoute(response) {
var bounds = new google.maps.LatLngBounds();
var route = response.routes[0];
var summaryPanel = document.getElementById("directions_panel");
var detailsPanel = document.getElementById("direction_details");
var path = response.routes[0].overview_path;
var legs = response.routes[0].legs;
for (i = 0; i < legs.length; i++) {
if (i == 0) {
marker = new google.maps.Marker({
position: legs[i].start_location,
draggable: true,
map: map
});
}
var steps = legs[i].steps;
for (j = 0; j < steps.length; j++) {
var nextSegment = steps[j].path;
for (k = 0; k < nextSegment.length; k++) {
polyline.getPath().push(nextSegment[k]);
bounds.extend(nextSegment[k]);
}
}
}
polyline.setMap(map);
map.fitBounds(bounds);
var snapToRoute = new SnapToRoute(map, marker, polyline);
}
function SnapToRoute(map, marker, polyline) {
this.routePixels_ = [];
this.normalProj_ = map.getProjection();
this.map_ = map;
this.marker_ = marker;
this.editable_ = Boolean(false);
this.polyline_ = polyline;
this.init_();
}
SnapToRoute.prototype.init_ = function() {
this.loadLineData_();
this.loadMapListener_();
};
SnapToRoute.prototype.updateTargets = function(marker, polyline) {
this.marker_ = marker || this.marker_;
this.polyline_ = polyline || this.polyline_;
this.loadLineData_();
};
SnapToRoute.prototype.loadMapListener_ = function() {
var me = this;
google.maps.event.addListener(me.marker_, "dragend", function(evt) {
me.updateMarkerLocation_(evt.latLng);
});
google.maps.event.addListener(me.marker_, "drag", function(evt) {
me.updateMarkerLocation_(evt.latLng);
});
google.maps.event.addListener(me.map_, "zoomend", function(evt) {
me.loadLineData_();
});
};
SnapToRoute.prototype.loadLineData_ = function() {
var zoom = this.map_.getZoom();
this.routePixels_ = [];
var path = this.polyline_.getPath();
for (var i = 0; i < path.getLength(); i++) {
var Px = this.normalProj_.fromLatLngToPoint(path.getAt(i));
this.routePixels_.push(Px);
}
};
SnapToRoute.prototype.updateMarkerLocation_ = function(mouseLatLng) {
var markerLatLng = this.getClosestLatLng(mouseLatLng);
this.marker_.setPosition(markerLatLng);
};
SnapToRoute.prototype.getClosestLatLng = function(latlng) {
var r = this.distanceToLines_(latlng);
return this.normalProj_.fromPointToLatLng(new google.maps.Point(r.x, r.y));
};
SnapToRoute.prototype.getDistAlongRoute = function(latlng) {
if (typeof(opt_latlng) === 'undefined') {
latlng = this.marker_.getLatLng();
}
var r = this.distanceToLines_(latlng);
return this.getDistToLine_(r.i, r.to);
};
SnapToRoute.prototype.distanceToLines_ = function(mouseLatLng) {
var zoom = this.map_.getZoom();
var mousePx = this.normalProj_.fromLatLngToPoint(mouseLatLng);
var routePixels_ = this.routePixels_;
return this.getClosestPointOnLines_(mousePx, routePixels_);
};
SnapToRoute.prototype.getDistToLine_ = function(line, to) {
var routeOverlay = this.polyline_;
var d = 0;
for (var n = 1; n < line; n++) {
d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(n - 1), routeOverlay.getAt(n));
}
d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(line - 1), routeOverlay.getAt(line)) * to;
return d;
};
SnapToRoute.prototype.getClosestPointOnLines_ = function(pXy, aXys) {
var minDist;
var to;
var from;
var x;
var y;
var i;
var dist;
if (aXys.length > 1) {
for (var n = 1; n < aXys.length; n++) {
if (aXys[n].x !== aXys[n - 1].x) {
var a = (aXys[n].y - aXys[n - 1].y) / (aXys[n].x - aXys[n - 1].x);
var b = aXys[n].y - a * aXys[n].x;
dist = Math.abs(a * pXy.x + b - pXy.y) / Math.sqrt(a * a + 1);
} else {
dist = Math.abs(pXy.x - aXys[n].x);
}
var rl2 = Math.pow(aXys[n].y - aXys[n - 1].y, 2) + Math.pow(aXys[n].x - aXys[n - 1].x, 2);
var ln2 = Math.pow(aXys[n].y - pXy.y, 2) + Math.pow(aXys[n].x - pXy.x, 2);
var lnm12 = Math.pow(aXys[n - 1].y - pXy.y, 2) + Math.pow(aXys[n - 1].x - pXy.x, 2);
var dist2 = Math.pow(dist, 2);
var calcrl2 = ln2 - dist2 + lnm12 - dist2;
if (calcrl2 > rl2) {
dist = Math.sqrt(Math.min(ln2, lnm12));
}
if ((minDist == null) || (minDist > dist)) {
to = Math.sqrt(lnm12 - dist2) / Math.sqrt(rl2);
from = Math.sqrt(ln2 - dist2) / Math.sqrt(rl2);
minDist = dist;
i = n;
}
}
if (to > 1) {
to = 1;
}
if (from > 1) {
to = 0;
from = 1;
}
var dx = aXys[i - 1].x - aXys[i].x;
var dy = aXys[i - 1].y - aXys[i].y;
x = aXys[i - 1].x - (dx * to);
y = aXys[i - 1].y - (dy * to);
}
return {
'x': x,
'y': y,
'i': i,
'to': to,
'from': from
};
};
html,
body,
#map_canvas {
height: 100%;
width: 100%;
margin: 0px;
padding: 0px
}
<script src="https://maps.googleapis.com/maps/api/js"></script>
<div id="map_canvas" style="border: 2px solid #3872ac;"></div>
I'm trying to add the maths to control a panorama with the gyroscope and struggling. This is a mobile app built in AS3.
I've got the data coming through from the gyroscope (x and y), and I've got the current angle (pan and tilt). What I want to do is update the cameraController with the new angle based on the data from the gyro.
I've been attempting to convert the Javascript I found on https://github.com/fieldOfView/krpano_fovplugins/blob/master/gyro/source/gyro.source.js into Actionscript 3, and it kind of works - but not really.
EDIT
Thanks I tried those changes and I added camera roll back in because the euler maths needed it, it runs but there is something wrong with the Maths.
The panorama only seems to drift up and left, after a while of moving the phone the other way it drifts down, and then moves right.
Can you see anything important I'm missing from the Javascript?
import com.adobe.nativeExtensions.GyroscopeEvent;
import flash.events.Event;
import flash.geom.Orientation3D;
public class GyroscopeMaths
{
public function GyroscopeMaths()
{
super();
}
private var isTopAccessible:Boolean = false;
private var isDeviceAvailable:Boolean;
private var isEnabled:Boolean = false;
private var vElasticity:Number = 0;
private var isVRelative:Boolean = false;
private var isCamRoll:Boolean = false;
private var friction:Number = 0.5;
private var isTouching:Boolean = false;
private var validSample:Boolean = false;
private var firstSample:* = null;
private var hOffset:Number = 0;
private var vOffset:Number = 0;
private var hLookAt:Number = 0;
private var vLookAt:Number = 0;
private var camRoll:Number = 0;
private var vLookAtNow:Number = 0;
private var hLookAtNow:Number = 0;
private var hSpeed:Number = 0;
private var vSpeed:Number = 0;
private var vElasticSpeed:Number = 0;
private var camRollNow:Number;
private var pitch:Number;
private var yaw:Number;
private var altYaw:Number;
private var factor:Number;
private var degRad:Number = Math.PI / 180;
public function handleDeviceOrientation(x:Number, y:Number, z:Number):void {
// Process event.alpha, event.beta and event.gamma
var orientation:* = rotateEuler({
"yaw":y * degRad,
"pitch":x * degRad,
"roll": z * degRad
});
yaw = wrapAngle(orientation.yaw / degRad);
pitch = orientation.pitch / degRad;
altYaw = yaw, factor;
hLookAtNow = Pano.instance.pan;
vLookAtNow = Pano.instance.tilt;
hSpeed = hLookAtNow - hLookAt,
vSpeed = vLookAtNow - vLookAt;
// Ignore all sample until we get a sample that is different from the first sample
if (!validSample) {
if (firstSample == null) {
firstSample = orientation;
} else {
if (orientation.yaw != firstSample.yaw || orientation.pitch != firstSample.pitch || orientation.roll != firstSample.roll) {
firstSample = null;
validSample = true;
if (isVRelative) {
vOffset = -pitch;
}
}
}
return;
}
// Fix gimbal lock
if (Math.abs(pitch) > 70) {
altYaw = y;
var altYaw:Number = wrapAngle(altYaw);
if (Math.abs(altYaw - yaw) > 180) {
altYaw += (altYaw < yaw) ? 360 :-360;
}
var factor:Number = Math.min(1, (Math.abs(pitch) - 70) / 10);
yaw = yaw * (1 - factor) + altYaw * factor;
//camRoll *= (1 - factor);
}
// Track view change since last orientation event
// ie:user has manually panned, or krpano has altered lookat
hOffset += hSpeed;
vOffset += vSpeed;
// Clamp vOffset
if (Math.abs(pitch + vOffset) > 90) {
vOffset = (pitch + vOffset > 0) ? (90 - pitch) :(-90 - pitch)
}
hLookAt = wrapAngle(-yaw - 180 + hOffset);
vLookAt = Math.max(Math.min((pitch + vOffset), 90), -90);
// Dampen lookat
if (Math.abs(hLookAt - hLookAtNow) > 180) {
hLookAtNow += (hLookAt > hLookAtNow) ? 360 :-360;
}
hLookAt = (1 - friction) * hLookAt + friction * hLookAtNow;
vLookAt = (1 - friction) * vLookAt + friction * vLookAtNow;
if (Math.abs(camRoll - camRollNow) > 180) {
camRollNow += (camRoll > camRollNow) ? 360 :-360;
}
camRoll = (1 - friction) * camRoll + friction * camRollNow;
var wAh:Number = wrapAngle(hLookAt);
Pano.instance.panoGyroChange(wAh, vLookAt);
//krpano.view.camroll = wrapAngle(camRoll);
if (vOffset != 0 && vElasticity > 0) {
if (vSpeed == 0) {
if (vElasticity == 1) {
vOffset = 0;
vElasticSpeed = 0;
} else {
// vElasticSpeed = 1 - ((1 - vElasticSpeed) * krpano.control.touchfriction);
vOffset *= 1 - (Math.pow(vElasticity, 2) * vElasticSpeed); // use Math.pow to be able to use saner values
if (Math.abs(vOffset) < 0.1) {
vOffset = 0;
vElasticSpeed = 0;
}
}
} else {
vElasticSpeed = 0;
}
}
}
private function rotateEuler(euler:Object):Object {
// This function is based on http://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToMatrix/index.htm
// and http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToEuler/index.htm
trace(euler);
var heading:Number;
var bank:Number;
var attitude:Number;
var ch:Number = Math.cos(euler.yaw);
var sh:Number = Math.sin(euler.yaw);
var ca:Number = Math.cos(euler.pitch);
var sa:Number = Math.sin(euler.pitch);
var cb:Number = Math.cos(euler.roll);
var sb:Number = Math.sin(euler.roll);
var matrix:Array = [
sh * sb - ch * sa * cb, -ch * ca, ch * sa * sb + sh * cb,
ca * cb, -sa, -ca * sb,
sh * sa * cb + ch * sb, sh * ca, -sh * sa * sb + ch * cb
]; // Note:Includes 90 degree rotation around z axis
/* [m00 m01 m02] 0 1 2
* [m10 m11 m12] 3 4 5
* [m20 m21 m22] 6 7 8 */
if (matrix[3] > 0.9999) {
// Deal with singularity at north pole
heading = Math.atan2(matrix[2], matrix[8]);
attitude = Math.PI / 2;
bank = 0;
} else if (matrix[3] < -0.9999) {
// Deal with singularity at south pole
heading = Math.atan2(matrix[2], matrix[8]);
attitude = -Math.PI / 2;
bank = 0;
} else {
heading = Math.atan2(-matrix[6], matrix[0]);
bank = Math.atan2(-matrix[5], matrix[4]);
attitude = Math.asin(matrix[3]);
}
return {
yaw:heading,
pitch:attitude,
roll:bank
};
}
private function wrapAngle(value:Number):Number {
value = value % 360;
return (value <= 180) ? value :value - 360;
} // wrap a value between -180 and 180
//function stringToBoolean(value:Number):String
//{ return (String("yesontrue1").indexOf( String(value:Number) ) >= 0) };
}
Without the rest of your program, I won't be able to compile this, but I've corrected what syntactical errors I could find. That said, if you can program in either JS or As3, you should be able to follow the logic and write your own class (they're both EMCAScript). Continue pursuing that until you've arrived at a more solid problem than "it doesn't work" (which is generally a question no one wants to answer).
private var isTopAccessible:Boolean = false;
private var isDeviceAvailable:Boolean;
private var isEnabled:Boolean = false;
private var vElasticity:Number = 0;
private var isVRelative:Boolean = false;
private var isCamRoll:Boolean = false;
private var friction:Number = 0.5;
private var isTouching:Boolean = false;
private var validSample:Boolean = false;
private var firstSample:* = null;
private var hOffset:Number = 0;
private var vOffset:Number = 0;
private var hLookAt:Number = 0;
private var vLookAt:Number = 0;
private var camRoll:Number = 0;
private var vLookAtNow:Number = 0;
private var hLookAtNow:Number = 0;
private var hSpeed:Number = 0;
private var vSpeed:Number = 0;
private var vElasticSpeed:Number = 0;
private var camRollNow:Number;
private var pitch:Number;
private var yaw:Number;
private var altYaw:Number;
private var factor:Number;
private var degRad:Number = Math.PI / 180;
public function handleDeviceOrientation(x:Number, y:Number):void {
// Process event.alpha, event.beta and event.gamma
var orientation:* = rotateEuler({
"yaw":y * degRad,
"pitch":x * degRad,
"roll":0
});
yaw = wrapAngle(orientation.yaw / degRad);
pitch = orientation.pitch / degRad;
altYaw = yaw, factor;
hLookAtNow = Pano.instance.pan;
vLookAtNow = Pano.instance.tilt;
hSpeed = hLookAtNow - hLookAt,
vSpeed = vLookAtNow - vLookAt;
// Ignore all sample until we get a sample that is different from the first sample
if (!validSample) {
if (firstSample == null) {
firstSample = orientation;
} else {
if (orientation.yaw != firstSample.yaw || orientation.pitch != firstSample.pitch || orientation.roll != firstSample.roll) {
firstSample = null;
validSample = true;
if (isVRelative) {
vOffset = -pitch;
}
}
}
return;
}
// Fix gimbal lock
if (Math.abs(pitch) > 70) {
altYaw = y;
/*switch(deviceOrientation) {
case 0:
if ( pitch>0 )
altYaw += 180;
break;
case 90:
altYaw += 90;
break;
case -90:
altYaw += -90;
break;
case 180:
if ( pitch<0 )
altYaw += 180;
break;
}*/
var altYaw:Number = wrapAngle(altYaw);
if (Math.abs(altYaw - yaw) > 180) {
altYaw += (altYaw < yaw) ? 360 :-360;
}
var factor:Number = Math.min(1, (Math.abs(pitch) - 70) / 10);
yaw = yaw * (1 - factor) + altYaw * factor;
//camRoll *= (1 - factor);
}
// Track view change since last orientation event
// ie:user has manually panned, or krpano has altered lookat
hOffset += hSpeed;
vOffset += vSpeed;
// Clamp vOffset
if (Math.abs(pitch + vOffset) > 90) {
vOffset = (pitch + vOffset > 0) ? (90 - pitch) :(-90 - pitch)
}
hLookAt = wrapAngle(-yaw - 180 + hOffset);
vLookAt = Math.max(Math.min((pitch + vOffset), 90), -90);
// Dampen lookat
if (Math.abs(hLookAt - hLookAtNow) > 180) {
hLookAtNow += (hLookAt > hLookAtNow) ? 360 :-360;
}
hLookAt = (1 - friction) * hLookAt + friction * hLookAtNow;
vLookAt = (1 - friction) * vLookAt + friction * vLookAtNow;
if (Math.abs(camRoll - camRollNow) > 180) {
camRollNow += (camRoll > camRollNow) ? 360 :-360;
}
camRoll = (1 - friction) * camRoll + friction * camRollNow;
var wAh:Number = wrapAngle(hLookAt);
Pano.instance.panoGyroChange(wAh, vLookAt);
//krpano.view.camroll = wrapAngle(camRoll);
if (vOffset != 0 && vElasticity > 0) {
if (vSpeed == 0) {
if (vElasticity == 1) {
vOffset = 0;
vElasticSpeed = 0;
} else {
// vElasticSpeed = 1 - ((1 - vElasticSpeed) * krpano.control.touchfriction);
vOffset *= 1 - (Math.pow(vElasticity, 2) * vElasticSpeed); // use Math.pow to be able to use saner values
if (Math.abs(vOffset) < 0.1) {
vOffset = 0;
vElasticSpeed = 0;
}
}
} else {
vElasticSpeed = 0;
}
}
}
private function rotateEuler(euler:Object):Object {
// This function is based on http://www.euclideanspace.com/maths/geometry/rotations/conversions/eulerToMatrix/index.htm
// and http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToEuler/index.htm
trace(euler);
var heading:Number;
var bank:Number;
var attitude:Number;
var ch:Number = Math.cos(euler.yaw);
var sh:Number = Math.sin(euler.yaw);
var ca:Number = Math.cos(euler.pitch);
var sa:Number = Math.sin(euler.pitch);
var cb:Number = Math.cos(euler.roll);
var sb:Number = Math.sin(euler.roll);
var matrix:Array = [
sh * sb - ch * sa * cb, -ch * ca, ch * sa * sb + sh * cb,
ca * cb, -sa, -ca * sb,
sh * sa * cb + ch * sb, sh * ca, -sh * sa * sb + ch * cb
]; // Note:Includes 90 degree rotation around z axis
/* [m00 m01 m02] 0 1 2
* [m10 m11 m12] 3 4 5
* [m20 m21 m22] 6 7 8 */
if (matrix[3] > 0.9999) {
// Deal with singularity at north pole
heading = Math.atan2(matrix[2], matrix[8]);
attitude = Math.PI / 2;
bank = 0;
} else if (matrix[3] < -0.9999) {
// Deal with singularity at south pole
heading = Math.atan2(matrix[2], matrix[8]);
attitude = -Math.PI / 2;
bank = 0;
} else {
heading = Math.atan2(-matrix[6], matrix[0]);
bank = Math.atan2(-matrix[5], matrix[4]);
attitude = Math.asin(matrix[3]);
}
return {
yaw:heading,
pitch:attitude,
roll:bank
};
}
private function wrapAngle(value:Number):Number {
value = value % 360;
return (value <= 180) ? value :value - 360;
} // wrap a value between -180 and 180
//function stringToBoolean(value:Number):String
//{ return (String("yesontrue1").indexOf( String(value:Number) ) >= 0) };
Say I have a GPS coord say A (in longitude-latitude decimal form then I want to create a zone of 30 meters radius (a circle) so that I can trap any device(with GPS coord) which enters that zone, how to do it ? Thanks
Take a look at this javascript example, but it's easy to implement on any other language.
Main code:
var earthRadiusKm = 3437.74677 * 1.1508 * 1.6093470878864446;
function GeoArea(centerLat, centerLng, distanceKm, northPoint, southPoint, eastPoint, westPoint) {
this.northPoint = northPoint;
this.southPoint = southPoint;
this.eastPoint = eastPoint;
this.westPoint = westPoint;
this.inArea = function (lat, lng) {
var inBox = southPoint.lat <= lat && lat <= northPoint.lat && westPoint.lng <= lng && lng <= eastPoint.lng;
if (inBox) {
var distanceFromCenterKm = calcDistance(centerLat, centerLng, lat, lng);
return distanceFromCenterKm <= distanceKm;
} else {
return false;
}
}
}
function GeoPoint(lat, lng) {
this.lat = lat;
this.lng = lng;
}
function toDegrees(radians) {
return radians / (Math.PI / 180);
}
function toRadians(degrees) {
return Math.PI / 180 * degrees;
}
function calcDistance(latA, lngA, latB, lngB) {
var rLatA = toRadians(latA);
var rLatB = toRadians(latB);
var rHalfDeltaLat = toRadians((latB - latA) / 2);
var rHalfDeltaLng = toRadians((lngB - lngA) / 2);
return 2 * earthRadiusKm * Math.asin(Math.sqrt(Math.pow(Math.sin(rHalfDeltaLat), 2) + Math.cos(rLatA) * Math.cos(rLatB) * Math.pow(Math.sin(rHalfDeltaLng), 2)));
}
function findPoint(lat, lng, bearing, distance) {
var rLat = toRadians(lat);
var rLng = toRadians(lng);
var rBearing = toRadians(bearing);
var rAngDist = distance / earthRadiusKm;
var rLatB = Math.asin(Math.sin(rLat) * Math.cos(rAngDist) + Math.cos(rLat) * Math.sin(rAngDist) * Math.cos(rBearing));
var rLngB = rLng + Math.atan2(Math.sin(rBearing) * Math.sin(rAngDist) * Math.cos(rLat), Math.cos(rAngDist) - Math.sin(rLat) * Math.sin(rLatB));
var pLat = toDegrees(rLatB);
var pLng = toDegrees(rLngB);
return new GeoPoint(pLat, pLng);
}
function calcArea(lat, lng, distanceKm) {
var northPoint = findPoint(lat, lng, 0, distanceKm);
var eastPoint = findPoint(lat, lng, 90, distanceKm);
var southPoint = findPoint(lat, lng, 180, distanceKm);
var westPoint = findPoint(lat, lng, 270, distanceKm);
return new GeoArea(lat, lng, distanceKm, northPoint, southPoint, eastPoint, westPoint);
}
Usage example:
// calculate area with center in lat:55.742793 lng:37.615401
// and distance in 23 km
var area = calcArea(55.742793, 37.615401, 23);
//returns true
area.inArea(55.714735, 37.629547);
//returns false
area.inArea(55.693842, 38.015442);
I have this script I am working on that utilizes the oCanvas JS Library (http://ocanvas.org/) that creates an HTML5 canvas and displays multiple objects within the canvas. Currently, I have the script reading from an external XML document and loops through each project node and creates a circle object on the canvas.
I am having issues with trying to place this objects on the canvas evenly spaced from the middle circle (the logo variable in the code below).
// GLOBALS
var xmlData = '<?xml version="1.0" encoding="UTF-8"?><root name="CompanyName"><projects><project name="Project1"></project><project name="Project2"></project></projects></root>'
var xmlObj = []
// var angle = (360 * Math.PI)/180
var padding = 15
var canvas = oCanvas.create({
canvas: '#myCanvas'
})
var c_width = canvas.width
var c_height = canvas.height
var logo = canvas.display.ellipse({
x: c_width / 2,
y: c_height / 3,
radius: 80,
fill: '#d15851'
})
canvas.addChild(logo)
// var getXML = function(file){
// $.ajax({
// url: file,
// type: 'GET',
// dataType: 'xml',
// async: false,
// success: parseXML
// })
// }
var parseXML = function() {
var xmlDoc = $.parseXML(xmlData)
var xml = $(xmlDoc)
xml.find('project').each(function(i){
xmlObj[i] = canvas.display.ellipse({
fill: '#'+'0123456789abcdef'.split('').map(function(v,i,a){
return i>5 ? null : a[Math.floor(Math.random()*16)] }).join(''),
radius: 40,
opacity: 1
})
});
var angleSingleton = {
"currentAngle": 0,
"currentOffset": 0,
"incrementAngle": function() {
this.currentAngle = this.currentAngle + this.currentOffset
}
}
angleSingleton.currentOffset = Math.floor((360 * Math.PI)/xmlObj.length);
for(h = 0; h < xmlObj.length; h++) {
xmlObj[h].x = (logo.x + logo.radius * Math.cos(angleSingleton.currentAngle)) + xmlObj[h].radius + padding;
xmlObj[h].y = (logo.y + logo.radius * Math.sin(angleSingleton.currentAngle)) + xmlObj[h].radius + padding;
canvas.addChild(xmlObj[h])
angleSingleton.incrementAngle()
}
}
//
$(document).ready(function(){
parseXML()
})
What you want to take a look at is the Parametric equation for circles. Basically it defines a point along a circles perimeter at a specific angle. This answer covers it in more detail.
To get your x and y values for the new circle you use the following equations:
x = logo.x + logo.radius * Math.cos(angle)
y = logo.y + logo.radius * Math.sin(angle)
However you need to account for the room the new circle is going to take up plus any room for padding if you want it.
x = (logo.x + logo.radius * Math.cos(angle)) + newCircle.radius + circlePadding
y = (logo.y + logo.radius * Math.sin(angle)) + newCircle.radius + circlePadding
For the angle function try something like this:
var angleSingleton = {
"currentAngle": 0,
"currentOffset": 0,
"incrementAngle": function() {
this.currentAngle = this.currentAngle + this.currentOffset
}
}
angleSingleton.currentOffset = (360 * Math.PI)/xmlObj.length;
Then you can use this to keep track of the angle you need for the formula. To get the current angle use angleSingleton.currentAngle and replace angle++ with angleSingleton.incrementAngle
I ended up figuring it out!
// EXTENDING OBJECTS
Array.prototype.min = function(array) {
return Math.min.apply(Math, array);
}
Array.prototype.max = function(array) {
return Math.max.apply(Math, array)
}
//
// GLOBALS
var xmlData = '<?xml version="1.0" encoding="UTF-8"?><root name="CompanyName"><projects><project name="Project1"></project><project name="Project2"></project><project name="Project3"></project></projects></root>'
var xmlObj = []
var xmlDoc, xml;
var padding = 15
var canvas = oCanvas.create({
canvas: '#myCanvas'
})
var c_width = canvas.width
var c_height = canvas.height
var logo = canvas.display.ellipse({
x: c_width / 2,
y: c_height / 3,
radius: 80,
fill: '#d15851'
})
var rectObj = function(){
this.x = 0;
this.y = 0;
this.width = 100;
this.height = 100;
this.size = this.width + this.height; //this would equate to a circles radius if dealing with circles
this.fillerText = null;
this.fillRect = function(hexVal){
if(!hexVal)
return '#'+'0123456789abcdef'.split('').map(function(v,i,a){
return i>5 ? null : a[Math.floor(Math.random()*16)] }).join('')
else
return hexVal
};
this.drawRect = function(){
return canvas.display.rectangle({
width: this.width,
height: this.height,
fill: this.fillRect(),
x: this.x,
y: this.y
})
};
this.checkCollisions = function(objToCheck) {
var centerA = { x: this.x+(this.size/2), y: this.y+(this.size/2) };
var centerB = { x:objToCheck.x+(objToCheck.size/2), y: objToCheck.y+(objToCheck.size/2) };
var distance = Math.sqrt(((centerB.x-centerA.x)*(centerB.x-centerA.x) + (centerB.y-centerA.y)*(centerB.y-centerA.y)));
if(distance < (this.size+objToCheck.size)) {
objToCheck.x = this.x - (canvas.width/4)
objToCheck.fillRect = function(){
return 'red'
}
}
}
}
canvas.addChild(logo)
var parseXML = function() {
xmlDoc = $.parseXML(xmlData)
xml = $(xmlDoc)
xml.find('project').each(function(i){
xmlObj[i] = new rectObj()
xmlObj[i].fillerText = $(this).attr('name')
xmlObj[i].x = (logo.x + logo.radius * Math.cos((360*Math.PI) / (i + 1)) + padding) + ((xmlObj[i].width / 2) + (i+1));
xmlObj[i].y = (logo.y + logo.radius * Math.sin((360*Math.PI) / (i + 1)) + padding);
});
for(i = 0; i < xmlObj.length; i++) {
for(a = i+1; a < xmlObj.length; a++) {
xmlObj[i].checkCollisions(xmlObj[a])
}
canvas.addChild(xmlObj[i].drawRect())
}
}
//
$(document).ready(function(){
parseXML()
})
Screen shot:
I obviously need to write in the Y coords for the rectangles so that they're not touching the main circle, but for now, they all "float" as they're supposed to :)
Thanks for all of your help Devin!
BTW, I was able to write my collision algorithm by studying this JS file: http://andersonferminiano.com/html5/studies/balls_collisions/collision.js
I have a Google Maps Circle drawn on v3 api. When the user has plotted there circle (or polygon if they choose), they can save the data to the server. If the user has picked a radial search, the Centre coordinates and the radius in feet is stored to the database. This means when the user reloads his search, it can pull through the circle again (like below).
I'm having 1 problem, however, which is when the user selects what search they would like to use. It loads the polygon fine, if they drew a polygon, and if it's a circle it pulls through the marker on the center. However what I need is a function in static maps to draw a circle.
A bit late in the game, but nothing I found solved my issue (serverside php only, no javascript).
I ended up getting there in the end and have detailed my method here: http://jomacinc.com/map-radius/ and the short version is below.
This PHP function will return an encoded polyline string of lat/lng points in a circle around the specified point, and at the specified radius. The function requires Gabriel Svennerberg’s PHP polyline encoding class available here (http://www.svennerberg.com/examples/polylines/PolylineEncoder.php.txt).
function GMapCircle($Lat,$Lng,$Rad,$Detail=8){
$R = 6371;
$pi = pi();
$Lat = ($Lat * $pi) / 180;
$Lng = ($Lng * $pi) / 180;
$d = $Rad / $R;
$points = array();
$i = 0;
for($i = 0; $i <= 360; $i+=$Detail):
$brng = $i * $pi / 180;
$pLat = asin(sin($Lat)*cos($d) + cos($Lat)*sin($d)*cos($brng));
$pLng = (($Lng + atan2(sin($brng)*sin($d)*cos($Lat), cos($d)-sin($Lat)*sin($pLat))) * 180) / $pi;
$pLat = ($pLat * 180) /$pi;
$points[] = array($pLat,$pLng);
endfor;
require_once('PolylineEncoder.php');
$PolyEnc = new PolylineEncoder($points);
$EncString = $PolyEnc->dpEncode();
return $EncString['Points'];
}
You can now the use the above function to create a static map.
/* set some options */
$MapLat = '-42.88188'; // latitude for map and circle center
$MapLng = '147.32427'; // longitude as above
$MapRadius = 100; // the radius of our circle (in Kilometres)
$MapFill = 'E85F0E'; // fill colour of our circle
$MapBorder = '91A93A'; // border colour of our circle
$MapWidth = 640; // map image width (max 640px)
$MapHeight = 480; // map image height (max 640px)
/* create our encoded polyline string */
$EncString = GMapCircle($MapLat,$MapLng, $MapRadius);
/* put together the static map URL */
$MapAPI = 'http://maps.google.com.au/maps/api/staticmap?';
$MapURL = $MapAPI.'center='.$MapLat.','.$MapLng.'&size='.$MapWidth.'x'.$MapHeight.'&maptype=roadmap&path=fillcolor:0x'.$MapFill.'33%7Ccolor:0x'.$MapBorder.'00%7Cenc:'.$EncString.'&sensor=false';
/* output an image tag with our map as the source */
echo '<img src="'.$MapURL.'" />'
function GMapCircle(lat,lng,rad,detail=8){
var uri = 'https://maps.googleapis.com/maps/api/staticmap?';
var staticMapSrc = 'center=' + lat + ',' + lng;
staticMapSrc += '&size=100x100';
staticMapSrc += '&path=color:0xff0000ff:weight:1';
var r = 6371;
var pi = Math.PI;
var _lat = (lat * pi) / 180;
var _lng = (lng * pi) / 180;
var d = (rad/1000) / r;
var i = 0;
for(i = 0; i <= 360; i+=detail) {
var brng = i * pi / 180;
var pLat = Math.asin(Math.sin(_lat) * Math.cos(d) + Math.cos(_lat) * Math.sin(d) * Math.cos(brng));
var pLng = ((_lng + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(_lat), Math.cos(d) - Math.sin(_lat) * Math.sin(pLat))) * 180) / pi;
pLat = (pLat * 180) / pi;
staticMapSrc += "|" + pLat + "," + pLng;
}
return uri + encodeURI(staticMapSrc);}
Javascript version
Based on the answer from Jomac, Here is a Java/Android version of the same code.
It uses the PolyUtil class from Google Maps Android API Utility Library to encode the path.
import android.location.Location;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.PolyUtil;
import java.util.ArrayList;
public class GoogleStaticMapsAPIServices
{
private static final double EARTH_RADIUS_KM = 6371;
private static String GOOGLE_STATIC_MAPS_API_KEY = "XXXXXXXXXXXXX";
public static String getStaticMapURL(Location location, int radiusMeters)
{
String pathString = "";
if (radiusMeters > 0)
{
// Add radius path
ArrayList<LatLng> circlePoints = getCircleAsPolyline(location, radiusMeters);
if (circlePoints.size() > 0)
{
String encodedPathLocations = PolyUtil.encode(circlePoints);
pathString = "&path=color:0x0000ffff%7Cweight:1%7Cfillcolor:0x0000ff80%7Cenc:" + encodedPathLocations;
}
}
String staticMapURL = "https://maps.googleapis.com/maps/api/staticmap?size=640x320&markers=color:red%7C" +
location.getLatitude() + "," + location.getLongitude() +
pathString +
"&key=" + GOOGLE_STATIC_MAPS_API_KEY;
return staticMapURL;
}
private static ArrayList<LatLng> getCircleAsPolyline(Location center, int radiusMeters)
{
ArrayList<LatLng> path = new ArrayList<>();
double latitudeRadians = center.getLatitude() * Math.PI / 180.0;
double longitudeRadians = center.getLongitude() * Math.PI / 180.0;
double radiusRadians = radiusMeters / 1000.0 / EARTH_RADIUS_KM;
double calcLatPrefix = Math.sin(latitudeRadians) * Math.cos(radiusRadians);
double calcLatSuffix = Math.cos(latitudeRadians) * Math.sin(radiusRadians);
for (int angle = 0; angle < 361; angle += 10)
{
double angleRadians = angle * Math.PI / 180.0;
double latitude = Math.asin(calcLatPrefix + calcLatSuffix * Math.cos(angleRadians));
double longitude = ((longitudeRadians + Math.atan2(Math.sin(angleRadians) * Math.sin(radiusRadians) * Math.cos(latitudeRadians), Math.cos(radiusRadians) - Math.sin(latitudeRadians) * Math.sin(latitude))) * 180) / Math.PI;
latitude = latitude * 180.0 / Math.PI;
path.add(new LatLng(latitude, longitude));
}
return path;
}
}
I think it is not possible to draw a circle on a static Google map. You would need to approximate the circle by a polyline (best in encoded format). This has already been mentioned in Stackoverflow and it is demonstrated e.g. by Free Map Tools.
Sharing my C# version
private string GMapCircle(double lat, double lng, double rad, int detail = 8)
{
const string uri = "https://maps.googleapis.com/maps/api/staticmap?";
var staticMapSrc = "center=" + lat + "," + lng;
staticMapSrc += "&zoom=16";
staticMapSrc += "&maptype=satellite";
staticMapSrc += "&key=[YOURKEYHERE]";
staticMapSrc += "&size=640x426";
staticMapSrc += "&path=color:0xff0000ff:weight:1";
const int r = 6371;
const double pi = Math.PI;
var latAux = (lat * pi) / 180;
var longAux = (lng * pi) / 180;
var d = (rad / 1000) / r;
var i = 0;
if (rad > 0)
{
for (i = 0; i <= 360; i += detail)
{
var brng = i * pi / 180;
var pLat = Math.Asin(Math.Sin(latAux) * Math.Cos(d) + Math.Cos(latAux) * Math.Sin(d) * Math.Cos(brng));
var pLng = ((longAux + Math.Atan2(Math.Sin(brng) * Math.Sin(d) * Math.Cos(latAux), Math.Cos(d) - Math.Sin(latAux) * Math.Sin(pLat))) * 180) / pi;
pLat = (pLat * 180) / pi;
staticMapSrc += "|" + pLat + "," + pLng;
}
}
else
{
//TODO - Add marker
}
return uri + staticMapSrc;
}
This solution uses the significantly more versatile Canvas API to draw over the map image . All code is in Typescript, so simply remove type declarations if you're using Javascript.
ADVANGES OF USING CANVAS:
Its easier to draw shapes on.
Those shapes can also be revised without re-requesting the map image from Google.
The drawing 'layer' can be serialized independently of the underlying map image.
USAGE:
// DEFINE BASIC VARIABLES
const latitude: -34.3566871,
const longitude: 18.4967666
const mapZoom = 12;
const imageWidth: 100;
const imageHeight: 100;
// INVOKE UTILITY FUNCTION
savePlaceImage({
// SET BASIC PROPS
latitude,
longitude,
mapZoom,
imageWidth,
imageHeight,
fileName: 'Cape Point',
// DRAW IMAGE USING CANVAS API
draw: ctx => {
// draw location as dot
ctx.fillStyle = '#FF3366';
ctx.beginPath();
ctx.arc(imageWidth / 2, imageHeight / 2, 10, 0, 2 * Math.PI);
ctx.fill();
// draw circle around location with 1 kilometer radius
ctx.strokeStyle = '#0000FF';
ctx.beginPath();
ctx.arc(imageWidth / 2, imageHeight / 2, pixelsPerMeter(latitude) * 1000, 0, 2 * Math.PI);
ctx.stroke();
}
})
UTILITIES:
function savePlaceImage(
config: {
latitude: number,
longitude: number,
mapZoom: number,
imageWidth: number,
imageHeight: number,
fileName: string,
draw: (ctx: CanvasRenderingContext2D) => void,
},
) {
// DOWNLOAD MAP IMAGE FROM GOOGLE'S STATIC MAPS API AS A BLOB
return from(axios.get<Blob>(`https://maps.googleapis.com/maps/api/staticmap`, {
params: {
key: GOOGLE_MAPS_API_KEY,
size: `${config.imageWidth}x${config.imageHeight}`,
zoom: `${config.mapZoom}`,
center: `${config.latitude},${config.longitude}`,
style: 'feature:all|element:labels|visibility:off',
},
responseType: 'blob',
// CONVERT BLOB TO BASE64 ENCODED STRING
}).then(response => {
const reader = new FileReader();
reader.readAsDataURL(response.data);
return new Promise<string>(resolve => reader.onloadend = () => resolve(reader.result as string));
// CREATE HTML IMG ELEMENT, SET IT'S SRC TO MAP IMAGE, AND WAIT FOR IT TO LOAD
}).then(response => {
const image = document.createElement('img');
image.src = response;
return new Promise<HTMLImageElement>(resolve => image.onload = () => resolve(image));
// CREATE HTML CANVAS ELEMENT, THEN DRAW ON TOP OF CANVAS USING CANVAS API, THEN CONVERT TO BLOB
}).then(image => {
const canvas = document.createElement('canvas');
canvas.width = config.imageWidth;
canvas.height = config.imageHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
config.draw(ctx);
return new Promise<Blob>(resolve => canvas.toBlob(blob => resolve(blob)));
// ATTACH BLOB TO HTML FORM WHICH CONVERTS IT TO A FILE TO BE POSTED, THEN SEND FILE TO SERVER
}).then(blob => {
const form = new FormData();
form.append('blob', blob, `${config.fileName}.png`);
const file = form.get('blob') as File;
return axios.post<{ file }>('https://www.my-api.com/save-image', form);
}));
}
function pixelsPerMeter(latitude: number) {
const radiusOfEarthInKilometers = 6371;
return Math.cos(latitude * Math.PI / 180) * 2 * Math.PI * radiusOfEarthInKilometers / (256 * Math.pow(2, 12));
}
python version, polyline library used for encoding the polygon
import math, polyline
def g_map_circle(lat,lng,radius,detail=8):
points = []
r = 6371
pi = math.pi
_lat = (lat * pi) /180
_lng = (lng * pi) /180
d = radius / r
i = 0
while i <= 360:
i = i + detail
brng = i * pi /180
p_lat = math.asin(math.sin(_lat) * math.cos(d) + math.cos(_lat) * math.sin(d) * math.cos(brng));
p_lng = (_lng + math.atan2(math.sin(brng) * math.sin(d) * math.cos(_lat), math.cos(d) - math.sin(_lat) * math.sin(p_lat))) * 180 / pi
p_lat = (p_lat * 180) /pi
points.append((p_lat,p_lng))
return polyline.encode(points)