In a recent article I discussed how the open source geo, GIS and map library OpenLayers can be used in an Oracle JET application. That article shows how countries selected in a standard Oracle JET Checkbox Set component are represented on a world map. In this article, we take this one step further – or actually several steps. By adding interactivity to the map, we allow users to select a country on the world map and we notify JET components of this selection. The synchronization works both ways: if the user types the name of a country in a JET input text component, this country is highlighted on the world map. Note that the map has full zooming and panning capabilities.
The Gif demonstrates how the map is first shown with France highlighted. Next, the user types Chile into the input-text component and when that change is complete, the map is synchronized: Chile is highlighted. The user then hovers over Morocco – and the informational DIV element shows that fact. No selection is made yet. Then the user mouse clicks on Morocco. The country is selected on the map and the JET input-text component is synchronized with the name of the country. Subsequently, the same is done with India: hover and then select. Note: the map can easily support multi-country selection; I had to turn off that option explicitly (default behavior is to allow it).
The challenges I faced when implementing this functionality:
- add vector layer with countries (features)
- highlight country when mouse is hovering over it
- add select interaction to allow user to select a country
- communicate country selection event in map to “regular” JET component
- synchronize map with country name typed into JET component
The steps (assuming that the steps in this article are performed first):
- Create mapArea.html with input-text, informational DIV and map container (DIV)
- Create mapArea.js for the mapArea module
- Add a div with data bind for mapArea module to index.html
- Download a file in GEOJSON format with annotated geo-json geometries for the countries of the world
- Initialize the map with two layers – raster OSM world map and vector country shapes
- Add overlay to highlight countries that are hovered over
- Add Select Interaction – to allow selection of a country – applying a bold style to the selected country
- Update JET component from country selection
- Set country selection on map based on value [change] in JET component
And here is the code used to implement this: https://github.com/lucasjellema/jet-and-openlayers .
Create mapArea.html with input-text, informational DIV and map container (DIV)
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.4/ol-debug.css" /> <h2>Select Country on Map</h2> <div id="componentDemoContent" style="width: 1px; min-width: 100%;"> <div id="div1"> <oj-label for="text-input">Country</oj-label> <oj-input-text id="text-input" value="{{selectedCountry}}" on-value-changed="[[countryChangedListener]]"></oj-input-text> </div> </div> <div id="info"></div> <div id="map2" class="map"></div>
Create ViewModel mapArea.js for the mapArea module
define( ['ojs/ojcore', 'knockout', 'jquery', 'ol', 'ojs/ojknockout', 'ojs/ojinputtext', 'ojs/ojlabel'], function (oj, ko, $, ol) { 'use strict'; function MapAreaViewModel() { var self = this; self.selectedCountry = ko.observable("France"); self.countryChangedListener = function(event) { } ... } return new MapAreaViewModel(); } );
Add a DIV with data bind for mapArea module to index.html
...</header> <div role="main" class="oj-web-applayout-max-width oj-web-applayout-content"> <div data-bind="ojModule:'mapArea'" /> </div> <footer class="oj-web-applayout-footer" role="contentinfo"> ...
Download a file in GEOJSON format with annotated geo-json geometries for the countries of the world
I downloaded a GEOJSON file with country data from GitHub: https://github.com/johan/world.geo.json and placed the file in the directory src\js\viewModels of my JET application:
Initialize the map with two layers – raster OSM world map and vector country shapes
function MapAreaViewModel() { var self = this; self.selectedCountry = ko.observable("France"); self.countryChangedListener = function(event) { // self.selectInteraction.getFeatures().clear(); // self.setSelectedCountry(self.selectedCountry()) } $(document).ready ( // when the document is fully loaded and the DOM has been initialized // then instantiate the map function () { initMap(); }) function initMap() { var style = new ol.style.Style({ fill: new ol.style.Fill({ color: 'rgba(255, 255, 255, 0.6)' }), stroke: new ol.style.Stroke({ color: '#319FD3', width: 1 }), text: new ol.style.Text() }); self.countriesVector = new ol.source.Vector({ url: 'js/viewModels/countries.geo.json', format: new ol.format.GeoJSON() }); self.map2 = new ol.Map({ layers: [ new ol.layer.Vector({ id: "countries", renderMode: 'image', source: self.countriesVector, style: function (feature) { style.getText().setText(feature.get('name')); return style; } }) , new ol.layer.Tile({ id: "world", source: new ol.source.OSM() }) ], target: 'map2', view: new ol.View({ center: [0, 0], zoom: 2 }) }); }//initMap
Add overlay to highlight countries that are hovered over
Note: this code is added to the initMap function:
// layer to hold (and highlight) currently selected feature(s) var featureOverlay = new ol.layer.Vector({ source: new ol.source.Vector(), map: self.map2, style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#f00', width: 1 }), fill: new ol.style.Fill({ color: 'rgba(255,0,0,0.1)' }) }) }); var highlight; var displayFeatureInfo = function (pixel) { var feature = self.map2.forEachFeatureAtPixel(pixel, function (feature) { return feature; }); var info = document.getElementById('info'); if (feature) { info.innerHTML = feature.getId() + ': ' + feature.get('name'); } else { info.innerHTML = ' '; } if (feature !== highlight) { if (highlight) { featureOverlay.getSource().removeFeature(highlight); } if (feature) { featureOverlay.getSource().addFeature(feature); } highlight = feature; } }; self.map2.on('pointermove', function (evt) { if (evt.dragging) { return; } var pixel = self.map2.getEventPixel(evt.originalEvent); displayFeatureInfo(pixel); });
Add Select Interaction – to allow selection of a country – applying a bold style to the selected country
This code is based on this example: http://openlayers.org/en/latest/examples/select-features.html .
// define the style to apply to selected countries var selectCountryStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#ff0000', width: 2 }) , fill: new ol.style.Fill({ color: 'red' }) }); self.selectInteraction = new ol.interaction.Select({ condition: ol.events.condition.singleClick, toggleCondition: ol.events.condition.shiftKeyOnly, layers: function (layer) { return layer.get('id') == 'countries'; }, style: selectCountryStyle }); // add an event handler to the interaction self.selectInteraction.on('select', function (e) { //to ensure only a single country can be selected at any given time // find the most recently selected feature, clear the set of selected features and add the selected the feature (as the only one) var f = self.selectInteraction.getFeatures() var selectedFeature = f.getArray()[f.getLength() - 1] self.selectInteraction.getFeatures().clear(); self.selectInteraction.getFeatures().push(selectedFeature); });
and just after the declaration of self.map2:
... self.map2.getInteractions().extend([self.selectInteraction]);
Update JET component from country selection
Add to the end of the select event handler of the selectInteraction:
var selectedCountry = { "code": selectedFeature.id_, "name": selectedFeature.values_.name }; // set name of selected country on Knock Out Observable self.selectedCountry(selectedCountry.name);
Create
self.setSelectedCountry = function (country) { //programmatic selection of a feature var countryFeatures = self.countriesVector.getFeatures(); var c = self.countriesVector.getFeatures().filter(function (feature) { return feature.values_.name == country }); self.selectInteraction.getFeatures().push(c[0]); }
Set country selection on map based on value [change] in JET component
Implement the self.countryChangedListener that is refered to in the mapArea.html file in the input-text componentL:
self.countryChangedListener = function(event) { self.selectInteraction.getFeatures().clear(); self.setSelectedCountry(self.selectedCountry()) }
Create the following listener (for the end of loading the GeoJSON data in the countriesVector); when loading is ready, the current country value in the selectedCountry observable backing the input-text component is used to select the initial country:
var listenerKey = self.countriesVector.on('change', function (e) { if (self.countriesVector.getState() == 'ready') { console.log("loading dione"); // and unregister the "change" listener ol.Observable.unByKey(listenerKey); self.setSelectedCountry(self.selectedCountry()) } });
References
GitHub Repo with the code (JET Application) : https://github.com/lucasjellema/jet-and-openlayers .
Countries GeoJSON file – https://github.com/johan/world.geo.json
Open Layers Example of Select Interaction – http://openlayers.org/en/latest/examples/select-features.html
Open Layers API – Vector: http://openlayers.org/en/latest/apidoc/ol.source.Vector.html
Event Listener for OpenLayers Vector with GEOJSON source – https://gis.stackexchange.com/questions/123149/layer-loadstart-loadend-events-in-openlayers-3/123302#123302
Animated Gif maker – http://gifmaker.me/
OpenLayers 3 : Beginner’s Guideby Thomas Gratier; Erik Hazzard; Paul Spencer, Published by Packt Publishing, 2015
OpenLayers Book – Handling Selection Events – http://openlayersbook.github.io/ch11-creating-web-map-apps/example-08.html