How can I adjust the zoom on Open Layers for viewing all POI and Markers on the map - zooming

I need to show all POI and Markers in a Open Layers map with the best zoom defined.
How can I get that?
I need the best zoom to show all markers on view.

function setMapCenter() {
var layers = multiMapa.getLayers();
//that's it. It's started inverted to be adjusted on first iteration below.
var minx = 9999999999;
var miny = 9999999999;
var maxx = -9999999999;
var maxy = -9999999999;
var extent = [];
layers.forEach(function (lay) {
if (lay instanceof ol.layer.Vector) {
vehicleLayer = lay;
var features = lay.getSource().getFeatures();
features.forEach(function (fea) {
//all vectors, in this case, are Points.
var coords = String(fea.getGeometry().getExtent()).split(",");
if (minx > coords[0]) {
minx = coords[0];
}
if (miny > coords[1]) {
miny = coords[1];
}
if (maxx < coords[0]) {
maxx = coords[0];
}
if (maxy < coords[1]) {
maxy = coords[1];
}
});
//console.log(lay.getStyle().getText().getText());
}
});
console.log([minx, miny, maxx, maxy]);
extent.push(minx);
extent.push(miny);
extent.push(maxx);
extent.push(maxy);
multiMapa.getView().fit(extent, multiMapa.getSize()); //[minx, miny, maxx, maxy]);
}

If all your features are on the same layer, get your vector's extent like;
const extent = yourLayer.getExtent();
Then do this;
map.getView().fit(extent);
If they are on separate layers, you can either jam all of the features in a separate layer and get the extent that way or you can combine all of the features extents like this;
let featExtent = featuresOfInterest[0].getGeometry().getExtent();
for (let i = 0; i < featuresOfInterest.length; i++) {
ol.extent.extend(featExtent, featuresOfInterest[i].getGeometry().getExtent());
}
Then use the fit function of the view again like;
map.getView().fit(featExtent);

Related

Merge or Use instance and Native render make no difference with Chrome plus Integrated Graphics Card [duplicate]

I am testing the FPS with my laptop using the Intel(R) Iris(R) Plus Graphics 655 card.
To test the threeJS example with Instance rendering and merge-drawcall rendering.
So I used both the QRCode_buffergeometry.json model and the suzanne_buffergeometry.json model.
for the QRCode_buffergeometry.json: vertex:12852, face: 4284
and for the suzanne_buffergeometry.json: vertex:1515 face: 967
Then the FPS for the suzanne_buffergeometry with 8000 count:
INSTANCE: 36
MERGED: 43
NATIVE: from 23 to 35 by rotation
for the QRCode_buffergeometry model with 8000 count:
INSTANCE: 9
MERGED: 15-17
NATIVE: 17-19
I am very confused with this performance.
1. As far as my understanding, with no matter if i use instance or merge-drawcall, the drawcall is fixed to be 1 and the total face number to draw is same, why merged-drawcall is better than instance? Since the face and vertex number are both same, I suppose what happened in the vertex shader for transform the vertex should be same too, so why merged is faster?
For the QRCode_buffergeometry model, native is almost same as merged, and better than instance, so I guess the CPU is not the bottle neck but the GPU is, however the final drawing data should be same, i mean eventually the face number to be draw should be same, why native is faster?, isn't that the instance is supposed to be the best way? I am pretty sure the camera's far and near is big enough, so there should not be any culling issue.
When I am trying to optimize some big scene, when should I pick merge? when to pick instance? and when maybe no doing anything is better?
Any help?
Thanks a lot~~~
Attached the code for the sample is here
body { margin: 0; }
<div id="container"></div>
<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three#0.112.1/build/three.module.js';
import Stats from 'https://cdn.jsdelivr.net/npm/three#0.112.1/examples/jsm/libs/stats.module.js';
import {
GUI
} from 'https://cdn.jsdelivr.net/npm/three#0.112.1/examples/jsm/libs/dat.gui.module.js';
import {
OrbitControls
} from 'https://cdn.jsdelivr.net/npm/three#0.112.1/examples/jsm/controls/OrbitControls.js';
import {
BufferGeometryUtils
} from 'https://cdn.jsdelivr.net/npm/three#0.112.1/examples/jsm/utils/BufferGeometryUtils.js';
var container, stats, gui, guiStatsEl;
var camera, controls, scene, renderer, material;
// gui
var Method = {
INSTANCED: 'INSTANCED',
MERGED: 'MERGED',
NAIVE: 'NAIVE'
};
var api = {
method: Method.INSTANCED,
mesh_number: 1,
count_per_mesh: 1000
};
var modelName = 'suzanne_buffergeometry.json';
var modelScale = (modelName === 'suzanne_buffergeometry.json' ? 1 : 0.01);
var modelVertex = (modelName === 'suzanne_buffergeometry.json' ? 1515 : 12852);
var modelFace = (modelName === 'suzanne_buffergeometry.json' ? 967 : 4284);
//
init();
initMesh();
animate();
//
function clean() {
var meshes = [];
scene.traverse(function(object) {
if (object.isMesh) meshes.push(object);
});
for (var i = 0; i < meshes.length; i++) {
var mesh = meshes[i];
mesh.material.dispose();
mesh.geometry.dispose();
scene.remove(mesh);
}
}
var randomizeMatrix = function() {
var position = new THREE.Vector3();
var rotation = new THREE.Euler();
var quaternion = new THREE.Quaternion();
var scale = new THREE.Vector3();
return function(matrix) {
position.x = Math.random() * 40 - 20;
position.y = Math.random() * 40 - 20;
position.z = Math.random() * 40 - 20;
rotation.x = Math.random() * 2 * Math.PI;
rotation.y = Math.random() * 2 * Math.PI;
rotation.z = Math.random() * 2 * Math.PI;
quaternion.setFromEuler(rotation);
scale.x = scale.y = scale.z = Math.random() * modelScale;
matrix.compose(position, quaternion, scale);
};
}();
function initMesh() {
clean();
console.time(api.method + ' (build)');
for (var i = 0; i < api.mesh_number; i++) {
// make instances
new THREE.BufferGeometryLoader()
.setPath('https://threejs.org/examples/models/json/')
.load(modelName, function(geometry) {
material = new THREE.MeshNormalMaterial();
geometry.computeVertexNormals();
switch (api.method) {
case Method.INSTANCED:
makeInstanced(geometry);
break;
case Method.MERGED:
makeMerged(geometry);
break;
case Method.NAIVE:
makeNaive(geometry);
break;
}
});
}
console.timeEnd(api.method + ' (build)');
var drawCalls = 0;
switch (api.method) {
case Method.INSTANCED:
case Method.MERGED:
drawCalls = api.mesh_number;
break;
case Method.NAIVE:
drawCalls = api.mesh_number * api.count_per_mesh;
break;
}
guiStatsEl.innerHTML = [
'<i>GPU draw calls</i>: ' + drawCalls,
'<i>Face Number</i>: ' + (modelFace * api.mesh_number * api.count_per_mesh),
'<i>Vertex Number</i>: ' + (modelVertex * api.mesh_number * api.count_per_mesh)
].join('<br/>');
}
function makeInstanced(geometry, idx) {
var matrix = new THREE.Matrix4();
var mesh = new THREE.InstancedMesh(geometry, material, api.count_per_mesh);
for (var i = 0; i < api.count_per_mesh; i++) {
randomizeMatrix(matrix);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
}
function makeMerged(geometry, idx) {
var instanceGeometry;
var geometries = [];
var matrix = new THREE.Matrix4();
for (var i = 0; i < api.count_per_mesh; i++) {
randomizeMatrix(matrix);
var instanceGeometry = geometry.clone();
instanceGeometry.applyMatrix(matrix);
geometries.push(instanceGeometry);
}
var mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
scene.add(new THREE.Mesh(mergedGeometry, material));
}
function makeNaive(geometry, idx) {
var matrix = new THREE.Matrix4();
for (var i = 0; i < api.count_per_mesh; i++) {
randomizeMatrix(matrix);
var mesh = new THREE.Mesh(geometry, material);
mesh.applyMatrix(matrix);
scene.add(mesh);
}
}
function init() {
var width = window.innerWidth;
var height = window.innerHeight;
// camera
camera = new THREE.PerspectiveCamera(70, width / height, 1, 100);
camera.position.z = 30;
// renderer
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
renderer.outputEncoding = THREE.sRGBEncoding;
container = document.getElementById('container');
container.appendChild(renderer.domElement);
// scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
// controls
controls = new OrbitControls(camera, renderer.domElement);
controls.autoRotate = true;
// stats
stats = new Stats();
container.appendChild(stats.dom);
// gui
gui = new GUI();
gui.add(api, 'method', Method).onChange(initMesh);
gui.add(api, 'count_per_mesh', 1, 20000).step(1).onChange(initMesh);
gui.add(api, 'mesh_number', 1, 200).step(1).onChange(initMesh);
var perfFolder = gui.addFolder('Performance');
guiStatsEl = document.createElement('li');
guiStatsEl.classList.add('gui-stats');
perfFolder.__ul.appendChild(guiStatsEl);
perfFolder.open();
// listeners
window.addEventListener('resize', onWindowResize, false);
Object.assign(window, {
scene
});
}
//
function onWindowResize() {
var width = window.innerWidth;
var height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
stats.update();
render();
}
function render() {
renderer.render(scene, camera);
}
//
function getGeometryByteLength(geometry) {
var total = 0;
if (geometry.index) total += geometry.index.array.byteLength;
for (var name in geometry.attributes) {
total += geometry.attributes[name].array.byteLength;
}
return total;
}
// Source: https://stackoverflow.com/a/18650828/1314762
function formatBytes(bytes, decimals) {
if (bytes === 0) return '0 bytes';
var k = 1024;
var dm = decimals < 0 ? 0 : decimals;
var sizes = ['bytes', 'KB', 'MB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
</script>
This is only guesses
Three.js by default culls if things are outside the frustum.
We can turn this off with mesh.frustumCulled = false. I didn't notice a difference and this should show up in the draw count.
Three.js by default sorts opaque objects back to front.
This means everything else being equal, sorted will run faster
than unsorted because of the depth test. If I set the depth test
to always
material.depthFunc = THREE.AlwaysDepth
Then I seem to get slightly faster rendering with instanced vs native. Of course
everything else is not equal.
An issue in Chrome.
If I run in Firefox or Safari I get the expected results. Merged > Instanced > Native
It could be a bug or it could be they're working around a driver or
security issue that the other browsers are not. You'd have to ask.

Picking under elements of an element in Autodesk Forge viewer

I would like to accomplish a feature that I can do in Three.js but cannot in Autodesk Forge viewer. Here is the link to test: http://app.netonapp.com/JavaScript/Three.js/select_inner_objects.html
The requirement is to select objects inside an object. This job can be done with THREE.Raycaster in the above demo, to use a raycaster to detect all elements which are on the line the ray going through. Then I can get objects behind or inner another object.
I tried this concept in Autodesk Forge viewer but having no success. Here is the code:
// Change this to:
// true to use original Three.js
// false to use Autodesk Forge Viewer API
var useThreeJS = true;
var container = $('div.canvas-wrap')[0];
container.addEventListener('mousedown', function (event) {
if (useThreeJS) {
var canvas = _viewer.impl.canvas;
var containerWidth = canvas.clientWidth;
var containerHeight = canvas.clientHeight;
var camera = _viewer.getCamera();
var mouse = mouse || new THREE.Vector3();
var raycaster = raycaster || new THREE.Raycaster();
mouse.x = 2 * (event.clientX / containerWidth) - 1;
mouse.y = 1 - 2 * (event.clientY / containerHeight);
mouse.unproject(camera);
raycaster.set(camera.position, mouse.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects(objects);
if (intersects.length == 1) {
var obj = intersects[0].object;
obj.material.color.setRGB(1.0 - i / intersects.length, 0, 0);
} else if (intersects.length > 1) {
// Exclude the first which is the outer object (i == 0)
for (var i = 1; i < intersects.length; i++) {
var obj = intersects[i].object;
obj.material.color.setRGB(1.0 - i / intersects.length, 0, 0);
}
}
} else {
var vp = _viewer.impl.clientToViewport(event.canvasX, event.canvasY);
var renderer = _viewer.impl.renderer();
var dbId = renderer.idAtPixel(vp.x, vp.y);
if (dbId) {
console.debug("Selected Id: " + dbId);
_viewer.select(dbId);
_viewer.impl.invalidate(true);
}
}
}, false);
I found the Forge viewer has viewer.impl.renderer().idAtPixel method which is great to get an element at the picking pixel. However, I want it to do more, to select all elements (which are under or nested) at the picking pixel. How I can do it with the Forge Viewer API?
Based on the suggestion of Zhong Wu in another post, here is the final solution to select element which is under or inside another element. I created an Autodesk Forge viewer extension to use it easily.
///////////////////////////////////////////////////////////////////////////////
// InnerSelection viewer extension
// by Khoa Ho, December 2016
//
///////////////////////////////////////////////////////////////////////////////
AutodeskNamespace("Autodesk.ADN.Viewing.Extension");
Autodesk.ADN.Viewing.Extension.InnerSelection = function (viewer, options) {
Autodesk.Viewing.Extension.call(this, viewer, options);
var _self = this;
var _container = viewer.canvas.parentElement;
var _renderer = viewer.impl.renderer();
var _instanceTree = viewer.model.getData().instanceTree;
var _fragmentList = viewer.model.getFragmentList();
var _eventSelectionChanged = false;
var _viewport;
var _outerDbId;
_self.load = function () {
_container.addEventListener('mousedown',
onMouseDown);
viewer.addEventListener(
Autodesk.Viewing.SELECTION_CHANGED_EVENT,
onItemSelected);
console.log('Autodesk.ADN.Viewing.Extension.InnerSelection loaded');
return true;
};
_self.unload = function () {
_container.removeEventListener('mousedown',
onMouseDown);
viewer.removeEventListener(
Autodesk.Viewing.SELECTION_CHANGED_EVENT,
onItemSelected);
console.log('Autodesk.ADN.Viewing.Extension.InnerSelection unloaded');
return true;
};
function onMouseDown(e) {
var viewport = viewer.impl.clientToViewport(e.canvasX, e.canvasY);
_viewport = viewport; // Keep this viewport to use in onItemSelected()
var dbId = _renderer.idAtPixel(viewport.x, viewport.y);
if (_outerDbId == dbId) {
_outerDbId = -1;
// Deselect everything
viewer.select();
} else {
_outerDbId = dbId;
// Hide outer element temporarily to allow picking its behind element
viewer.hideById(dbId);
_eventSelectionChanged = true;
}
viewer.impl.sceneUpdated(true);
}
function onItemSelected(e) {
if (_eventSelectionChanged) {
// Prevent self looping on selection
_eventSelectionChanged = false;
// Show outer element back
viewer.show(_outerDbId);
// Get inner element Id after the outer element
// was just hidden on mouse down event
var innerDbId = _renderer.idAtPixel(_viewport.x, _viewport.y);
if (innerDbId > -1) {
// Select the inner element when it is found
viewer.select(innerDbId);
console.debug("Selected inner Id: " + innerDbId);
} else if (_outerDbId > -1) {
// Select the outer element if the inner element is not found
viewer.select(_outerDbId);
console.debug("Selected outer Id: " + _outerDbId);
}
}
}
};
Autodesk.ADN.Viewing.Extension.InnerSelection.prototype =
Object.create(Autodesk.Viewing.Extension.prototype);
Autodesk.ADN.Viewing.Extension.InnerSelection.prototype.constructor =
Autodesk.ADN.Viewing.Extension.InnerSelection;
Autodesk.Viewing.theExtensionManager.registerExtension(
'Autodesk.ADN.Viewing.Extension.InnerSelection',
Autodesk.ADN.Viewing.Extension.InnerSelection);
As of now (Dec/16), when you select using mouse click, the Viewer will not ignore transparent elements, so it will select an element even if it is transparent. Below is a code I used to track what's under the cursor, maybe can be useful.
// use jQuery to bind a mouve move event
$(_viewer.container).bind("mousemove", onMouseMove);
function onMouseMove(e) {
var screenPoint = {
x: event.clientX,
y: event.clientY
};
var n = normalize(screenPoint);
var dbId = /*_viewer.utilities.getHitPoint*/ getHitDbId(n.x, n.y);
//
// use the dbId somehow...
//
}
// This is a built-in method getHitPoint, but the original returns
// the hit point, so this modified version returns the dbId
function getHitDbId(){
y = 1.0 - y;
x = x * 2.0 - 1.0;
y = y * 2.0 - 1.0;
var vpVec = new THREE.Vector3(x, y, 1);
var result = _viewer.impl.hitTestViewport(vpVec, false);
//return result ? result.intersectPoint : null; // original implementation
return result ? result.dbId : null;
}
function normalize(screenPoint) {
var viewport = _viewer.navigation.getScreenViewport();
var n = {
x: (screenPoint.x - viewport.left) / viewport.width,
y: (screenPoint.y - viewport.top) / viewport.height
};
return n;
}
I see method viewer.impl.renderer().idAtPixel works better than viewer.impl.hitTestViewport to select element on mouse pick. The first one can click through the hidden/ghost element to get the objectId of element behind. While the second cannot. Here is the code to test:
var container = $('div.canvas-wrap')[0];
container.addEventListener('mousedown', function (event) {
var clickThroughHiddenElement = true;
if (clickThroughHiddenElement) {
var vp = _viewer.impl.clientToViewport(event.canvasX, event.canvasY);
var renderer = _viewer.impl.renderer();
var dbId = renderer.idAtPixel(vp.x, vp.y);
if (!!dbId) {
_viewer.select(dbId);
}
console.debug("Selected Id: " + dbId);
} else {
var screenPoint = {
x: event.clientX,
y: event.clientY
};
var viewport = _viewer.navigation.getScreenViewport();
var x = (screenPoint.x - viewport.left) / viewport.width;
var y = (screenPoint.y - viewport.top) / viewport.height;
// Normalize point
x = x * 2.0 - 1.0;
y = (1.0 - y) * 2.0 - 1.0;
var vpVec = new THREE.Vector3(x, y, 1);
var result = _viewer.impl.hitTestViewport(vpVec, false);
if (!!result) {
var dbId = result.dbId;
_viewer.select(dbId);
console.debug("Selected Id: " + dbId);
}
}
}
However, they are not what I want, to click through transparent element to get elements behind. If user selects transparent element, it will be selected. If user selects inner elements, it will ignore outer transparent element to select the pick inner element.
I check Forge viewer uses THREE.Raycaster with element bounding box to detect intersection on mouse click. It seems my problem is doable with the Forge viewer like it does in my Three.js demo.

AS3: Random Point on Irregular Shape

I have a MovieClip holding an irregular shape such as this one:
I need to generate a random point on this shape.
I can use brute force by generating points within the bounding box and then hitTesting to see if they reside on the irregular shape. However, I'm sure there's a more efficient way to tackle this problem.
What is the most efficient way to generate a random point on an irregular shape?
You mentioned hitTest, but I assume you meant hitTestPoint().
If so, a function go get the random points you mention, would look a bit like this:
function getRandomPointsInClip(target:MovieClip,numPoints:int):Vector.<Point>{
var points:Vector.<Point> = new Vector.<Point>(numPoints,true);
var width:Number = target.width,height:Number = target.height;
for(var i:int = 0; i < numPoints ; i++){
var point:Point = new Point(target.x+Math.random() * width,target.y+Math.random() * height);
if(target.hitTestPoint(point.x,point.y,true)) points[i] = point;//is the random coord inside ?
else i = i-1;//nope, go back one step - > retry above until it is inside
}
return points;
}
The other I hinted at in my comment involves looping through non transparent pixels in a bitmap data of your object. This method would insure you don't have many duplicates, as opposed to the previous method, but it also means, you have less control over the number of points created and there's extra memory used for creating the bitmap. Still, for documentation purposes, here is the function:
function getGridPointsInClip(target:MovieClip,res:int,offset:Number = 3):Vector.<Point>{
var points:Vector.<Point> = new Vector.<Point>();
var x:int,y:int,alpha:int,w:int = int(target.width),h:int = int(target.height);
var bmd:BitmapData = new BitmapData(w,h,true,0x00FFFFFF);bmd.draw(target);
var pixels:Vector.<uint> = bmd.getVector(bmd.rect),numPixels:int = w*h;
for(var i:int = 0; i < numPixels; i+=res) {
x = i%bmd.width;
y = int(i/bmd.width);
alpha = pixels[i] >>> 24;
if(alpha > 0) points.push(new Point(x+random(-offset,offset),y+random(-offset,offset)));
}
return points;
}
function random(from:Number,to:Number):Number {
if (from >= to) return from;
var diff:Number = to - from;
return (Math.random()*diff) + from;
}
And here'a very basic test:
var pts:Vector.<Point> = getRandomPointsInClip(mc,300);
//var pts:Vector.<Point> = getGridPointsInClip(mc,100,4);
for(var i:int = 0 ; i < pts.length; i++) drawCircle(pts[i].x,pts[i].y,3,0x009900);
function getRandomPointsInClip(target:MovieClip,numPoints:int):Vector.<Point>{
var points:Vector.<Point> = new Vector.<Point>(numPoints,true);
var width:Number = target.width,height:Number = target.height;
for(var i:int = 0; i < numPoints ; i++){
var point:Point = new Point(target.x+Math.random() * width,target.y+Math.random() * height);
if(target.hitTestPoint(point.x,point.y,true)) points[i] = point;//is the random coord inside ?
else i = i-1;//nope, go back one step - > retry above until it is inside
}
return points;
}
function getGridPointsInClip(target:MovieClip,res:int,offset:Number = 3):Vector.<Point>{
var points:Vector.<Point> = new Vector.<Point>();
var x:int,y:int,alpha:int,w:int = int(target.width),h:int = int(target.height);
var bmd:BitmapData = new BitmapData(w,h,true,0x00FFFFFF);bmd.draw(target);
var pixels:Vector.<uint> = bmd.getVector(bmd.rect),numPixels:int = w*h;
for(var i:int = 0; i < numPixels; i+=res) {
x = i%bmd.width;
y = int(i/bmd.width);
alpha = pixels[i] >>> 24;
if(alpha > 0) points.push(new Point(x+random(-offset,offset),y+random(-offset,offset)));
}
return points;
}
function random(from:Number,to:Number):Number {
if (from >= to) return from;
var diff:Number = to - from;
return (Math.random()*diff) + from;
}
function drawCircle(x:Number,y:Number,radius:Number,color:uint):void{
graphics.lineStyle(1,color);
graphics.drawCircle(x-radius,y-radius,radius);
}
HTH
If you think of some non-blob like shapes, it's clear the check random pixel, try again method isn't really a good way. The bounding box area could be huge compared to the shape area.
What you could do to improve the effectiveness is getting a vector of the BitmapData of the shape. It should contain all pixels of the bounding box. Update - it would be nice now if we could pick a random point, and remove it from the vector if it isn't inside the shape. Unfortunately the vector only contains the pixels' colour, not the position which is implicit and only correct if we don't change the vector's length. Since we don't need to know the actual colour, we can omit all transparent pixels and store an inside pixel's position as it's value in the vector. This way we don't need to create a new object for each pixel of the shape (that would be quite expensive!).
var v:Vector.<uint> shapeBoxBitmap.getVector(shapeBoxBitmap.rect);
var pixelNum:int = v.length;
for(var i:uint = 0; i < pixelNum; i++) {
if( v[i] && 0xFF000000 == 0) { // transparent pixel, outside off shape
v.splice(i,1);
} else {
v[i] = i;
}
}
//get random point
var randomPixel:int = v[Math.floor(Math.random()*v.length)];
var point:Point = new Point(randomPixel%shapeBitmap.width,int(randomPixel/shapeBitmap.width));

Google Maps API V3: limit map bounds [duplicate]

This question already has answers here:
How do I limit panning in Google maps API V3?
(15 answers)
Closed 8 years ago.
I'm trying to set bounds where you can drag the map using Google Maps API V3
Here is the solution for V2 http://econym.org.uk/gmap/example_range.htm that works pretty well.
However with API V3 it isn't so good: when you use the same checkbounds() function, the map is twitching when you reach the bound while map.setCenter() changes the center of the map.
How to fix it? What is the solution for API V3?
I've had the same problem, but this should sort it out (it's the same function, the event to listen to is changed from 'move' or 'drag' to 'center_changed', works like a charm!:
google.maps.event.addListener(map,'center_changed',function() { checkBounds(); });
function checkBounds() {
if(! allowedBounds.contains(map.getCenter())) {
var C = map.getCenter();
var X = C.lng();
var Y = C.lat();
var AmaxX = allowedBounds.getNorthEast().lng();
var AmaxY = allowedBounds.getNorthEast().lat();
var AminX = allowedBounds.getSouthWest().lng();
var AminY = allowedBounds.getSouthWest().lat();
if (X < AminX) {X = AminX;}
if (X > AmaxX) {X = AmaxX;}
if (Y < AminY) {Y = AminY;}
if (Y > AmaxY) {Y = AmaxY;}
map.setCenter(new google.maps.LatLng(Y,X));
}
}
You might also need to consider wrapping coordinates, curve distortion, and centerizing to the bound dimensions if the map resizes or zooms in/out. This is especially required if your bounds takes up a large percentage of the entire map (eg. like a continent).
One of the problems with checkBounds() though, is that it doesn't take into account that latitude values close to the north/south poles, which have non-linear distortion which make limiting the bounds in-accurate (I use approximate magic number multipliers that won't work in all situations). By right, you should first convert the bounds to linear 2d world coordinates to see how far off the bounds it is in terms of world coordinates, than map the actual target center point in world coordinate to the target actual latitude position. For longitude values, this doesn't seem like much of issue and the linear clipping approach seems accurate enough, the main issue is with the wrapping of longitude coordinates which is accounted for (somewhat) in the below code.
// Persitant variables
var allowedBounds; // assign something here
var lastValidCenter; // initialize this using map.getCenter()
function checkBounds() { // when bounds changes due to resizing or zooming in/out
var currentBounds = map.getBounds();
if (currentBounds == null) return;
var allowed_ne_lng = allowedBounds.getNorthEast().lng();
var allowed_ne_lat = allowedBounds.getNorthEast().lat();
var allowed_sw_lng = allowedBounds.getSouthWest().lng();
var allowed_sw_lat = allowedBounds.getSouthWest().lat();
var wrap;
var cc = map.getCenter();
var centerH = false;
var centerV = false;
// Check horizontal wraps and offsets
if ( currentBounds.toSpan().lng() > allowedBounds.toSpan().lng() ) {
centerH = true;
}
else { // test positive and negative wrap respectively
wrap = currentBounds.getNorthEast().lng() < cc.lng();
var current_ne_lng = !wrap ? currentBounds.getNorthEast().lng() : allowed_ne_lng +(currentBounds.getNorthEast().lng() + 180 ) + (180 - allowed_ne_lng);
wrap = currentBounds.getSouthWest().lng() > cc.lng();
var current_sw_lng = !wrap ? currentBounds.getSouthWest().lng() : allowed_sw_lng - (180-currentBounds.getSouthWest().lng()) - (allowed_sw_lng+180);
}
// Check vertical wraps and offsets
if ( currentBounds.toSpan().lat() > allowedBounds.toSpan().lat() ) {
centerV = true;
}
else { // test positive and negative wrap respectively
wrap = currentBounds.getNorthEast().lat() < cc.lat(); if (wrap) { alert("WRAp detected top") } // else alert("no wrap:"+currentBounds); wrap = false;
var current_ne_lat = !wrap ? currentBounds.getNorthEast().lat() : allowed_ne_lat + (currentBounds.getNorthEast().lat() +90) + (90 - allowed_ne_lat);
wrap = currentBounds.getSouthWest().lat() > cc.lat(); if (wrap) { alert("WRAp detected btm") } //alert("no wrap:"+currentBounds);
var current_sw_lat = !wrap ? currentBounds.getSouthWest().lat() : allowed_sw_lat - (90-currentBounds.getSouthWest().lat()) - (allowed_sw_lat+90);
}
// Finalise positions
var centerX = cc.lng();
var centerY = cc.lat();
if (!centerH) {
if (current_ne_lng > allowed_ne_lng) centerX -= current_ne_lng-allowed_ne_lng;
if (current_sw_lng < allowed_sw_lng) centerX += allowed_sw_lng-current_sw_lng;
}
else {
centerX = allowedBounds.getCenter().lng();
}
if (!centerV) {
if (current_ne_lat > allowed_ne_lat) {
centerY -= (current_ne_lat-allowed_ne_lat) * 3; // approximation magic numbeer. Adjust as u see fit, or use a more accruate pixel measurement.
}
if (current_sw_lat < allowed_sw_lat) {
centerY += (allowed_sw_lat-current_sw_lat)*2.8; // approximation magic number
}
}
else {
centerY = allowedBounds.getCenter().lat();
}
map.setCenter(lastValidCenter = new google.maps.LatLng(centerY,centerX));
}
function limitBound(bound) // Occurs during dragging, pass allowedBounds to this function in most cases. Requires persistant 'lastValidCenter=map.getCenter()' var reference.
{
var mapBounds = map.getBounds();
if ( mapBounds.getNorthEast().lng() >= mapBounds.getSouthWest().lng() && mapBounds.getNorthEast().lat() >= mapBounds.getSouthWest().lat() // ensure no left/right, top/bottom wrapping
&& bound.getNorthEast().lat() > mapBounds.getNorthEast().lat() // top
&& bound.getNorthEast().lng() > mapBounds.getNorthEast().lng() // right
&& bound.getSouthWest().lat() < mapBounds.getSouthWest().lat() // bottom
&& bound.getSouthWest().lng() < mapBounds.getSouthWest().lng()) // left
{
lastValidCenter=map.getCenter(); // valid case, set up new valid center location
}
// if (bound.contains(map.getCenter()))
// {
map.panTo(lastValidCenter);
// }
}
// Google map listeners
google.maps.event.addListener(map, 'zoom_changed', function() {
//var zoom = map.getZoom();
checkBounds();
});
google.maps.event.addListener(map, "bounds_changed", function() {
checkBounds();
});
google.maps.event.addListener(map, 'center_changed', function() {
limitBound(allowedBounds);
});
p.s For checkBounds(), to get proper 2d world coordinate from the map's center, given 2 lat/lng values, use map.getProjection().fromLatLngToPoint(). Compare the 2 points, find the linear difference between them, and map the difference in world coordinates back to lat/lng using map.getProjection().fromPointToLatLng(). This will give you accurate clip offsets in lat/lng units.
This script gets the initial bounds (allowedBounds) and limit the bounds on drag and zoom_changed. Also the zoom is limited on < 7.
var allowedBounds = false;
google.maps.event.addListener(map, 'idle', function() {
if (!allowedBounds) {
allowedBounds = map.getBounds();
}
});
google.maps.event.addListener(map, 'drag', checkBounds);
google.maps.event.addListener(map, 'zoom_changed', checkBounds);
function checkBounds() {
if (map.getZoom() < 7) map.setZoom(7);
if (allowedBounds) {
var allowed_ne_lng = allowedBounds.getNorthEast().lng();
var allowed_ne_lat = allowedBounds.getNorthEast().lat();
var allowed_sw_lng = allowedBounds.getSouthWest().lng();
var allowed_sw_lat = allowedBounds.getSouthWest().lat();
var currentBounds = map.getBounds();
var current_ne_lng = currentBounds.getNorthEast().lng();
var current_ne_lat = currentBounds.getNorthEast().lat();
var current_sw_lng = currentBounds.getSouthWest().lng();
var current_sw_lat = currentBounds.getSouthWest().lat();
var currentCenter = map.getCenter();
var centerX = currentCenter.lng();
var centerY = currentCenter.lat();
if (current_ne_lng > allowed_ne_lng) centerX = centerX-(current_ne_lng-allowed_ne_lng);
if (current_ne_lat > allowed_ne_lat) centerY = centerY-(current_ne_lat-allowed_ne_lat);
if (current_sw_lng < allowed_sw_lng) centerX = centerX+(allowed_sw_lng-current_sw_lng);
if (current_sw_lat < allowed_sw_lat) centerY = centerY+(allowed_sw_lat-current_sw_lat);
map.setCenter(new google.maps.LatLng(centerY,centerX));
}
}
Thanks #sairafi. Your answer got me very close. I was getting an error where getBounds was undefined, so I wrapped it in another listener to make sure that the map was fully loaded first.
google.maps.event.addListenerOnce(map, 'tilesloaded', function() {
allowedBounds = map.getBounds();
google.maps.event.addListener(map,'center_changed',function() { checkBounds(allowedBounds); });
});
// Limit map area
function checkBounds(allowedBounds) {
if(!allowedBounds.contains(map.getCenter())) {
var C = map.getCenter();
var X = C.lng();
var Y = C.lat();
var AmaxX = allowedBounds.getNorthEast().lng();
var AmaxY = allowedBounds.getNorthEast().lat();
var AminX = allowedBounds.getSouthWest().lng();
var AminY = allowedBounds.getSouthWest().lat();
if (X < AminX) {X = AminX;}
if (X > AmaxX) {X = AmaxX;}
if (Y < AminY) {Y = AminY;}
if (Y > AmaxY) {Y = AmaxY;}
map.setCenter(new google.maps.LatLng(Y,X));
}
}
southWest = new google.maps.LatLng(48.59475380744011,22.247364044189453);
northEast = new google.maps.LatLng(48.655344320891444,22.352420806884766);
var limBound = new google.maps.LatLngBounds(southWest,northEast);
var lastCenter;
var option = {zoom:15,
center: limBound.getCenter(),
mapTypeId: google.maps.MapTypeId.ROADMAP};
var map = new google.maps.Map(document.getElementById('divMap'),option);
google.maps.event.addListener(map,'zoom_changed', function() {
minZoom(15);
});
google.maps.event.addListener(map,'drag',function(e){
limitBound(limBound);
});
function minZoom(minZoom){
if (map.getZoom()<minZoom)
{map.setZoom(minZoom);}
};
function limitBound(bound)
{
if (bound.getNorthEast().lat() > map.getBounds().getNorthEast().lat()
&& bound.getNorthEast().lng() > map.getBounds().getNorthEast().lng()
&& bound.getSouthWest().lat() < map.getBounds().getSouthWest().lat()
&& bound.getSouthWest().lng() < map.getBounds().getSouthWest().lng())
{
lastCenter=map.getCenter();
$('#divText').text(lastCenter.toString());
}
if (bound.contains(map.getCenter()))
{
map.setCenter(lastCenter);
}
}
pls check this Google Maps API v3: Can I setZoom after fitBounds?
map.fitBounds(mapBounds);
#sairafi and #devin
thanks for both your answers. I could not get this to work in Chrome/Windows 7 because the .comtains() check evlauated to both true and false as soon as you hit the boundary.
So I changed the setCenter() at the bottom to panTo()
The second problem was that if you establish boundaries on initial load, you have to use the google.maps.event.addListenerOnce(map,'idle'...) event otherwise it keeps resetting the boundaries to the currently viewable map.
Finally I do use the drag event for tracking centers, for some reason that worked more smoothly.
The resulting code is this:
google.maps.event.addListenerOnce(map,'idle',function() {
allowedBounds = map.getBounds();
});
google.maps.event.addListener(map,'drag',function() {
checkBounds();
});
function checkBounds() {
if(! allowedBounds.contains(map.getCenter()))
{
var C = map.getCenter();
var X = C.lng();
var Y = C.lat();
var AmaxX = allowedBounds.getNorthEast().lng();
var AmaxY = allowedBounds.getNorthEast().lat();
var AminX = allowedBounds.getSouthWest().lng();
var AminY = allowedBounds.getSouthWest().lat();
if (X < AminX) {X = AminX;}
if (X > AmaxX) {X = AmaxX;}
if (Y < AminY) {Y = AminY;}
if (Y > AmaxY) {Y = AmaxY;}
map.panTo(new google.maps.LatLng(Y,X));
}
}

Setting maximum and minimum bounds using Google Maps API

Was wondering if someone could provide an example of how to set maximum and minimum extents using GMaps2? In OpenLayers, here's how it is done:
var point1 = new OpenLayers.Geometry.Point(7, 48);
point1.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
var point2 = new OpenLayers.Geometry.Point(11, 54);
point2.transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913"));
var bounds = new OpenLayers.Bounds();
bounds.extend(point1);
bounds.extend(point2);
bounds.toBBOX();
map = new OpenLayers.Map("map", {
maxExtent: bounds,
maxResolution:"auto",
maxZoomLevel: 8,
projection:"EPSG:900913",
controls: []
});
map.addLayer(new OpenLayers.Layer.OSM.Osmarender("Osmarender"));
map.zoomToMaxExtent();
//map.zoomToExtent(bounds);
What would be the GMaps2 equivalent?
The following is the Google Maps API 2.x equivalent to limit the panning of the map to predefined bounds:
// Bounds for North America
var allowedBounds = new GLatLngBounds(new GLatLng(48.197218, -127.529297),
new GLatLng(28.72913, -68.818359));
function checkBounds() {
if (allowedBounds.contains(map.getCenter())) {
return;
}
var c = map.getCenter();
var x = c.lng();
var y = c.lat();
var maxX = allowedBounds.getNorthEast().lng();
var maxY = allowedBounds.getNorthEast().lat();
var minX = allowedBounds.getSouthWest().lng();
var minY = allowedBounds.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 GLatLng(y, x));
}
GEvent.addListener(map, "move", function() { checkBounds(); });
Thanks for getting back to me Chris and Daniel. What I meant by 'extents' is that when zoomed out is maximized, the world map doesn't tile. I don't want to see 2, 3 or even 4 North America's and other continents. The world is big enough with just one. When I zoom in, I want to set a limit to how far one can zoom in to account for our data resolution. Zoom in too far returns results outside our data resolution. I did manage to find an acceptable solution by restricting the zoom levels. Anyway, FWIW, here's what I came up. It's a bit less verbose but isn't doing exactly what Daniel does in his code:
//===== Restrict Zoom Levels =====
var mapTypes = map.getMapTypes();
// Overwrite the getMinimumResolution() and getMaximumResolution() methods
for (var i=0; i<mapTypes.length; i++) {
mapTypes[i].getMinimumResolution = function() {return 2;}
mapTypes[i].getMaximumResolution = function() {return 9;}
}
I still want to try Daniel's code to see what it does. Special thanks to you for taking the time to write this up Dan.
-=Al