A cartographic application is of a level of complexity greater than a simple graphic plot, as we have described earlier. We will not try to code it ourselves, even if, for most of the libraries (open source or proprietary), they just draw on a <canvas>
. There are various methods for drawing lines or polygons or pixel matrices, for computing topological relations such as sharing a vertex, being included or intersecting each other, and also for computing distances, transforming geographical coordinates between map projections, etc.
It is not our intention to detail any of these numerous features. We will merely present the basic use of one library, open source, which is complete enough to sample all of the most useful features, in a way similar to those of proprietary libraries such as Google, Bing and Yahoo. That library is OpenLayers, code licensed under the “2-Clause BSD”, with a very active community (2006–2017): at the time of writing this book, the latest version is v4.6.5 (March 18, 2018).
The requirements are the library, the basic styles, and a simple <div>
element (or a <canvas>
) that will be used by the library:
<link rel="stylesheet" href=
"https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.4/ol.css">
<body>
<div id="olmap" class="olmap"></div>
<script src=
"https://cdnjs.cloudflare.com/ajax/libs/openlayers/4.6.4/ol.js">
</script>
In this application, we use data from four sources:
The three lists are JSON files. We must make a join, in a way similar to the electoral application, between the list of partnerships and the list of institutions of Quebec (respectively, France). The common key is a normalized label of the institution (the process of normalizing is not described). The first join links the partnerships data with the coordinates of the single point associated with each institution. We can define an activity index whose value is the number of partnerships of each institution.
What is really specific in this application is the second join, between the coordinates of the points representing the institutions and the points actually displayed on the canvas by the OpenLayers library. That cartographic join allows us to transform the activity of each institution into a marker on the map, displaying that activity: the marker is a circle whose center is the point representing the institution and whose radius is proportional to the activity index.
Here is an example of what we can display on the map of Québec. One similar map can be drawn to display the same activity on the France side.
In this chapter, we will focus on the cartographic join. There are two main operations: the preparation of the map and the display of the markers.
The sequence of operations to be performed is pretty much the same with every available cartographic library. This code details the sequence with OpenLayers:
function prepareMap(latlon, zoom) {
const sourceUnivs = new ol.source.Vector({}); // empty source
const layerUnivs = new ol.layer.Vector({ // markers's layer
title: 'Universities',
source: sourceUnivs
});
const center = ol.proj.fromLonLat([latlon[1],latlon[0]]);
const map = new ol.Map({
target: canvasName,
layers: [new ol.layer.Tile({source: new ol.source.OSM()})],
view: new ol.View({center, zoom})
});
map.on('singleclick', function(e){/* handler: click on map */});
}
The construction map object requires several predefined objects:
<canvas>
element in the HTML DOM;ol.source.OSM()
, a given feature of the openlayers library. Moreover, the specific layer for the markers that we will compute later as circles, must be set as an ol.layer.Vector()
feature: we name it Universities
and we attach its source (see below);ol.source.Vector({})
and leave empty ({}) at first;center
location and a zoom
factor;For short, we can say that, for making a map, we need one or several sources to be displayed in as many layers, transposed within a view, displayed onto a target, and we can add specific controls and display some additional overlays.
For each university, we want to compute a marker representing its activity in terms of its number of partnerships. The marker is, a circle, that is, a geometric feature in the vector source, defined by a center and a radius. In the code below, the object univMark = new ol.Feature({geometry, name, properties})
is that feature, initialized with an ol.geom.Circle()
geometry, with an identified name
, and some additional properties: {id,np,content}
, which can be used for a popup overlay.
Here is a sample code for the creation of these circles:
const fill = new ol.style.Fill({color:'rgba(255,0,0,0.4)'}),
const stroke = new ol.style.Stroke({color:'rgb(0,0,0,1), width:1})
function makeMarker(x, y, np, content, id, fill, stroke) {
let rad = np * 150 + 2500; // @np: number of partners
let cen = ol.proj.fromLonLat([y, x]); // @x, @y: point
const univMark = new ol.Feature({
"geometry": new ol.geom.Circle(cen, rad, 'XY'),
"name": id,
"properties": {id, np, content}
});
univMark.setId(id);
univMark.setStyle(new ol.style.Style({fill, stroke}));
sourceUnivs.addFeature(univMark);
return univMark;
}
Each marker is pushed into an array and we use its index in that array as its id
(used in several places above), which we can relate uniquely to the institution, of which it is the marker. We attach a style (stroke
and fill
) to each marker. In this case, the style is constant, but it could be computed (e.g. a varying color). The variable content
is defined elsewhere.
In Figure 13.1, the markers are created with the full list of partnerships of every institution. It is interesting to allow the user to make a selection, according to one or several filters. After each selection, we start with erasing the previous markers, then we compute the new markers: some universities are filtered out, the radius is recomputed according to the filtered partnerships.
Finally, the map must be “refreshed”. Here is the code:
const newMarkers = []; // start with empty
// partnerships: set by JSON reading
// selectedUnivs: from the handler of the selection form
clearMarkers(newMarkers); newMarkers.length = 0;
selectedUnivs.forEach(function(u) { // reset markers
let np = u.parts.length;
let content = makePopupToDisplay(u.parts, partnerships);
newMarkers.push( makeMarker(u.x, u.y, np, content, u.u) );
}
layerUnivs.setMap(map); // refresh the map
Here are the results of selecting the partnerships with the interface form on the left: only major topics in engineering or biology are selected. The next map shows the selection over the keyword image (and displays topics on the right).
We have learnt some bases of the SVG language, which can be:
xmlns
attribute), hence benefiting from DOM access and CSS rules mechanisms.This application has been used already as illustration in Part 2.
Many SVG data are available on the Internet, and especially geographic maps, by country, region or continent. Once selected, the data file can be read by a simple AJAX request, in plain text mode.
We use an SVG file, from WikiMedia commons, of the borders of the countries of the European continent and the Mediterranean rim. It is several megabytes in size, which is voluminous for text data, meant to be processed initially as a string. Therefore, we have first used a preprocessing that degrades gently the precision of the coordinates and the total number of points used for representing the borders. We can easily find online such “svg-optimizers”: the result is a 340 kB file.
The main element in that file is the <path>
element, with its two attributes:
id=number
(supposedly unique);d= "M83.6619 151.193c-0.2476,0.0627 …z"
, which generally starts with M
and ends with z
, which indicates a polygon in the coordinates that complies with the chosen viewport.Each country is represented by one <path>
(simplest case), or several <path>
, wrapped into a graphic group <g id=num></g>
, and sometimes nested groups. The United Kingdom is in that most complex category (mainland, NI, Gibraltar and a nested group for islands). Each group can be individually selected. Disputed territories, such as Crimea, or the Golan Heights, are represented that same way.
There is a second source, a JSON file containing information about each country:
{
"countries": [
{
"name": "Germany",
"capitale": "Berlin",
"EUmember": "1958",
"money": "euro",
"Shengen": "Shengen", "population": "81.5",
"gdp": "3874.5"
},
{}
]}
The goal is twofold:
In Chapter 8, we have described the construction of HTML elements from JavaScript using the “lazy approach”. We can do something similar with the SVG document. It is very simple, perfect for the lazy among us (I often use it):
<div class="svgExt"><!-- here: the SVG code --></div>
<script>
// const textSVG = … in the callback of a file request document.querySelector(".svgExt").innerHTML = textSVG;
</script>
the entire SVG text, from <svg>
to </svg>
, is copied into the <div>
; for instance, if the file has been limited to Monaco, we get:
<div class="svgExt">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 230 192.5">
<g class="maing" transform="translate(-9,-9)">
<rect x="10" y="10.15" width="228.02" height="189.85">
<g class="pays memberstate" id="mc">
<path class="p0" d="M83.6619 151.193c-0.2476,0.0627 -
0.5932,0.1566 -0.9258,0.2615l0 0 0.0691 -0.4229c0.2099,-0.203
0.4828,-0.1426 0.7337,-0.1075 0.0318,0.0044 0.0933,0.259
0.123,0.2689l0 0z" />
</g>
</g>
</svg></div>
This is the second, constructive, approach, where the SVG document is read from file, parsed and each element is added to the HTML DOM, one by one. We must use a special createElement
method:
const NSuri = "http://www.w3.org/2000/svg"; // namespace svg2000
const svg = document.createElementNS(NSuri, 'svg');
// document.createElement('svg') doesn't work
There is a namespace specification for every SVG element in svg2000: <g>
, <rect>
, <path>
, etc This constructive approach is not difficult to code but takes more time. However, we can control every step, and add as many appropriate handlers as we may need.
Once the embedding is done, by either approach, the HTML layout engine takes care of the display using CSS rules: it fills in a blue <rect>
for the ocean because of: rect {fill:"light blue"}
; draws each country with a white background and a black border, because of: path {stroke:"black"; fill:"white"}
; etc.
The identifier chosen for a <g>
group representing a country is the standard “a_code
”, the Internet domain code of that country. Therefore, we need to join that code with the name of the country, as used in the JSON data file. We use a new JSON file for that purpose, containing couples:
{"a_code":"de", "name":"Germany"
{"a_code":"mc", "name":"Monaco"}
The a_code
is the value chosen for the id
attribute of the <g>
elements:
<g id="mc"><path class="pays" d=" … " /></g>
We know how to handle such multiple joins.
Here is a simple example: let us select the countries whose money is the euro. The name of the country is joined with the acode
and that acode
gives access to the right <g>
group. Given the list of these <g>
, we can “mark” them, for instance by attributing an additional class name: “euro”. Then we can retrieve them and have access to all the concerned <path>
elements, and we can change an attribute of their style, for instance the fill
color.
Array.from(document.querySeletorAll("g.euro")) // select <g>
.forEach(function(g){
Array.from(g.querySelectorAll("path")) // select <path> in g
.forEach(function(p){p.path.style.fill = 'yellow';});// color
});
Here is a very useful feature which we can easily implement: obtaining the coordinates attached to an entry of the Wikipedia encyclopedia.
We can use a direct access to the data through the Wikipedia API1:
https://en.wikipedia.org/w/api.php?data
followed by the appropriate parameters in the data fields:
action=query the action type;
prop=coordinates the property to select;
format=json the ouput format;
titles= … name(s) of the page(s) to browse.
For example:
https://en.wikipedia.org/w/api.php?action=query&prop=coordinates&format=json&titles=McGill_University
The result is:
{"query":{
"normalized":[{"from":"McGill_University","to":"McGill_University"}],
"pages":{ "7954643":{
"pageid":7954643, "ns":0,
"title":"McGill University",
"coordinates":[{"lat":45.504166,"lon":-73.574722,"globe":"earth"}]
}}
}}
Then we can parse the result, and join the institutionʼs coordinates with extra information on the same institution.
EXERCISE.– use Fetch
and what you have learned so far, to plot the capital cities of a few European countries (with a radius proportional to population size).
It is possible to group several pages in the same request (pipe sign):
https://en.wikipedia.org/w/api.php?action=query&prop=coordinates&format=json&titles=McGill_University|Universite_Laval
3.142.249.42