Now we have the knowledge we need to build our final example. We will add some interactivity to our countries layer by highlighting the country under the mouse with a different style—specifically, we will:
var countries = new ol.layer.Vector({
source: new ol.source.GeoJSON({
projection: 'EPSG:3857',
url: '../assets/data/countries.geojson'
})
});
var center = ol.proj.transform([0, 0], 'EPSG:4326', 'EPSG:3857');
var view = new ol.View({
center: center,
zoom: 1,
});
var map = new ol.Map({
target: 'map',
layers: [countries],
view: view
});var baseTextStyle = {
font: '12px Calibri,sans-serif',
textAlign: 'center',
offsetY: -15,
fill: new ol.style.Fill({
color: [0,0,0,1]
}),
stroke: new ol.style.Stroke({
color: [255,255,255,0.5]
width: 4
})
};
var highlightStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
color: [255,0,0,0.6],
width: 2
}),
fill: new ol.style.Fill({
color: [255,0,0,0.2]
}),
zIndex: 1
});function styleFunction(feature, resolution) {
var style;
var geom = feature.getGeometry();
if (geom.getType() == 'Point') {
var text = feature.get('text');
baseTextStyle.text = text;
var isoCode = feature.get('isoCode').toLowerCase();
style = new ol.style.Style({
text: new ol.style.Text(baseTextStyle),
image: new ol.style.Icon({
src: '../assets/img/flags/'+isoCode+'.png'
}),
zIndex: 2
});
} else {
style = highlightStyle;
}
return [style];
}var featureOverlay = new ol.FeatureOverlay({
map: map,
style: styleFunction
});pointermove event and manage the features in our feature overlay based on where the mouse is. This is a pretty big function that exercises our knowledge of geometries from the previous chapter:map.on('pointermove', function(browserEvent) {
featureOverlay.getFeatures().clear();
var coordinate = browserEvent.coordinate;
var pixel = browserEvent.pixel;
map.forEachFeatureAtPixel(pixel, function(feature, layer) {
if (!layer) {
return; // ignore features on the overlay
}
var geometry = feature.getGeometry();
var point;
switch (geometry.getType()) {
case 'MultiPolygon':
var poly = geometry.getPolygons().reduce(function(left, right) {
return left.getArea() > right.getArea() ? left : right;
});
point = poly.getInteriorPoint().getCoordinates();
break;
case 'Polygon':
point = geometry.getInteriorPoint().getCoordinates();
break;
default:
point = geometry.getClosestPoint(coordinate);
}
textFeature = new ol.Feature({
geometry: new ol.geom.Point(point),
text: feature.get('name'),
isoCode: feature.get('iso_a2').toLowerCase()
});
featureOverlay.addFeature(textFeature);
featureOverlay.addFeature(feature);
});
}
As you can see, feature overlays make it very simple to create an interactive experience with vector layers. There are a few new concepts in this example, as well as some old ones, so let's review the code step by step.
Step 1 should be pretty familiar to you now—we are creating a vector layer with a GeoJSON source, and adding it to a map with a view centered on 0,0.
In step 2, we set up some styles for our feature overlay to use. There are two styles—one for text and one for polygon highlighting. Notice that the baseTextStyle is an object literal, not a new instance of ol.style.Text. When you create an instance of ol.style.Text, the text to be drawn needs to be passed to that object and you can't change the text after it has been created. The style function allows us to create text styles with the text of the current feature, but we'll need to specify the other options. Since all labels will share the other text options, we can set them up once and just refer to them later in the style function. The options we've specified here are to center align the text, offset it up by 15 pixels (recall that a positive offsetY moves the text down), and provide a fill and stroke color. For the text, the stroke is rendered around the outside of each character so we set a wide, semitransparent stroke to make the text stand out from the map beneath it.
The highlightStyle is straightforward— a fill and stroke style for the highlighted polygon.
The style function needs to be defined before we can use it to create the feature overlay, so in step 3, we defined it. Recall that the style function receives the feature being rendered as the first argument. We drew two types of features, points, and polygons, with two different styles. So, the first thing we did was get the feature's geometry and check to see whether it was a point. If it is a point, we create a new text style and a new icon style specific to the current feature. We got the feature's text property and combined it with the baseTextStyle object to create the text style. Next, we got the isoCode property and used it to create a URL to the flag for the country (the flag icons are conveniently named using the two-letter ISO country code) for a new icon style. Then, we created a new style object for the current feature. If the feature is a polygon, it's much simpler—all we need to do is return the highlightStyle object. Finally, we returned an array containing the style (recall that style functions are required to return arrays for performance reasons).
Step 4, by comparison, was very short! We created a new feature overlay and configured it with the map object and style function. It's really the style function, and step 5, that do all the work.
Step 5 added a handler for the map's pointermove event; so, we could find the feature closest to the mouse and add it to the feature overlay. We actually wanted to highlight two features— the polygon itself and a point at the center of the polygon. It turns out that getting this center point is a bit tricky. Let's review the code carefully.
Line 1 of code in the handler clears any existing features in the feature overlay. It is much easier to retrieve the collection and clear it than to remove individual features in our case.
The browserEvent object provides us with the map coordinate and pixel that the event happened at. We used the pixel location with ol.Map classes forEachFeatureAtPixel function on the next line to retrieve all features at that location from our vector layer. This function invokes a callback function for every feature at the pixel location, providing both the feature and the layer that the feature was found on. The layer parameter may be null if the feature was found on a FeatureOverlay.
Inside our callback function, we tested first to see if the feature was actually on a layer before proceeding. Then we needed to find both the geometry and the center point of the geometry. If all the country features were polygons, we could simply call getInteriorPoint() to retrieve the center and we would be done. Unfortunately, we didn't know what type of features we were dealing with—some of the features in the country data were actually MultiPolygons, and we needed to handle them differently. The switch statement chooses a path based on the type of the feature. Let's look at each case separately.
The first case was for MultiPolygon. As the name suggests, a MultiPolygon class contains multiple polygons (a country and some islands perhaps) and there isn't a convenient way to determine the center of a group of polygons. Instead, a MultiPolygon class has several centers, one for each of its constituent polygons. The getInteriorPoints() method returns the center points for us. We only really wanted a single label though. One way is to get all the interior points and use the first one. The problem with this approach is that there is no particular order to the polygons, and it looks odd to label some random island off the coast rather than the major landmass of a given country. To get around this, we wanted our label to appear at the center of the largest polygon. To get the largest polygon, we first got the array of polygons and then reduced that array to a single value with a function that compared two polygons based on their area. The reduce method is a standard method of JavaScript arrays. Once we've found the largest polygon, we can ask for its interior point.
The second case is for polygon, and was much simpler— we just needed the interior point of the polygon and we were done!
The final case was the default case. While it isn't strictly needed, it is good practice to include a default case in switch statements. The getClosestPoint() method is available on all geometry types and is a safe fallback for our default case.
Now that we had a point coordinate at the best location we could determine for the feature under the mouse, we created a new Feature and provided the point geometry for its location. We also added two properties—text (that we displayed at the point) and isoCode (that we will use to find the flag icon for the country).
Finally, we added both features to the feature overlay so that when the map is next rendered, the country under the mouse will be highlighted in red and display the flag and country name at the center.