Openlayers3 - fractional zoom on touch device using pinch gesture? - javascript

Openlayers3 can display a map at a fractional zoom level programmatically by specifying a fractional number in setZoom().
However, I want my users to be able to get a fractional zoom level on a mobile touch-driven device (ie, a smart phone). Pinch or reverse pinch zooms out/in by jumping to the nearest whole zoom level when the fingers are removed from the screen.
How can a touch/mobile user get Openlayers3 to stay at the exact extent (fractional zoom level) that the user has pinched to?
Is there something I can add to the javascript for my Openlayers3 map or view to get this to work as desired?
In particular, I note that Openlayers3 has a ol.interaction.PinchZoom() class ( https://openlayers.org/en/latest/apidoc/ol.interaction.PinchZoom.html ). This is a subclass of ol.interaction.Pointer which has a handleUpEvent and a handleEvent (which should be a function). So in theory, I should be able to either replace the handleUpEvent in PinchZoom OR replace the default PinchZoom interaction with one that has a custom event handler function. But with both of these approaches, I can't get my new handleUpEvent function to be called.
(While trawling the OL code for PinchZoom I have found that the default PinchZoom does what I want if one finger is lifted from the touch-screen before the other finger, but I still want to get this working when both fingers are lifted simultaneously.)
Here is what I've tried so far...
FIRST ATTEMPT - This just attempts to replace the standard PinchZoom's handleUpEvent with a custom one, and set this as the only interaction for the map. However, the event function is never called (never logs anything).
function handleUpEvent(evt) {
console.log("Up event handler");
return true; // Stop drag
}
map = new ol.Map({
layers: [],
target: 'map',
controls: controls,
interactions: [new ol.interaction.PinchZoom({handleEvent: handleUpEvent})],
view: new ol.View({projection: projCode})
});
SECOND ATTEMPT - This attempt is based on the actual OL code for creating the standard PinchZoom interaction. In this case, all my event handler functions DO get called, the number of touches ( targetPointers ) is always zero (as logged). I'm no javascript guru, but I suspect that this is because the symbols in ol.js are different to in ol-debug.js which I'm basing this on. In fact I had to declare targetPointers myself to even get this to run, even though it is declared by OL itself already (but presumably using a different symbol name in the non-debug version).
function handleDownEvent(mapBrowserEvent) {
console.log("DOWN event handler");
this.anchor_ = null;
this.lastDistance_ = undefined;
this.lastScaleDelta_ = 1;
mapBrowserEvent.map.render();
return true; // Start drag
}
function handleDragEvent(mapBrowserEvent) {
if ( this.targetPointers.length < 2 ) {
console.log("DRAG event ignored - touches ", this.targetPointers.length);
} else {
console.log("DRAG event handled");
var scaleDelta = 1.0;
var touch0 = this.targetPointers[0];
var touch1 = this.targetPointers[1];
var dx = touch0.clientX - touch1.clientX;
var dy = touch0.clientY - touch1.clientY;
// distance between touches
var distance = Math.sqrt(dx * dx + dy * dy);
if (this.lastDistance_ !== undefined) {
scaleDelta = this.lastDistance_ / distance;
}
this.lastDistance_ = distance;
if (scaleDelta != 1.0) {
this.lastScaleDelta_ = scaleDelta;
}
var map = mapBrowserEvent.map;
var view = map.getView();
var resolution = view.getResolution();
// scale anchor point.
var viewportPosition = map.getViewport().getBoundingClientRect();
var centroid = ol.interaction.Pointer.centroid(this.targetPointers);
centroid[0] -= viewportPosition.left;
centroid[1] -= viewportPosition.top;
this.anchor_ = map.getCoordinateFromPixel(centroid);
// scale, bypass the resolution constraint
map.render();
ol.interaction.Interaction.zoomWithoutConstraints(
map, view, resolution * scaleDelta, this.anchor_);
}
}
function handleUpEvent(mapBrowserEvent) {
console.log("UP event handler");
return true; // Stop drag
}
var pinchZoom = new ol.interaction.Pointer({
handleDownEvent: handleDownEvent,
handleDragEvent: handleDragEvent,
handleUpEvent: handleUpEvent
});
pinchZoom.targetPointers = [];
map = new ol.Map({
interactions: ol.interaction.defaults().extend([pinchZoom]),
layers: [
new ol.layer.Tile({
source: new ol.source.TileJSON({url: 'https://api.tiles.mapbox.com/v3/mapbox.geography-class.json?secure'})
})
],
target: 'map',
view: new ol.View({center: [0, 0], zoom: 3})
});

The problems with your attempts:
First attempt
The option handleEvent for ol.interaction.PinchZoom does not exists. That is why your callback is not executed.
Second attempt
As you already noted you try to use openlayers internals. This might work with the debug build but will not work in the release build as the names are different than.
Solution
Add the option to keep a fractional resolution at the pinch zoom end in the openlayers sources.
Issue: https://github.com/openlayers/ol3/issues/6223
Pull request: https://github.com/openlayers/ol3/issues/6224
The pinch zoom will by default keep the fractional zoom level as chosen by the user .. starting with OpenLayers 3.20
An earlier idea:
As an example I added the option keepFractionalZoomLevel to ol.interaction.PinchZoom: https://github.com/aAXEe/ol3/commit/7639cb20d17858492652896bcd4a6ff7992a9bb0
See this fiddle for a working example: https://jsfiddle.net/wm78prro/5/
Note: The fiddle uses a custom openlayers build: https://github.com/aAXEe/ol3/pull/1
The deploy url is: https://deploy-preview-1--pump-attendant-rhinoceros-42285.netlify.com/ol-debug.js
Does this example behave as you want?
As this may be usefull for others we can try to integrate it into openlayers.

Related

Creating gravity point with Matter.js [duplicate]

Is it possible to create a single gravity / force point in matter.js that is at the center of x/y coordinates?
I have managed to do it with d3.js but wanted to enquire about matter.js as it has the ability to use multiple polyshapes.
http://bl.ocks.org/mbostock/1021841
The illustrious answer has arisen:
not sure if there is any interest in this. I'm a fan of what you have created. In my latest project, I used matter-js but I needed elements to gravitate to a specific point, rather than into a general direction. That was very easily accomplished. I was wondering if you are interested in that feature as well, it would not break anything.
All one has to do is setting engine.world.gravity.isPoint = true and then the gravity vector is used as point, rather than a direction. One might set:
engine.world.gravity.x = 355;
engine.world.gravity.y = 125;
engine.world.gravity.isPoint = true;
and all objects will gravitate to that point.
If this is not within the scope of this engine, I understand. Either way, thanks for the great work.
You can do this with the matter-attractors plugin. Here's their basic example:
Matter.use(
'matter-attractors' // PLUGIN_NAME
);
var Engine = Matter.Engine,
Events = Matter.Events,
Runner = Matter.Runner,
Render = Matter.Render,
World = Matter.World,
Body = Matter.Body,
Mouse = Matter.Mouse,
Common = Matter.Common,
Bodies = Matter.Bodies;
// create engine
var engine = Engine.create();
// create renderer
var render = Render.create({
element: document.body,
engine: engine,
options: {
width: Math.min(document.documentElement.clientWidth, 1024),
height: Math.min(document.documentElement.clientHeight, 1024),
wireframes: false
}
});
// create runner
var runner = Runner.create();
Runner.run(runner, engine);
Render.run(render);
// create demo scene
var world = engine.world;
world.gravity.scale = 0;
// create a body with an attractor
var attractiveBody = Bodies.circle(
render.options.width / 2,
render.options.height / 2,
50,
{
isStatic: true,
// example of an attractor function that
// returns a force vector that applies to bodyB
plugin: {
attractors: [
function(bodyA, bodyB) {
return {
x: (bodyA.position.x - bodyB.position.x) * 1e-6,
y: (bodyA.position.y - bodyB.position.y) * 1e-6,
};
}
]
}
});
World.add(world, attractiveBody);
// add some bodies that to be attracted
for (var i = 0; i < 150; i += 1) {
var body = Bodies.polygon(
Common.random(0, render.options.width),
Common.random(0, render.options.height),
Common.random(1, 5),
Common.random() > 0.9 ? Common.random(15, 25) : Common.random(5, 10)
);
World.add(world, body);
}
// add mouse control
var mouse = Mouse.create(render.canvas);
Events.on(engine, 'afterUpdate', function() {
if (!mouse.position.x) {
return;
}
// smoothly move the attractor body towards the mouse
Body.translate(attractiveBody, {
x: (mouse.position.x - attractiveBody.position.x) * 0.25,
y: (mouse.position.y - attractiveBody.position.y) * 0.25
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.12.0/matter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/matter-attractors#0.1.6/build/matter-attractors.min.js"></script>
Historical note: the "gravity point" functionality was proposed as a feature in MJS as PR #132 but it was closed, with the author of MJS (liabru) offering the matter-attractors plugin as an alternate. At the time of writing, this answer misleadingly seems to indicate that functionality from the PR was in fact merged.
Unfortunately, the attractors library is 6 years outdated at the time of writing and raises a warning when using a newer version of MJS than 0.12.0. From discussion in issue #11, it sounds like it's OK to ignore the warning and use this plugin with, for example, 0.18.0. Here's the warning:
matter-js: Plugin.use: matter-attractors#0.1.4 is for matter-js#^0.12.0 but installed on matter-js#0.18.0.
Behavior seemed fine on cursory glance, but I'll keep 0.12.0 in the above example to silence it anyway. If you do update to a recent version, note that Matter.World is deprecated and should be replaced with Matter.Composite and engine.gravity.

can't change marker icons / colors in mapbox

I'm manipulating the mapbox marker radius example here:
https://www.mapbox.com/mapbox.js/example/v1.0.0/marker-radius-search/
to attempt to change the color / icon of the markers within a certain radius of a random point, but the colors aren't changing despite the properties being registered as changed. Here's my code:
clusterLayer = L.mapbox.featureLayer('examples.map-h61e8o8e').on('ready', function(e) {
clusterGroup = new L.MarkerClusterGroup({
showCoverageOnHover: false,
animateAddingMarkers: true
});
e.target.eachLayer(function(layer) {
clusterGroup.addLayer(layer);
layerArray.push(layer);
});
map.addLayer(clusterGroup);
});
window.setTimeout(eventFunction,eventTiming);
function eventFunction(){
clusterLayer.setFilter(affectMarker);
}
function affectMarker(feature) {
var fLat = feature.geometry.coordinates[1];
var fLng = feature.geometry.coordinates[0];
var fPt = L.latLng(fLat,fLng);
var dist = eventPt.distanceTo(fPt);
if (dist < eventRadius){
feature.properties['marker-color'] = eventColorNegative;
feature.properties['marker-symbol'] = 'danger';
}
}
Why doesn't this work? I've verified that it is returning valid points.
Note also that the markers being used are MakiMarkers
I found two ways to do this, though neither, I think, is as ideal as being able to do so with the code above. The first is, rather than to use setFilter, use eachLayer:
clusterLayer.eachLayer(affectMarker);
and then in the loop, use setIcon:
layer.feature.properties['marker-color'] = eventColorNegative;
layer.feature.properties['marker-symbol'] = 'danger';
layer.setIcon(L.mapbox.marker.icon(layer.feature.properties));
The other way is to first include the MakiMarkers extension (which I believe has been deprecated and rolled into Mapbox):
https://github.com/jseppi/Leaflet.MakiMarkers
and then use this syntax:
layer.setIcon(L.MakiMarkers.icon({icon: "danger", color: eventColorNegative}));

Custom Map Tile Overlay Issues

I have a need to display a couple hundred to perhaps one thousand high resolution aerial photographs over the standard satellite imagery provided by Google maps. The images are geographically dispersed, so I decided to implement a tile server as a generic asp.net handler (*.ashx file). I will be basing my issue descriptions on the map shown on Google's developer site at the following URL:
https://developers.google.com/maps/documentation/javascript/examples/maptype-overlay
Everything is more-or-less working, but I am having the following two issues:
1) After selecting the "Satellite" map type, hovering over that button produces a dropdown with a checkbox called "Labels". How can I add another checkbox to that dropdown titled "Aerial Photographs" that will toggle my overlay on/off? Will I have to hard-code JQuery hacks that utilize Google Maps implementation details, or can I accomplish this through the API?
2) My *.ashx handler returns either the image, or a status 204 (no content) if the specified tile does not exist. The issue is that the 204 results are not cached, so every time I zoom out and back in to the same location, my server gets re-hit for all the tiles that the client should already know don't exist. I failed to see it documented what a tile server should return for such an "empty" tile, so the client can cache the result. What should I return if there is no map tile for a specific location?
Thanks.
Given the lack of response to this question, it is clear that sparse tile servers are an uncommon practice. The following is the solution (however hacky it may be) for both problems:
1) How do I add a checkbox to the "Satellite" dropdown to toggle my map layer? Unfortunately, there is no supported way to do this, so I came up with the following INCREDIBLY hacky code:
// Create a function to select the "Labels" checkbox
var getLabelButton = function() {
return $('.gm-style-mtc:last > div:last > div:last');
};
// Clone the "Labels" checkbox used to show/hide the hybrid map overlay
var labelBtn = getLabelButton();
var labelClone = labelBtn.clone();
// Change the display and hover text for the new button
labelClone.prop('title', Localizer.GetString('CustomImagery.Description'));
labelClone.find('label').html(Localizer.GetString('CustomImagery.Name'));
// Highlight the button when the client hovers the mouse over it
var checkbox = labelClone.children('span');
var prevBackColor = labelClone.css('background-color');
var prevBorderColor = checkbox .css('border-color');
labelClone.hover(function() {
labelClone.css('background-color', '#EBEBEB');
checkbox .css('border-color' , '#666');
}, function() {
labelClone.css('background-color', prevBackColor);
checkbox .css('border-color' , prevBorderColor);
});
// Set the checkmark image source to be the correct value, instead of transparent
var checkmark = checkbox .children('div');
var checkmarkImg = checkmark.children('img');
checkmarkImg.attr('src', 'https://maps.gstatic.com/mapfiles/mv/imgs8.png');
// Attach the new checkbox after the Labels checkbox
labelBtn.after(labelClone);
// Create a method to determine if the selected map type supports custom imagery
var mapTypesSupportingCustomImagery = [
google.maps.MapTypeId.SATELLITE,
google.maps.MapTypeId.HYBRID
];
var isImagerySupportedOnSelectedMapType = function() {
var mapTypeId = googleMap.getMapTypeId();
return (0 <= mapTypesSupportingCustomImagery.indexOf(mapTypeId));
};
// Show the checkmark and imagery if the initial map type supports it
if (isImagerySupportedOnSelectedMapType()) {
checkmark.css('display', '');
googleMap.overlayMapTypes.push(tileServer);
}
// Show/hide the checkmark and imagery when the user clicks on the checkbox
labelClone.on('click', function() {
var showImagery = (checkmark.css('display') === 'none');
if (showImagery) {
checkmark.css('display', '');
googleMap.overlayMapTypes.push(tileServer);
} else {
checkmark.css('display', 'none');
var tileServerIndex = googleMap.overlayMapTypes.indexOf(tileServer);
googleMap.overlayMapTypes.removeAt(tileServerIndex);
}
});
// Create a function that returns whether the custom imagery should be displayed
var displayCustomImagery = function() {
return (isImagerySupportedOnSelectedMapType() && checkmark.css('display') != 'none');
};
// Add an event listener to add the tile server when displaying satellite view
google.maps.event.addListener(googleMap, 'maptypeid_changed', function() {
var tileServerIndex = googleMap.overlayMapTypes.indexOf(tileServer);
if (displayCustomImagery()) {
if (tileServerIndex < 0) {
googleMap.overlayMapTypes.push(tileServer);
}
} else if (0 <= tileServerIndex) {
googleMap.overlayMapTypes.removeAt(tileServerIndex);
}
});
In the above code, googleMap is the map object, and tileServer is my implementation of a google.maps.ImageMapType object.
2) What should I return to represent an empty tile?
My solution to this question was quite a bit cleaner. I simply list the file names of all the tiles on the server, which are a base-4 encoding of the Morton number for the tile being requested. Then I send this list to the client as a dictionary from string to bool (always true). The client simply checks to see if the server contains a map tile before making the request, so the server doesn't have to worry about what to return (I left it as returning a 204 error if an invalid request is made). The javascript to get the tile name within the getTileUrl method is as follows:
function(coord, zoom) {
// Return null if the zoom level is not supported
// NOTE: This should not be necessary, but minZoom and
// maxZoom parameters are ignored for image map types
if (zoom < minZoom || maxZoom < zoom) {
return null;
}
// Get the name of the map tile being requested
var tileName = '';
var y = coord.y << 1;
for (var shift = zoom - 1; 0 <= shift; --shift) {
var digit = (coord.x >>> shift) & 1;
digit |= ( y >>> shift) & 2;
tileName += digit;
}
// Return if the map tile being requested does not exist
if (!mapTiles[tileName]) {
return null;
}
// Return the url to the tile server
...
}

How to display markers (huge numbers) on a map which has been logically divided into segments?

What i have done so far:
i'm developing an application where i have to display more than(50K) points/Markers on the Navteq map divided into different segments.
for example: if i have 50K points i will divide all points into different segments.
if i divide 50K points into 50 segments each segment would have 1000 points (may not be 50 segments , it may depend).
right now it is working but it takes long time and hangs to render all the points on the MAP.so that i would like to perform segmentation displaying to display only few points with clustering.
so that i can get an idea of how the segment will look like.
but the problem here is i should only perform the clustering based on the segments.otherwise points from different segments willbe mixed together and displayed
as single unit and that conveys the wrong information to the user.
so here my question is: is it possible to perform the clustering based on the segment. so that only points from same segment will be clustered.
Note: if this is not possible, i would like to use Latest version of here-maps 2.5.3 (Asynchronous) may reduce some time while loading, so that i would like to use indexing functionality also while rendering the points
to improve the rendering time using nokia.maps.clustering.Index class.
i studied that indexing would reduce the time while rendering the points/markers on map. does it help in my case? could anybody please suggest how to perform indexing ?
This is the code with which i'm displaying points on map:
function displayAllLightPoints(arrLightPointCoordinats, totalLightPoints,
selectedSegmentId, totalSegmentsCount,segmentColorcode)
{
var MyTheme1 = function () {
};
segmentColorcode = segmentColorcode.substring(2,segmentColorcode.length-1);
MyTheme1.prototype.getNoisePresentation = function (dataPoint) {
var markerLightPoint = new nokia.maps.map.Marker(dataPoint, {
icon: new nokia.maps.gfx.BitmapImage("..//Images//Lightpoint//" +
segmentColorcode + ".png"),
anchor: {
x: 12,
y: 12
}
});
return markerLightPoint;
};
MyTheme1.prototype.getClusterPresentation = function (data) {
var markerLightPoint = new
nokia.maps.map.StandardMarker(data.getBounds().getCenter(), {
icon: new nokia.maps.gfx.BitmapImage("..//Images//
Segment/" + segmentColorcode + ".png", null, 66, 65),
text: data.getSize(),
zIndex: 2,
anchor: {
x: 12,
y: 12
}
});
return markerLightPoint;
};
var ClusterProvider = nokia.maps.clustering.ClusterProvider,
theme = new MyTheme1(),
clusterProvider = new ClusterProvider(map, {
eps: 0.00000000001,
minPts: 1000000,
strategy: nokia.maps.clustering.ClusterProvider.
STRATEGY_DENSITY_BASED,
theme: theme,
dataPoints: []
});
var lightpointsDataSet1 = new Array();
for (var i = 0; i < totalLightPoints; i++) {
lightpointsDataSet1[i] = { latitude: arrLightPointCoordinats[i][0],
longitude: arrLightPointCoordinats[i][1], title:
'LightPoint ' + (i + 1) };
}
clusterProvider.addAll(lightpointsDataSet1);
clusterProvider.cluster();
}
To deal with a very large (50K+) data set , I would do all the heavy number crunching server side and send over a new JSON response whenever the map is updated. Something like the HTML page described here
The key section of the code is the ZoomObserver:
var zoomObserver = function (obj, key, newValue, oldValue) {
zoom = newValue;
if (zoom < 7)
{ zoom = 7;}
if (zoom > 16)
{ zoom = 16;}
// Define the XML filename to read that contains the marker data
placeMarkersOnMaps('http://api.maps.nokia.com/downloads/java-me/cluster/'+ zoom + '.xml'
+ '?lat1=' + map.getViewBounds().topLeft.latitude
+ '&lng1='+ map.getViewBounds().topLeft.longitude
+ '&lat2='+ map.getViewBounds().bottomRight.latitude
+ '&lng2='+ map.getViewBounds().bottomRight.longitude);
};
map.addObserver("zoomLevel", zoomObserver );
Where the REST service returns a "well-known" data format which can be used to add markers and clusters to the map.
Now assuming you have two massive data sets you could make two requests to different endpoints, or somehow distinguish which cluster of data belongs to which so that you would just be returning information of the form:
{latitude':51.761,'longitude':14.33128,'value':102091},
i.e. using the DataPoint standard (which means you could use a heat map as well.
Of course, what I'm not showing here is the back-end functionality to cluster in the first place - but this leaves the client (and the API) to do what it does best displaying data, not number crunching.

Can't seem to apply styles to Google MAP API

I trying to change the saturation for this instance of a Google map. Everything else is working EXCEPT the saturation style. What am i doing wrong ?
Code:
$(function() { // when the document is ready to be manipulated.
if (GBrowserIsCompatible()) { // if the browser is compatible with Google Map's
var map = document.getElementById("myMap"); // Get div element
var m = new GMap2(map); // new instance of the GMap2 class and pass in our div location.
m.setCenter(new GLatLng(59.334951,18.088598), 13); // pass in latitude, longitude, and zoom level.
m.openInfoWindow(m.getCenter(), document.createTextNode("Looking Good")); // displays the text
m.setMapType(G_NORMAL_MAP); // sets the default mode. G_NORMAL_MAP, G_HYBRID_MAP
// var c = new GMapTypeControl(); // switch map modes
// m.addControl(c);
// m.addControl(new GLargeMapControl()); // creates the zoom feature
var styleArray = [
{
featureType: "all",
stylers: [
{saturation: -80}
]
}
];
}
else {
alert("Upgrade your browser, man!");
}
});
I have been searching all day (and failing) to find a solution to this problem.
Do you have a line like:
m.setOptions({styles: styleArray});
Somewhere after you've declared the styler? The above line will apply the styles you created to the map that you have.
Looks like your styleArray is never used.
Thanks for all your help i finally figured it out. Feeling a bit stupid but the "m.setOtions" is apparently a function of V3 API and i was using V2.

Categories