I'm using openlayers 3 to show a map with some markers that have a small icon. Upon clicking one of them, the browser switches to another page associated with the marker.
The markers are currently implemented as features:
const style = new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1.0],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
src: img_url,
})
});
const feature = new ol.Feature({
geometry: new ol.geom.Point([x, y]),
});
feature.setStyle(style);
Here's my click handler:
map.on("click", e => {
map.forEachFeatureAtPixel(e.pixel, (feature) => {
window.location.href = "/s/" + feature.getId();
return true; // stop after first feature
});
});
Unfortunately, the icons are quite small and therefore hard to hit on a touch-based interface such as an iPad.
Is there an accepted way of making the target larger? My ideas are the following:
Create an additional invisible marker for each marker and make those clickable.
Instead of only using the event's location, I could sample some pixels around it and use all features nearby.
Is there a better way to do this?
My suggestion is that you create an invisible square around your icon like:
const style = [
new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1.0],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
src: img_url,
})
}),
new ol.style.Style({
image: new ol.style.RegularShape({
stroke: new ol.style.Stroke({ color: [0, 0, 0, 0] }),
points: 4,
radius: 50, // <--------- control its size
angle: Math.PI / 4
})
})
];
I've initially tried out Jonatas' approach where I add a larger style. This works quite well. One caveat is that upon clicking on a feature, we have to figure out which feature is the closest because they can easily overlap.
I've finally decided to go with a slightly different approach after discovering the getClosestFeatureToCoordinate() method. I do everything in the click handler:
map.on("click", event => {
const distance = feature => {
const coords = feature.getGeometry().getCoordinates();
const pixel = map.getPixelFromCoordinate(coords);
const distSquared = Math.pow(event.pixel[0] - pixel[0], 2)
+ Math.pow(event.pixel[1] - pixel[1], 2);
return distSquared;
};
const clickedFeature = { feat: null, dist: Infinity };
/* See if we clicked on a feature. If yes, take closest */
map.forEachFeatureAtPixel(event.pixel, (feature) => {
const dist = distance(feature);
if (dist < clickedFeature.dist) {
clickedFeature.feat = feature;
clickedFeature.dist = dist;
}
});
/* If we are touch-based, we also take into account clicks that happen nearby */
if (!clickedFeature.feat) {
if (ol.has.TOUCH) {
const coords = this._map.getCoordinateFromPixel(event.pixel);
const closestFeat = this._featureSource.getClosestFeatureToCoordinate(coords);
const dist = distance(closestFeat);
/* touch size taken from Apple's guidelines */
if (dist < Math.pow(22,2)) {
clickedFeature.feat = closestFeat;
clickedFeature.dist = dist;
}
}
}
/* go to station */
if (clickedFeature.feat) {
window.location.href = "/s/" + clickedFeature.feat.getId();
}
});
Related
I want to display on duty taxi positions using openlayers.
I have coordinates and heading of each taxi cab but when I'm trying to display them everything is okay with coordinates but headings aren't displaying properly. It's because I have the same style for every marker and adding them to the same vector layer.
I would like to show each vehicle with its own heading.
Any solutions for this issue will be appreciated.
My code:
var taxiMarker = new ol.style.Style({
image: new ol.style.Icon({
scale: 0.7, anchor: [0.5, 0.5],
src: 'uploads/taxiMarker.png'
})
});
var taxi_vector = new ol.layer.Vector({
source: new ol.source.Vector({
}),
style: [taxiMarker]
});
for(let i = 0; i < res.length; i++) {
let tCoords = res[i][0].split(',');
let heading = parseFloat(res[i][1]);
let marker = new ol.geom.Point(tCoords);
let featureMarker = new ol.Feature(marker);
taxiMarker.getImage().setRotation(heading);
taxi_vector.getSource().addFeature(featureMarker);
}
I'm trying to open a popup in a map with markers, getting the markers points from a list where latitude and longitude are given, and they are plotted correctly in the map.
Following https://openlayers.org/en/latest/examples/popup.html I added the code for opening popup, and this is my code:
var container = document.getElementById('popup');
var content = document.getElementById('popup-content');
var closer = document.getElementById('popup-closer');
var overlay = new ol.Overlay({
element: container,
autoPan: true,
autoPanAnimation: {
duration: 250
}
});
closer.onclick = function() {
overlay.setPosition(undefined);
closer.blur();
return false;
};
// create the map with the proper center
var map = new ol.Map({
controls: ol.control.defaults().extend(
[new ol.control.ScaleLine()]
),
view: new ol.View({
center: ol.proj.fromLonLat([center.long, center.lat]),
zoom: zoom
}),
overlays: [overlay],
layers: [new ol.layer.Tile(
{source: new ol.source.OSM()}
)
],
target: 'mapdiv',
keyboardEventTarget: document
}
);
// the style for the markers
var markerStyle = new ol.style.Style({
image: new ol.style.Icon(/** #type {module:ol/style/Icon~Options} */ ({
anchor: [0.5, 1],
anchorXUnits: 'fraction',
anchorYUnits: 'fraction',
scale: 0.4,
src: 'img/ic_place_void_black_24dp_2x.png'
}))
});
for (i = 0; i < pointList.length; ++i) {
// add the marker
markersList[i] = new ol.Feature({
geometry: new ol.geom.Point(ol.proj.fromLonLat([pointList[i].long, pointList[i].lat])),
namesList: pointList[i].mediaNameList
});
// apply the style to the marker
markersList[i].setStyle(markerStyle);
}
// generate the markers vector
var markers = new ol.source.Vector({
features: markersList
});
// generate the markers layer
var markerVectorLayer = new ol.layer.Vector({
source: markers,
});
// add the markers layer to the map
map.addLayer(markerVectorLayer);
/**
* Add a click handler to the map to render the popup.
*/
map.on('singleclick', function(evt) {
var clickedPosition = ol.proj.toLonLat(evt.coordinate);
console.log(clickedPosition);;
overlay.setPosition(ol.proj.fromLonLat(clickedPosition));
});
Now I'm stuck with a unexplicable behaviour; whenever I click, the popup is shown about one screen south-east, whatever zoom level of the map.
The coordinates of clickedPosition (I'm seeing them in the console) are the correct coordinates where I clicked, but the popup is shown in a wrong point, with a shift which is always the same in pixels.
What am I missing?
You can refer overlay positioning for showing features in any world.
follow the link
I need to implement a measure tool with OpenLayers, and I would like to display distance on the segment, with a smart management of the scale (a mark on the segment each 10m, then each 50m, 100m, 1km, 5km, for example...), very much like the GoogleMaps "measure distance" tool.
Is there any library doing that ? What would be the good approach to implement it ?
In short: No, I don't know any lib or class that provides what you want out of the box.
Option 1: Customize ScaleLine
ScaleLine (see api docs) has the option to provide your own render function (see code). The default implementation just calculates the distance and shows it as {number} {scale} by calling the internal function updateElement_, which then updates the ScaleLine's innerHtml.
You could theoretically replace that method and set the innerHTML yourself. That approach might limit you to the development-variant of the library, because the production code is minified and those elements (innerElement_, element_) are not marked as api.
new ol.control.ScaleLine({
render: function(mapEvent) {
// do stuff
}
});
Option 2: Use the Draw Feature with customized LineString styles
so that might be too complicated and I suggest you go for the ol.interaction.Draw feature. The Measure Example shows us how one could draw stuff while the user is drawing a line. You can combine that with custom styles on a LineString.
// TODO split the uses drawn line into segments, like this mockup
const line = new ol.geom.LineString([
[20.0, 50.0],
[30.0, 47.0],
[40.0, 47.0],
[50.0, 47.0]
]);
line.transform('EPSG:4326', 'EPSG:3857');
const lineFeature = new ol.Feature(line);
const lineSource = new ol.source.Vector({
features: [lineFeature]
});
function segmentText(coord, coord2) {
const coord_t = ol.proj.transform(coord, 'EPSG:3857', 'EPSG:4326');
let coordText = coord_t[1].toFixed(0) + '/' + coord_t[0].toFixed(0);
if(coord2) {
const length = ol.Sphere.getLength(new ol.geom.LineString([coord2, coord]));
const distance = (Math.round(length / 1000 * 100) / 100) + ' km';
coordText = coordText + '\n' + distance;
} else {
coordText = coordText + '\n0';
}
return new ol.style.Text({
text: coordText,
fill: new ol.style.Fill({
color: "#00f"
}),
offsetY: 25,
align: 'center',
scale: 1,
});
}
function styleFunction(feature) {
var geometry = feature.getGeometry();
var styles = [
// linestring style
new ol.style.Style({
stroke: new ol.style.Stroke({
color: '#ff0000',
width: 2
})
})
];
function createSegmentStyle(coord, coord2, rotation) {
return new ol.style.Style({
geometry: new ol.geom.Point(coord),
image: new ol.style.Icon({
src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAIAAADdv/LVAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAUSURBVBhXY1Da6MPEwMDAxMDAAAALMAEkQYjH8gAAAABJRU5ErkJggg==',
anchor: [0.75, 0.5],
rotateWithView: true,
rotation: -rotation,
scale: 4
}),
text: segmentText(coord, coord2)
})
};
const firstCoord = geometry.getFirstCoordinate();
geometry.forEachSegment(function (start, end) {
var dx = end[0] - start[0];
var dy = end[1] - start[1];
var rotation = Math.atan2(dy, dx);
if (firstCoord[0] === start[0] && firstCoord[1] === start[1]) {
styles.push(createSegmentStyle(start, null, rotation));
}
styles.push(createSegmentStyle(end, firstCoord, rotation));
});
return styles;
}
const map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.Stamen({ layer:'toner-lite' })
}),
new ol.layer.Vector({
source: lineSource,
style: styleFunction
})
],
view: new ol.View({
center: ol.proj.transform(
[35, 45], 'EPSG:4326', 'EPSG:3857'),
zoom: 4
})
});
<link href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.5.0/ol.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.5.0/ol-debug.js"></script>
<div id="map" style="height:300px"></div>
Is it possible in OpenLayers 3 to create a text label which clones multiple times along a linestring feature, depending on a scale? Something like:
Here you can see, that when we change scale label "IX Corps Blvd" appears twice. How can we implement this?
You can achieve this with style function. My code sample is about making arrows to line string (slightly different case), but I have commented parts necessary to be changed (at least):
var raster = new ol.layer.Tile({
source: new ol.source.OSM()
});
var source = new ol.source.Vector();
var styleFunction = function(feature) {
var geometry = feature.getGeometry();
var styles = [
// linestring
new ol.style.Style({
stroke: new ol.style.Stroke({ // apply street style here
color: '#ffcc33',
width: 2
})
})
];
geometry.forEachSegment(function(start, end) {
var dx = end[0] - start[0];
var dy = end[1] - start[1];
var rotation = Math.atan2(dy, dx);
// arrows
styles.push(new ol.style.Style({
geometry: new ol.geom.Point(end),
image: new ol.style.Icon({ // Use here label, not icon.
src: 'http://openlayers.org/en/v3.17.1/examples/data/arrow.png',
anchor: [0.75, 0.5],
rotateWithView: false,
rotation: -rotation
})
}));
});
return styles;
};
var vector = new ol.layer.Vector({
source: source,
style: styleFunction
});
map.addInteraction(new ol.interaction.Draw({
source: source,
type: /** #type {ol.geom.GeometryType} */ ('LineString')
}));
Some more effort is needed to place titles in correct placements. I left this answer like this to serve a solid starting point for building your feature.
My source:
[1] http://openlayers.org/en/master/examples/line-arrows.html
I got a problem in web development with OpenLayers 3. I want to add an arrow image by drawing a lineString, the image can draw and point to the direction along the lineString direction, but the image only displays in half, it cannot go over the the line. An excerpt from the code:
var start = e.feature.getGeometry().getFirstCoordinate();
var end = e.feature.getGeometry().getLastCoordinate();
var dx = end[0] - start[0];
var dy = end[1] - start[1];
var rotation = Math.atan2(dy, dx);
var a = new ol.Feature({
geometry: new ol.geom.Point(end)
});
var b = new ol.style.Style({
image: new ol.style.Icon(({
anchor: [0.5, 0.5],
anchorOrigin: 'top-left',
offset: [0, 0],
scale: 1,
opacity: 1,
rotateWithView: false,
rotation: -rotation,
src: '//openlayers.org/en/v3.8.2/examples/data/arrow.png',
}))
});
var m = a.setStyle(b);
source.addFeature(a);
Full example replicating the problem can be seen in jsFiddle.
The immediate problem with only part of the image being displayed was caused by you specifying value of offset; as per the documentation:
define the sub-rectangle to use from the original icon image. Default value is [0, 0].
Setting it to [0, 0] fixes the problem.
Another problem is that the anchor point of the image is not in "top-left", it's roughly in the middle of the icon. The value of anchor should be set to [0.5, 0.5]. The last problem is that the arrow head is attached to the start of the arrow, not the end.
var a = new ol.Feature({
geometry: new ol.geom.Point(end)
});
var b = new ol.style.Style({
image: new ol.style.Icon(({
anchor: [0.5, 0.5],
anchorOrigin: 'top-left',
offset: [0, 0],
scale: 1,
opacity: 1,
rotateWithView: false,
rotation: -rotation,
src: '//openlayers.org/en/v3.8.2/examples/data/arrow.png',
}))
});
I fixed your JSFiddle and posted here.