JavaScript: best way to maintain bounds (Google Maps)? - javascript

I am using the Google Maps v3 API. I currently am making a request to fetch new data each time a person changes the viewport (by either zooming or shifting the map) and am throwing away the old data I have. This works great, but now I want to cache the data so that I don't need to fetch the data each time the viewport changes. The Google Maps API defines a viewport by its Northeast and Southwest coordinate consisting of a latitude and a longitude. They are stored in objects called LatLngBounds.
I have come up with 2 ways I can do this:
Store the bounds of each new viewport the user visits and check if the new viewport is in an old viewport and fetch new data only of the part of the new viewport that is not within an old viewport. Essentially,
Divide each new viewport up into rectangular sections of data that we have and data that needs to be fetched. Store the bounds of each of the rectangular sections.
If anyone can think of a better way to do this, feel free to suggest new approaches.
My question is which one is going to be better in terms of better performance/memory usage and overall speed? They are both similar algorithms so does it really matter?
Also, right now both algorithms rely on dividing up the new viewport based on old viewports. What would the algorithm to divide new viewports look like? (Assume I implemented my 2nd algorithm)
var prevBounds = [ /* Array of previously seen bounds */ ];
var newViewport = map.getBounds(); // New Viewport to divide up
var sw = newViewport.getSouthWest();
var swlat = sw.lat();
var swlng = sw.lng();
var ne = newViewport.getNorthEast();
var nelat = ne.lat();
var nelng = ne.lng();
// newViewport.intersects(bounds)
// Returns true if this bounds shares any points with this bounds.

I might consider doing this more or less exactly the way Google serves up map tiles - instead of loading data for the entire viewport at once, carve up the entire map into square areas (though probably bigger areas than Google's 256x256 tiles), determine which areas are in the current viewport, and load data for those areas. As the user pans and zooms the map, check the viewport bounds to see whether new areas have come into the frame, and load them as necessary. Rough pseudocode:
var cache = {}
function onViewportChange() {
// get new bounds
var bounds = map.getBounds();
// identify all the areas in the bounds
var areas = getAreas(bounds);
areas.forEach(function(area) {
if (area.key in cache) {
// do nothing, or load items from cache
} else {
// load new data, storing the key (and maybe the data)
// to the cache
}
})
}
function getAreas(bounds) {
/* given a set of bounds, return an array of objects like:
[
{
x: 1,
y: 2,
zoom: 4,
key: "4-1,2",
bounds: b // lat/lon bounds of this area
},
...
]
*/
}
(See the Google explanation of map coordinates and this example for an idea of how to implement getAreas.)
The appeal of this is that the areas you're retrieving are much simpler, and it becomes very easy to check whether you've already loaded data for a given area - each area can have a simple unique key, probably a string made out of the x/y/zoom coordinates, and as each new area is loaded you save the key (or maybe the key and the data - it depends on whether you're removing old data from the map or just leaving it there) to some cache object. Then all you have to do when the viewport moves to a new area is check for the existence of that key in your cache.
The downside is that you might often load data outside the current viewport, and you're probably sending more requests to the server than you would with the implementations you suggest. This method might work best if you're serving up different data at different zoom levels - otherwise you might be stuck trying to choose a single-size cache area that works well at different zooms.

Related

Maximum latitude map panning with leaflet

I am new to leaflet and I want the restrict the panning of a world map horizontally and not vertically (longitude but not latitude) because this map will display pictures when I click on them and I cant see well the image when i restrict the panning horizontally AND vertically. The map by itself it not a picture, it's a real world map. But when I click on certain location, a small picture will appear on the map.
I try to play with maxBounds and setMaxbounds. The normal maxBounds (to view the world map) is :
maxBounds: [[-85, -180.0],[85, 180.0]],
When i try to put the latitude to
[[-150, -180.0],[150, 180.0]]
, the vertical panning is still restricted. Can somebody help please? Thank you.
This sounds similar to a (quite obscure) issue in the Leaflet issue tracker a
while back: see https://github.com/Leaflet/Leaflet/issues/3081
However, that issue was dealing with infinite horizontal bounds, not vertical bounds in a CRS that already has some preset limits.
If you set the map's maxBounds to a value larger than 85 (the value for MAX_LATITUDE of L.Projection.Spherical) and run a debugger, the call stack goes through the map's _panInsideMapBounds(), then panInsideBounds(), then _limitCenter(), then _getBoundsOffset, then project(), then through the map CRS's latLngToPoint, then untimately L.Projection.Spherical's project(). L.Projection.Spherical.project() projects the bounds' limits into pixel coordinates, and clamps the projected point to be inside the projection's limits.
There are a lot of reasons behind this, one of them being to prevent users from putting markers outside the area covered with tiles:
(This is particularly important when a user confuses lat-lng with lng-lat and tries to use a value outside the [-90,90] range for latitude, and the projection code starts returning Infinity values everywhere)
How to get around this? Well, we can always specify the map's CRS, and we can create a CRS with a hacked projection which enforces a different limit. Please be aware that this changes how the pixelOrigin works internally (as explained in the Leaflet tutorial about extending layers), so stuff (particularly plugins) might break.
So something like:
var hackedSphericalMercator = L.Util.extend(L.Projection.SphericalMercator, {
MAX_LATITUDE: 89.999
});
var hackedEPSG3857 = L.Util.extend(L.CRS.EPSG3857, {
projection: hackedSphericalMercator
});
var map = new L.Map('mapcontainer', {
crs: hackedEPSG3857,
});
Of course, then you can set up your own maxBounds:
var map = new L.Map('mapcontainer', {
crs: hackedEPSG3857,
maxBounds: [[-Infinity, -10], [Infinity, 10]]
});
In this case, the bounds' limits would still be clamped to hackedSphericalMercator.MAX_LATITUDE, but you should have enough wiggle room for your application.
As a side note: A radically different approach to this problem would be to use a different map projection. We're used to a spherical cylindrical projection, but that's not the only way to flatten the earth.
In particular, a Transverse Mercator projection (or pretty much any other transverse cylindrical projection, for that matter) works pretty much in the same way, but wraps vertically instead of horizontally, and it's the projected longitudes, not latitudes, the ones which approach infinity asymptotically when approaching the [-180, 180] range. Let me borrow an image from its wikipedia article:
This implies a different set of challenges (namely finding some raster tiles appropriate for your application, including which prime meridian to use, and making proj4leaflet play nice), but it's definitely doable.

angularjs google map zoom map after max zoom exceeded

I want to zoom the google map after it exceeds it maximum zoom level. I google it and didn't find any solution. I am building a rooftop app any one can please help me on this.
The Google Maps API docs show that the google.maps.MapOptions object has a property called maxZoom that you can set when creating the map to set the maximum amount the map can be zoomed.
You can also set minZoom in the same way. So as long as your users don't need to be able to zoom very far out in order to move around effectively (e.g. if your app moves the map programmatically somehow) then you may even want to set the minZoom to a realistic value to ensure that people don't accidentally zoom too far out when trying to scroll down the page (I personally do that all the time with embedded google maps and it drives me crazy). But be careful with this: you don't want to ruin the user experience by removing your users' ability to navigate the map efficiently.
One thing you'll notice when reading the description of the maxZoom and minZoom parameters is that the docs don't specify the range of acceptable numbers. So how are you to figure out what numbers to pass in? And how do you change the max zoom after creating the map?
If the Map object is bound to a variable called map, then you can get the current zoom with map.getZoom() and you can set the current zoom with map.setZoom(zoom) where zoom is floating-point Number such that zoom >= 0. (Note: These methods get and set the "zoom" property of the Map object, but making changes directly to map.zoom will not cause the map to change, so be sure to use the getter and setter methods).
The most-zoomed-out level for the zoom property is 0, but the most-zoomed-in level will actually vary depending on where in the world the map is focused. (This makes sense, since street-level in some places could be a mile underground in others depending on elevation.) And at some zoom level, Google will no longer have (usable) satellite images.
Enter the MaxZoomService Class. This object's constructor can be found at google.maps.MaxZoomService. The instances of this class have only one useful method: getMaxZoomAtLatLng which takes a LatLng instance or a LatLngLiteral as its first parameter and a callback function to handle the result as its second.
A LatLng instance can be created with the google.maps.LatLng constructor, and a LatLngLiteral is just a normal object literal that has a lat property and a lng property. But since you'll probably be passing in a LatLng object from another method like map.getCenter(), you shouldn't have to worry about it.
Here's an example showing how to find out the maximum zoom available on the Satellite View at the center of the map's current view.
var mzs = new google.maps.MaxZoomService(); // Create a MaxZoomService instance
var mapCenterLatLng = map.getCenter(); // Get a LatLng instance
var handleMaxZoom = function(res) { // Callback Function accepting MaxZoomResult instance
if (res.status === "OK") {
// handle success
console.log("Max Zoom: " + res.zoom); // Print max possible zoom
map.setZoom(res.zoom); // Set the zoom to the max possible
} else {
// handle failure
console.log("Oops");
}
};
mzs.getMaxZoomAtLatLng(mapCenterLatLng, handleMaxZoom);
// => Max Zoom: [Number]
Edit:
If you really want to exceed the maximum zoom level (not just the default maximum zoom level as described above) your only real option is going to be using css transforms. Depending on how complicated the page layout is, this could end up being pretty difficult to get perfect.
I should note that this is typically not a good idea even if you can manage to get the layout to look good and the images to not be blurry. It carries all the same problems as using CSS to transform canvas elements (and some additional problems on top because it's not as simple as dealing with just a single DOM element). One notable problem is that applying CSS transforms will not affect the internal coordinates of the map (like those contained in click events) so you'll have to transform those every time you want to use them. Additionally, since the primary map div (the one returned by map.getDiv()) has to sit inside a container, this method will often cause it to overflow its container and you'll have to deal with that added complexity.
If you still want to do it, then here are some pieces of the puzzle that may help:
var mapCanvas = map.getDiv(); // The "main" div element that contains the map
var container = mapCanvas.parentElement; // You'll need some sort of container here
var scalingFactor = 1.5; // => Set a scaling factor (ex: 1.5 means scale it to 150% normal size)
/* Use CSS zoom property and/or vendor transforms to scale the map (though you could do this in straight CSS instead) */
mapCanvas.style.zoom = scalingFactor;
mapCanvas.style.MozTransform = 'scale(' + scalingFactor + ')';
mapCanvas.style.WebkitTransform = 'scale(' + scalingfactor + ')';
/* Then try to fit the scaled map back into its container, which will depend on how you're displaying it */
mapCanvas.style.width = "calc(100% / " + scalingFactor + ")"; // The CSS 'calc()' method might be useful here

Creating accurate line buffer in ESRI Javascript API

I'm using the ESRI JavaScript API v3.8. (I know 3.11 is out - can't upgrade yet.)
What I'm trying to do is to create a geometric buffer of a size provided by the user from an arbitrary line (or point) selected by the user. Some of the relevant code is shown below:
var params = new esri.tasks.BufferParameters();
params.distances = [values.distance]; //the input distance
params.geometries = [gr.geometry]; //the input geometry
params.unit = esri.tasks.GeometryService.UNIT_FOOT;
params.outSpatialReference = mapView.map.spatialReference; //always 3857
params.bufferSpatialReference = gr.geometry.spatialReference; //always 3857
esri.config.defaults.io.corsEnabledServers.push('mydomain.com');
esri.config.defaults.io.proxyUrl = 'https://serverdomain.com/proxy';
var gsvc = new esri.tasks.GeometryService('https://tasks.arcgisonline.com/ArcGIS/rest/services/Geometry/GeometryServer');
gsvc.simplify(params.geometries, function(geometries){
params.geometries = geometries;
gsvc.buffer(params, function(geometries){
//add output geometry to the map and perform spatial query with it
}, function(err){
//handle error
});
}, function(err){
//handle error
});
The problem is that, if I use an input distance of 500 (feet), then measure the distance from the center line of the input geometry on self._queryGeometry, using ESRI's measurement tool, the actual width of the polygon created is something like 370 feet on either side of the center line.
I've managed to get this to work more accurately using the Illinois State Plane spatial reference, as my test objects are in Illinois, but the logic needs to work everywhere.
When I try various incarnations of doing a geodesic buffer, the input distance unit seems to get ignored and, using an input distance value of 500, I get a buffer that spans the entire world! Either that or the results are exactly the same, depending on how things are set up.
I believe I need to do a geodesic buffer, but I have absolutely no idea how to go about that in such a way that the geometry service will actually pay attention to the units I'm sending in.
Any ideas would be greatly appreciated. Let me know if I've left anything out.
Sounds like there might be a spatial reference issue somewhere. You can try re-projecting geometry into 3857 if that's what the map is and I would inspect the geometry being returned from the buffer and simplify to make sure it looks like what your expecting. I have had issues with the geometry service area's and length's returning slightly incorrect geometries and in my case it ended up being an issue with an incorrect spatial reference. Also, I know you said you can't upgrade, but 3.13 is out and can do geometry options locally without the need for a proxy or any network requests, if possible, it would be worth trying out.

ZoomTo boundingBox in SearchBox.onSelect with HERE maps and place

I am using nokia.places.widget.SearchBox together with nokia.maps.map.Display.
You can use onSelect function of the SearchBox to perform operations when a user picks an items from the results list.
What I need to do is center and zoom the map to the picked place. My problem is concerning zooming because I don't know what level to use.
I noticed that some places have boundingBox (to use with the zoomTo function of the Display component) property but other doesn't. I could use category ('administrative-region', 'street-square', etc.) but it is other than precise and moreover I can't find a list of possible category codes.
Any suggestion?
To get the map to move/zoom to a location when a suggestion is chosen, you will need to implement an onResults handler:
var fromSearchBox = new nokia.places.widgets.SearchBox({
targetNode: "fromSearchBox",
template: "fromSearchBox",
map: map,
onResults: function (data) {
// Your code goes here //
}
});
As you noticed the data field may or may not hold a bounding box. This is because only a few locations cover a defined area, most are point addresses.
For the subset which have a boundingbox you can do the following:
if (data.results.items[0].boundingBox){
map.zoomTo(data.results.items[0].getBoundingBox());
}
For the remainder the answer will depend upon what you are trying to achieve, but you could try any of the following:
Move to the location on the map without altering the zoom. (i.e. let the user decide)
map.set("center", data.results.items[0].position);
Move to the specified bounding box for a point object at the
specified location.
var obj = new nokia.maps.map.StandardMarker(startPoint);
map.zoomTo(obj.getBoundingBox());
Or alternatively: map.zoomTo(nokia.maps.geo.BoundingBox.coverAll([startPoint]));
Define a bounding box surrounding the point location and zoom to that instead
startPoint =data.results.items[0].position;
bottomLeft = new nokia.maps.geo.Coordinate(startPoint.latitude - 0.1,
startPoint.longitude + 0.1);
topRight = new nokia.maps.geo.Coordinate(startPoint.latitude + 0.1,
startPoint.longitude - 0.1);
map.zoomTo(nokia.maps.geo.BoundingBox.coverAll([topRight, bottomLeft]));
Additionally, Display.zoomTo() can also take an additional parameter, so if you use map.zoomTo(BoundingBox, true) you will also keep the current center for the map on screen, and this may give more context to a user.

Erratic overlay behavior with custom projection, Google Maps API v3

I want to browse a single image with the Google Maps API, for which I've defined my own projection. I wanted to use a GroundOverlay instead of several image tiles, because I only have one small-resolution image, but I wanted it to still be zoomable. However, I get some erratic behavior when trying to work with this projection:
No overlays show up at all at zoom level 0.
At zoom level 1 and higher, Markers show up, but GroundOverlays still don't.
However, I can get GroundOverlays to show up very briefly, if I zoom out from any level. It will only show while it's zooming out and disappear again immediately. Also, while it does show up shortly, it does not show up at the right coordinates, but the Markers do.
I'm rather new to the API, so I would not be surprised if it was a simple oversight on my part, but I just can't see what could cause this. Here is the code for my projection, which just maps the lat/lng linearly to map coordinates:
function EvenMapProjection() {
var xPerLng = 512/360;
var yPerLat = 512/180;
this.fromLatLngToPoint = function(latlng) {
var x = (latlng.lng()+180)*xPerLng;
var y = (latlng.lat()+90)*yPerLat;
console.log('Lng', latlng.lng(), 'Lat', latlng.lat(), '-> Point', x, y);
return new google.maps.Point(x, y);
};
this.fromPointToLatLng = function(point) {
var lat = point.y/yPerLat-90;
var lng = point.x/xPerLng-180;
console.log('Point', point.x, point.y, '-> Lng', lng, lat);
return new google.maps.LatLng(lat, lng);
};
}
An example of what I'm trying to do without the projection (using the default Mercator projection):
http://95.156.209.71/tmp/a.html
The same example with the projection as defined above:
http://95.156.209.71/tmp/b.html
And finally an example using the projection but without the GroundOverlay, and instead just using tiled images (always the same image):
http://95.156.209.71/tmp/c.html
The last link also shows the Marker at LatLng(0, 0) appear at zoom level 1 (or higher), but not at level 0.
Is there something I'm just missing, or some buggy code, or is this actually a problem in the API?
I just found out that my mistake was in the definition of the ground overlay. I was at zoom level 0, which meant that I set the bounds for the overlay from (-90,-180) to (90,180), but the API seems to have issues with these levels, because they wrap longitude, hence I got weird errors. I adjusted it to be at level 1 for minimum zoom, and set the overlay from (-45,-90) to (45,90), and now it all works fine.

Categories