Related
Hello world for the first time famed stack overflow!
I am parsing a Tiled-made .json map(just a test) from Reading JSON Tiled map editor file and displaying to canvas
I don't fully understand the renderLayer function, and if advisable will open a new question on that. From my rough understanding it renders each layer by tile and pushes to the final "scene" img that will be drawn.
My question specifically is how to draw a "player" object between layers. I have a Top, Middle, and Bottom layer. The middle layer is empty and where I intend to draw the player in order to add depth. I don't know where in the code to add my player update function, and I also feel like having a player update function inside the map rendering is not very OOP..
In comments is how I can get the layer name, and I have a player update function that will handle drawing.
Thanks in advance for any suggestions\explanation.
Code for the rendering the map:
$(function() {
var c = document.getElementById("canvas").getContext('2d');
scene = {
layers: [],
renderLayer: function(layer) {
/*
if(layer.name == "Middle"){
player.update();
}
*/
if (layer.type !== "tilelayer" || !layer.opacity) { return; }
var s = c.canvas.cloneNode(),
size = scene.data.tilewidth;
s = s.getContext("2d");
if (scene.layers.length < scene.data.layers.length) {
layer.data.forEach(function(tile_idx, i) {
if (!tile_idx) { return; }
var img_x, img_y, s_x, s_y,
tile = scene.data.tilesets[0];
tile_idx--;
img_x = (tile_idx % (tile.imagewidth / size)) * size;
img_y = ~~(tile_idx / (tile.imagewidth / size)) * size;
s_x = (i % layer.width) * size;
s_y = ~~(i / layer.width) * size;
s.drawImage(scene.tileset, img_x, img_y, size, size, s_x, s_y, size, size);
});
scene.layers.push(s.canvas.toDataURL());
c.drawImage(s.canvas, 0, 0);
}else {
scene.layers.forEach(function(src) {
//console.log(scene);
var i = $("<img />", { src: src })[0];
c.drawImage(i, 0, 0);
});
}
},
renderLayers: function(layers) {
layers = Array.isArray(layers) ? layers : this.data.layers;
layers.forEach(this.renderLayer);
},
loadTileset: function(json) {
this.data = json;
this.tileset = $("<img />", { src: json.tilesets[0].image })[0]
this.tileset.onload = $.proxy(this.renderLayers, this);
},
load: function() {
return $.ajax({
url: "map.json",
dataType: "text json"
}).done($.proxy(this.loadTileset, this));
}
};
scene.load();
});
renderMap = function(){
scene.renderLayers();
}
Your understanding is right.
I suggest to draw each layer separately. With this you can draw your Player between specified layers and you also have more control over visibilty.
As you might know, in tiled you can add several layers. Load them separately in your game and draw them one by one.
So you have to split your renderLayers() in that manner that not all layers are drawn at once (layers.forEach(this.renderLayer); ) but in a given order.
Pseudo Code:
scene.renderLayer(0);
scene.renderLayer(1);
player.Draw()
scene.renderLayer(2);
I have a dynamic tile set where I do NOT want to allow panning outside of its bounds.
The below code gets me close, but the user can still scroll horizontally outside of strict bounds because it uses the map center for comparison
var strictBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(sw_lat, sw_lon),
new google.maps.LatLng(ne_lat, ne_lon)
);
google.maps.event.addListener(map, 'drag', function()
{
if (strictBounds.contains(map.getCenter())) return;
// We're out of bounds - Move the map back within the bounds
var c = map.getCenter(),
x = c.lng(),
y = c.lat(),
maxX = strictBounds.getNorthEast().lng(),
maxY = strictBounds.getNorthEast().lat(),
minX = strictBounds.getSouthWest().lng(),
minY = strictBounds.getSouthWest().lat();
if (x < minX) x = minX;
if (x > maxX) x = maxX;
if (y < minY) y = minY;
if (y > maxY) y = maxY;
map.setCenter(new google.maps.LatLng(y, x));
});
For quick access, here is the jsfiddle for this solution: http://jsfiddle.net/nYz6k/
Supposed you're happy with the current solution of detecting bounds against the center, all you have to do is to restrict the bounds based on the current map canvas size.
The result that you're getting now is that every corner of the restricting bounds appears on the center of the map when it's full restricting (i.e. both latitude and longitude exceed the bounds corners).
The solution is to actually remove the x and y offset from the bounds, and still be able to check against the center (which is the most efficient solution, compared to other bounds-based checking solutions).
Also, you have to restrict the bounds only when you initialize the map and when the window is resizing, which means that when you pan there is no extra processing overhead besides the check method that you have already provided.
Don't forget to set the maxZoom property for the map (tweak it as needed) because above a determined zoom level the restricting bounds will by themselves fit in the viewport and there's no solution for that.
Important! Use 'center_changed' instead of 'drag' because 'drag' has that sliding behavior that when you finished dragging and set the center, the maps still slides in the direction of the panning.
Before you implement the solution I recommend you to check out how the Maps API coordinate system and projection works, because this solution is heavily based on it, and if you want to tweak it out, it's useful to know this information.
https://developers.google.com/maps/documentation/javascript/maptypes and check out the Custom Map Types -> Map Coordinates section.
Here's what you need to do. First, you need to implement 2 short methods of converting between geographic coordinates (LatLng) and pixel coordinates.
var fromLatLngToPixel = function (latLng) {
var point = map.getProjection().fromLatLngToPoint(latLng);
var zoom = map.getZoom();
return new google.maps.Point(
Math.floor(point.x * Math.pow(2, zoom)),
Math.floor(point.y * Math.pow(2, zoom))
);
}
var fromPixelToLatLng = function (pixel) {
var zoom = map.getZoom();
var point = new google.maps.Point(
pixel.x / Math.pow(2, zoom),
pixel.y / Math.pow(2, zoom)
);
return map.getProjection().fromPointToLatLng(point);
}
Next, implement the method that effectively restricts the bounds. Note that you always have to keep a variable that stores the original bounds, because each time the map canvas is resized, the resulting bounds will change.
For this, let's say originalBounds keeps the bounds you provided with sw_lat, sw_long etc, and shrinkedBounds is modified by this method. You can rename it to strictBounds to still work in your method, but it's up to you. This uses jQuery for getting the width and height of the canvas object.
var shrinkBounds = function () {
zoom = map.getZoom();
// The x and y offset will always be half the current map canvas
// width and height respectively
xoffset = $('#map_canvas').width() / 2;
yoffset = $('#map_canvas').height() / 2;
// Convert the bounds extremities to global pixel coordinates
var pixswOriginal = fromLatLngToPixel(originalBounds.getSouthWest());
var pixneOriginal = fromLatLngToPixel(originalBounds.getNorthEast());
// Shrink the original bounds with the x and y offset
var pixswShrinked = new google.maps.Point(pixswOriginal.x + xoffset, pixswOriginal.y - yoffset);
var pixneShrinked = new google.maps.Point(pixneOriginal.x - xoffset, pixneOriginal.y + yoffset);
// Rebuild the shrinkedBounds object with the modified
shrinkedBounds = new google.maps.LatLngBounds(
fromPixelToLatLng(pixswShrinked),
fromPixelToLatLng(pixneShrinked));
}
Next, all you have to do is to call this method:
once when the map is initialized. Be aware that depending on when you call the method, you may be getting some weird errors because the map might not have yet initialized all it's properties. Best way is to use the projection_changed event.
google.maps.event.addListener(map, 'projection_changed', function (e) {
shrinkBounds();
});
every time the map canvas is resized
google.maps.event.addListener(map, 'resize', function (e) {
shrinkBounds();
});
But the most important part of this is that the 'resize' event is never triggered for programmatic resizes of the map container, so you have to trigger it manually every time you resize the canvas programmatically (if you do, but I doubt you do).
The most common way is to trigger it when the window is resized:
$(window).resize(function () {
google.maps.event.trigger(map, 'resize');
});
After all of this, you can now safely use your method, but as a handler for 'center_changed' and not for 'drag' as I've mentioned earlier.
I set up a jsfiddle with the full working code.
http://jsfiddle.net/nYz6k/
You can see the restricting at the bottom left corner with that little half-marker that shows up, which is positioned in the SW corner of the bounds. There is also one at the NW corner but naturally you can't see it because it's displayed above the position.
Instead of using map.getCenter() as a check, use map.getBounds()
Untested, but a simple way to accomplish this would be to check if the union of both bounds were identical (if not, your map bounds is outside your strict bounds):
var union = strictBounds.union(map.getBounds());
if (strictBounds.equals(union)) { //new map bounds is within strict bounds
Tiborg version above is the best version so far in keeping the bounds...
I noticed a bug that occurs when zoom level and width/height are larger than original bounds
I have modified his code and added a rectangle to see it in action.
var buildBounds = function(sw, ne) {
var swY = sw.lat();
var swX = sw.lng();
var neY = ne.lat();
var neX = ne.lng();
if (swY > neY) {
var cY = (swY + neY) / 2;
swY = cY;
neY = cY;
}
if (swX > neX) {
var cX = (swX + neX) / 2;
swX = cX;
neX = cX;
}
return new google.maps.LatLngBounds(
new google.maps.LatLng(swY, swX),
new google.maps.LatLng(neY, neX));
}
http://jsfiddle.net/p4wgjs6s/
In Google maps API v2, map of our country was nicely fitted to 700x400px map with the following:
map.getBoundsZoomLevel(<bounds of our country>)
But in API v3, the map.fitBounds() method doesn't fit it at that zoom level to 700x400 - it zooms out one level.
This means that map.fitBounds() counts with some "grace margin" or something.
How can I affect the size of this margin?
Here's a solution that will zoom into the map as far as possible without a custom margin. You should be able to adapt it to account for some margin if you want to.
My solution is based on this comment in a Google Groups thread. Unfortunately, the call to helper.getProjection() always returned undefined, so I adapted the above answer a bit and came up with this working code.
Just replace your existing calls to map.fitBounds(bounds) with myFitBounds(map, bounds):
function myFitBounds(myMap, bounds) {
myMap.fitBounds(bounds);
var overlayHelper = new google.maps.OverlayView();
overlayHelper.draw = function () {
if (!this.ready) {
var projection = this.getProjection(),
zoom = getExtraZoom(projection, bounds, myMap.getBounds());
if (zoom > 0) {
myMap.setZoom(myMap.getZoom() + zoom);
}
this.ready = true;
google.maps.event.trigger(this, 'ready');
}
};
overlayHelper.setMap(myMap);
}
// LatLngBounds b1, b2 -> zoom increment
function getExtraZoom(projection, expectedBounds, actualBounds) {
var expectedSize = getSizeInPixels(projection, expectedBounds),
actualSize = getSizeInPixels(projection, actualBounds);
if (Math.floor(expectedSize.x) == 0 || Math.floor(expectedSize.y) == 0) {
return 0;
}
var qx = actualSize.x / expectedSize.x;
var qy = actualSize.y / expectedSize.y;
var min = Math.min(qx, qy);
if (min < 1) {
return 0;
}
return Math.floor(Math.log(min) / Math.log(2) /* = log2(min) */);
}
// LatLngBounds bnds -> height and width as a Point
function getSizeInPixels(projection, bounds) {
var sw = projection.fromLatLngToContainerPixel(bounds.getSouthWest());
var ne = projection.fromLatLngToContainerPixel(bounds.getNorthEast());
return new google.maps.Point(Math.abs(sw.y - ne.y), Math.abs(sw.x - ne.x));
}
Now, the method fitBounds has a second parameter that represents the size of the padding. For those who want to remove it, you just need to pass 0.
Map.map.fitBounds(bounds, 0);
New method signature: fitBounds(bounds:LatLngBounds|LatLngBoundsLiteral, padding?:number)
I'm trying to do an overlay on Google Maps. I have generated tiles of my image using maptiler, but the example generated by maptiler is in v2 and i want to use v3. The example generated by maptiler is also very complex and does some unnecessary opacity stuff. Now v3 of GM has changed a lot since v2 and i have some problems to generate the LatLng of a certain point on the screen. getProjection() keeps being undefined, whatever i do, any idea how to get the projection?
<script>
var mapBounds = new google.maps.LatLngBounds();
var mapMinZoom = 8;
var mapMaxZoom = 14;
var overlay;
var maptiler = new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
if ((zoom < mapMinZoom) || (zoom > mapMaxZoom)) {
return "none.png";
}
var ymax = 1 << zoom;
var y = ymax - coord.y -1;
var tileBounds = new google.maps.LatLngBounds(
overlay.getProjection().fromDivPixelToLatLng( new google.maps.Point( (coord.x)*256, (coord.y+1)*256 ) , zoom ),
overlay.getProjection().fromDivPixelToLatLng( new google.maps.Point( (coord.x+1)*256, (coord.y)*256 ) , zoom )
);
if (mapBounds.intersects(tileBounds)) {
return "" + zoom + "/" + coord.x + "/" + (Math.pow(2,zoom)-coord.y-1) + ".png";
} else {
return "none.png";
}
},
tileSize: new google.maps.Size(256, 256),
isPng: true
});
var map;
function initialize() {
map = new google.maps.Map(document.getElementById("map_canvas"));
map.setZoom(11);
map.setMapTypeId('satellite');
mapBounds.extend(new google.maps.LatLng(50.9388615939, 3.80480816501));
mapBounds.extend(new google.maps.LatLng(51.4402541425, 4.73612507791));
map.fitBounds(mapBounds);
overlay = new google.maps.OverlayView();
overlay.draw = function() {};
overlay.setMap(map);
map.overlayMapTypes.insertAt(0, maptiler);
}
</script>
the overlay part is a hack i found on the internet which supposed to get you to the projection. unfortunately it didn't work. Any idea's how to fix this? in V2 you could do something like this:
var mercator = new GMercatorProjection()
mercator.fromPixelToLatLng( new GPoint( (tile.x)*256, (tile.y+1)*256 ) , zoom )
But this isn't possible anymore in v3.
Anyone that can help?
The example generated by maptiler can be found here:
http://gmapsexample.staging1.kunstmaan.be/googlemapsv2.html
This is a simple example in v3 which works:
http://gmapsexample.staging1.kunstmaan.be/googlemapsv3_simple.html
but i want everything except the map to be a specific color, so this is the example i'm trying to get working:
http://gmapsexample.staging1.kunstmaan.be/googlemapsv3.html
thanks,
Daan
You do not need to worry about getting or setting the projection if you are using MapTiler
These two YouTube videos will walk you through (among other things) using MapTiler with Google Maps API v3 map styled to be a certain color which is (if I understand correctly) exactly what you're asking about:
http://www.youtube.com/watch?v=CeSFUSZLeao
http://www.youtube.com/watch?v=WqSOLca2xOc
Try this as your Mercator function:
function GMercatorProjection() {
this.pixelOrigin_ = new google.maps.Point(tileSize / 2, tileSize / 2);
this.pixelsPerLonDegree_ = tileSize / 360;
this.pixelsPerLonRadian_ = tileSize / (2 * Math.PI)
}
Check the documentation about custom overlays in API3
https://code.google.com/apis/maps/documentation/javascript/overlays.html#CustomOverlays
Is there a way to set a max zoom level for fitBounds()?
My problem is that when the map is only fed one location, it zooms in as far as it can go, which really takes the map out of context and renders it useless. Perhaps I am taking the wrong approach?
I like mrt's solution (especially when you don't know how many points you will be mapping or adjusting for), except it throws the marker off so that it isn't in the center of the map anymore. I simply extended it by an additional point subtracting .01 from the lat and lng as well, so it keeps the marker in the center. Works great, thanks mrt!
// Pan & Zoom map to show all markers
function fitToMarkers(markers) {
var bounds = new google.maps.LatLngBounds();
// Create bounds from markers
for( var index in markers ) {
var latlng = markers[index].getPosition();
bounds.extend(latlng);
}
// Don't zoom in too far on only one marker
if (bounds.getNorthEast().equals(bounds.getSouthWest())) {
var extendPoint1 = new google.maps.LatLng(bounds.getNorthEast().lat() + 0.01, bounds.getNorthEast().lng() + 0.01);
var extendPoint2 = new google.maps.LatLng(bounds.getNorthEast().lat() - 0.01, bounds.getNorthEast().lng() - 0.01);
bounds.extend(extendPoint1);
bounds.extend(extendPoint2);
}
map.fitBounds(bounds);
// Adjusting zoom here doesn't work :/
}
You can setup your map with maxZoom in the MapOptions (api-reference) like this:
var map = new google.maps.Map(document.getElementById("map"), { maxZoom: 10 });
This would keep the map from zooming any deeper when using fitBounds() and even removes the zoom levels from the zoom control.
Another solution is to expand bounds if you detect they are too small before you execute fitBounds():
var bounds = new google.maps.LatLngBounds();
// here you extend your bound as you like
// ...
if (bounds.getNorthEast().equals(bounds.getSouthWest())) {
var extendPoint = new google.maps.LatLng(bounds.getNorthEast().lat() + 0.01, bounds.getNorthEast().lng() + 0.01);
bounds.extend(extendPoint);
}
map.fitBounds(bounds);
Once you've added all of the real bounds add these lines
var offset = 0.002;
var center = bounds.getCenter();
bounds.extend(new google.maps.LatLng(center.lat() + offset, center.lng() + offset));
bounds.extend(new google.maps.LatLng(center.lat() - offset, center.lng() - offset));
it get the center of the real bounds then adds two additional points one to the northeast and one to the southwest of you center
This effectively sets the minimum zoom, change the value of offset to increase or decrease the zoom
var map = new google.maps.Map(document.getElementById("map"), { maxZoom: 10 });
Using the MaxZoom option works best for not zooming to close on to the marks you have.
If it is for a single location, you can use setCenter() and setZoom() instead.
u can use
map.setOptions({
maxZoom: [what u want],
minZoom: [what u want]
});
this way u set the properties of the map after the map has been initialized .... u can set them as many times as u want ... but in ur case ... u can set them before fitBounds()
good luck,
rarutu
The way I prevent the map from zooming in to far is by adding this line of code:
var zoomOverride = map.getZoom();
if(zoomOverride > 15) {
zoomOverride = 15;
}
map.setZoom(zoomOverride);
Directly after this line:
map.setCenter(bounds.getCenter(), map.getBoundsZoomLevel(bounds));
Feel free to change the zoom level to whatever level you don’t want the map to zoom past.
If you have any problems or questions, just leave me a comment on the blog post I wrote about this at http://icode4you.net/creating-your-own-store-locator-map-how-to-prevent-the-map-from-zooming-in-too-close-on-a-single-marker
I really like mrt's solution and it works perfectly if you've always only have one point to work with. I did however find that if the bounding box was not based on one point, but the points were very close together, this could still cause the map to be zoomed in too far.
Here's a way to first check if the points are within a defined distance of each other, then if they are smaller than that minimum distance, extend the bounds by that minimum distance:
var bounds = new google.maps.LatLngBounds();
// here you extend your bound as you like
// ...
var minDistance = 0.002;
var sumA = bounds.getNorthEast().lng() - bounds.getSouthWest().lng();
var sumB = bounds.getNorthEast().lat() - bounds.getSouthWest().lat();
if((sumA < minDistance && sumA > -minDistance)
&& (sumB < minDistance && sumB > -minDistance)){
var extendPoint1 = new google.maps.LatLng(bounds.getNorthEast().lat() + minDistance, bounds.getNorthEast().lng() + minDistance);
var extendPoint2 = new google.maps.LatLng(bounds.getNorthEast().lat() - minDistance, bounds.getNorthEast().lng() - minDistance);
bounds.extend(extendPoint1);
bounds.extend(extendPoint2);
}
Hope this helps someone!
As for me guys i solve it by creating an idle event after fitBounds. Working perfectly. Guess that's one of the most clean solutions here
var locations = [['loc', lat, lng], ['loc', lat, lng]];
.....
for (i = 0; i < locations.length; i++) {
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 10
});
.... create markers, etc.
}
....
map.fitBounds(bounds);
google.maps.event.addListenerOnce(map, 'idle', function() {
if (locations.length == 1) {
map.setZoom(11);
}
});
This gives you a direct control upon max allowed zoom on bounds fitting.
var fitToMarkers = function(map, markers, maxZoom) {
if (typeof maxZoom == 'undefined') maxZoom = 15;
google.maps.event.addListenerOnce(map, 'bounds_changed', function(event) {
if (this.getZoom() > maxZoom) {
this.setZoom(maxZoom);
}
});
var bounds = new google.maps.LatLngBounds();
for (var m = 0; m < markers.length; m++) {
var marker = markers[m];
var latlng = marker.getPosition();
bounds.extend(latlng);
}
map.fitBounds(bounds);
};
I solved with this chunk, since Google Maps V3 is event driven:
you can tell the API to set back the zoom to a proper amount when the zoom_changed event triggers:
var initial = true
google.maps.event.addListener(map, "zoom_changed", function() {
if (intial == true){
if (map.getZoom() > 11) {
map.setZoom(11);
intial = false;
}
}
});
I used intial make the map not zooming too much loading when the eventual fitBounds permorfed, without it any zoom event over 11 would be impossible for the user.
After calling fitBounds() method, try to setup zoom level again. It will force the map to be at that zoom level whilst being centered at the right place.
I have soulution based on limiting max zoom when fitting bounds. Works for me (tested on Win 7 - IE 9, FF 13, Chrome 19):
// When fitting bounds:
var bounds = new google.maps.LatLngBounds();
// ...
// extend bounds as you like
// ..
// now set global variable when fitting bounds
window.fittingBounds = true;
map.fitBounds(bounds);
window.fittingBounds = false;
// attach this event listener after map init
google.maps.event.addListener(map, 'zoom_changed', function() {
// set max zoom only when fitting bounds
if (window.fittingBounds && map.getZoom() > 16) {
this.setZoom(16);
}
});
And .. here is another one.
Same idea as mrt and Ryan, but
also works if bounds size is not exactly zero (*)
prevents distortion near the poles
uses getCenter() instead of getNorthEast()
(*) Note: If the box is already big enough, then adding those two extra points should have no effect. So we don't need any further checking.
function calcBounds(markers) {
// bounds that contain all markers
var bounds = new google.maps.LatLngBounds();
// Using an underscore _.each(). Feel free to replace with standard for()
_.each(markers, function(marker) {
bounds.extend(marker.getPosition());
});
// prevent lat/lng distortion at the poles
var lng0 = bounds.getNorthEast().lng();
var lng1 = bounds.getSouthWest().lng();
if (lng0 * lng1 < 0) {
// Take the cos at the equator.
var cos = 1;
}
else {
var cos0 = Math.cos(lng0);
var cos1 = Math.cos(lng1);
// Prevent division by zero if the marker is exactly at the pole.
var cos_safe = Math.max(cos0, cos1, 0.0001);
}
var cos0 = Math.cos(bounds.getNorthEast.lng() * Math.PI / 180);
var cos1 = Math.cos(bounds.getSouthWest.lng() * Math.PI / 180);
// "radius" in either direction.
// 0.0006 seems to be an ok value for a typical city.
// Feel free to make this value a function argument.
var rLat = 0.0006;
var rLng = rLat / cos_safe;
// expand the bounds to a minimum width and height
var center = bounds.getCenter();
var p0 = new google.maps.LatLng(center.lat() - rLat, center.lng() - rLng);
var p1 = new google.maps.LatLng(lat.center() + rLat, center.lng() + rLng);
bounds.extend(p0);
bounds.extend(p1);
return bounds;
}
EDIT: I am not exactly sure if my ratio calculation correctly, considering we have a Mercator projection. I might re-edit this..
Its already answered here Google Maps v3: Enforcing min. zoom level when using fitBounds it works as expected :) so now if after fit bounds zoom is less then lets say 13 then you can set new zoom which you preffer
Here is my go at a solution, which also works when two markers are very close. The effective maximum zoom level is the same in both situations. So we do not end up zooming unneccesarily out, when there are more than one marker
The effect, again is ensuring a maximum zoom, without using the maxZoom option, which has the probably unwanted effect of making it impossible for the user to zoom further than the maxZoom level with the zoom control
I have calculated maxLat, minLat, maxLng and minLng beforehand...
var minLatSpan = 0.002;
if (maxLat - minLat < minLatSpan) {
// ensures that we do not zoom in too much
var delta = (minLatSpan - (maxLat - minLat)) / 2;
maxLat += delta;
minLat -= delta;
}
map.fitBounds({
east: maxLng,
west: minLng,
north: maxLat,
south: minLat,
});