Skip to content

Instantly share code, notes, and snippets.

@nl-hugo
Last active July 23, 2018 13:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nl-hugo/21305b18ca50bede3bdc843549b8832f to your computer and use it in GitHub Desktop.
Save nl-hugo/21305b18ca50bede3bdc843549b8832f to your computer and use it in GitHub Desktop.
SE Asia map with GPX data
height: 650
license: MIT
node_modules
npm-debug.log
shapes/

A visualisation of GPX waypoints A test case to automate download and conversion of CBS maps into a D3.js map.

This block packs the following features:

  • Locations in the gpx file are encoded in the TopoJSON file by the npm commands in package.json.
  • Colorizes countries (visited, not visited) based on locations in the gpx file. Note that this is not always accurate due to polygon simplification.
  • The gpx track is divided into segments for each pair of subsequent locations. Each of the segments can be styled using the 'sym' tag of the location.
  • Filters 'major' and 'minor' waypoints based on the 'description' tag in the gpx file. All waypoints are used to draw the path, but only major waypoints have mouseover interactivity.
  • Voronoi polygons for easier selection.
  • Mouseover reveals the location description from the gpx file.
  • Click-to-zoom using the voronoi polygons.

To reproduce the json file for the map: npm install npm run map

Resources:

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!DOCTYPE html>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet">
<style>
svg {
font-family: "Montserrat", sans-serif;
display: block;
font-size: 9px;
line-height: 1.5em;
}
.borders {
fill: none;
stroke: #ccc;
stroke-linejoin: round;
}
.country {
fill: none;
}
.country.visited {
fill: #efefee;
}
.dimmed {
fill-opacity: 0.8;
}
.country-label {
fill: none;
}
.country-label.visited {
fill: #777;
fill-opacity: .4;
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
text-anchor: middle;
}
path.segment {
fill: none;
stroke: steelblue;
}
path.segment-plane {
stroke-dasharray: 2 4;
}
.polygons {
fill: none;
pointer-events: all;
}
.waypoint.dimmed {
opacity: 0.5;
}
.waypoint path {
fill: none;
stroke: steelblue;
}
.waypoint text {
fill: #777;
stroke: none;
}
.g-legend .waypoint {
opacity: 0.0;
font-size: 11px;
}
.g-legend .highlighted {
opacity: 1.0;
/*fill: none;*/
}
.g-legend .legend-title {
font-weight: bold;
}
</style>
<body>
<svg width="960" height="650"></svg>
</body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script>
let svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
margin = {top: 20, right: 20, bottom: 20, left: 20},
centered;
let projection = d3.geoMercator()
.center([129, -20])
.translate([width / 2, height / 2])
.scale(width / 2);
let path = d3.geoPath()
.projection(projection)
.pointRadius(2);
let voronoi = d3.voronoi()
.x(d => projection(d.geometry.coordinates)[0])
.y(d => projection(d.geometry.coordinates)[1])
.extent([[0, 0], [width, height]]);
let g = svg.append("g")
.attr("class", "g-map");
let l = svg.append("g")
.attr("class", "g-legend")
.attr("transform", "translate(" + margin.left + "," + (height - 100) + ")");
d3.json("asia-map.json", (error, map) => {
if (error) return console.error(error);
let countries = topojson.feature(map, map.objects.countries).features;
let route = topojson.feature(map, map.objects.route).features;
// mark countries as 'visited' when it contains a location from the gpx file
// may not always be accurate, due to GeoJSON simplification
countries.forEach(d => d.properties.visited = route.some(e => d3.geoContains(d, e.geometry.coordinates)));
// countries
g.append("g")
.attr("id", "country")
.selectAll("path")
.data(countries)
.enter().append("path")
.attr("d", path)
.attr("class", d => "country country-" + d.id);
d3.selectAll(".country").classed("visited", d => d.properties.visited);
g.append("path")
.datum(topojson.mesh(map, map.objects.countries))
.attr("class", "borders")
.attr("d", path);
g.selectAll(".country-label")
.data(countries)
.enter().append("text")
.attr("class", d => "country-label " + d.id)
.attr("transform", d => "translate(" + path.centroid(d) + ")")
.attr("dy", ".35em")
.text(d => d.properties.country);
d3.selectAll(".country-label").classed("visited", d => d.properties.visited);
// route
let segments = g.selectAll(".segment")
.data(pathSegments(route))
.enter().append("path")
.attr("d", d => path({ type: "LineString", coordinates: [d[0].geometry.coordinates, d[1].geometry.coordinates] }))
.attr("class", d => "segment segment-" + d[1].properties.sym);
// waypoints
let waypoint = g.selectAll(".waypoint")
.data(route.filter(d => d.properties.desc)) // with description only
.enter().append("g")
.attr("class", d => "waypoint waypoint-" + d.properties.name);
waypoint.append("path")
.attr("d", path);
waypoint.append("text")
.attr("transform", d => "translate(" + path.centroid(d) + ")")
.attr("y", 10)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(d => d.properties.name);
// voronoi overlay
g.selectAll(".polygons")
.data(voronoi.polygons(route.filter(d => d.properties.desc)))
.enter().append("path")
.attr("class", "polygons")
.attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; })
.style("stroke", "#A074A0") // show the cells
.on("mouseover", function(d) {
d3.selectAll(".waypoint").classed("dimmed", true);
d3.selectAll(".waypoint-" + d.data.properties.name).classed("dimmed", false).classed("highlighted", true);
})
.on("mouseout", function() { d3.selectAll(".waypoint").classed("dimmed highlighted", false); })
.on("click", clicked);
// waypoint descriptions
let description = l.selectAll(".waypoint")
.data(route.filter(d => d.properties.desc)) // with description only
.enter().append("g")
.attr("class", d => "waypoint waypoint-" + d.properties.name);
description.append("text")
.attr("x", 16)
.attr("y", 6)
.attr("dy", ".35em")
.attr("class", "legend-title")
.text(d => d.properties.name);
description.append("text")
.attr("x", 16)
.attr("y", 30)
.attr("dy", ".35em")
.text(d => d.properties.desc);
});
// Produce an array of two-element arrays [x, y] for each segment of values.
function pathSegments(values) {
let i = 0, n = values.length, segments = new Array(n - 1);
while (++i < n) segments[i - 1] = [values[i - 1], values[i]];
return segments;
}
function clicked(d) {
let dx, dy, k, i;
if (d && centered !== d) {
let centroid = projection(d.data.geometry.coordinates);
dx = centroid[0];
dy = centroid[1];
k = 4;
centered = d;
} else {
dx = width / 2;
dy = height / 2;
k = 1;
centered = null;
}
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -dx + "," + -dy + ")");
}
</script>
{
"name": "se-asia-map",
"version": "0.0.1",
"description": "SE Asia map",
"keywords": [
"Visualization",
"Map",
"D3.js"
],
"author": "Hugo Janssen <nl-hugo@hugojanssen.nl>",
"private": true,
"license": "MIT",
"devDependencies": {
"download-cli": "~1.0",
"topojson": "~1.6",
"rimraf": "latest",
"ogr2ogr": "1.0.1"
},
"scripts": {
"clean": "rimraf shapes",
"download": "download --extract --out shapes https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip",
"shape:countries": "ogr2ogr -f GeoJSON -t_srs EPSG:4326 -clipdst 71 20 180 -51 shapes/countries.json shapes/ne_10m_admin_0_countries.shp",
"shape:wpts": "ogr2ogr -f GeoJSON -t_srs EPSG:4326 shapes/route.json route.gpx track_points -fieldTypeToString DateTime",
"shape": "npm run shape:wpts && npm run shape:countries",
"topojson": "topojson --id-property ADM0_A3 -p country=NAME -p time -p name -p sym -p desc -s 3e-9 -o asia-map.json -- shapes/countries.json shapes/route.json",
"map": "npm run download && npm run shape && npm run topojson && npm run clean"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Galileo Offline Maps Pro v3.4.7(3740)" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"><metadata><name>AUS-NZ-SEA</name><desc></desc><time>2018-09-16T16:18:48.027Z</time></metadata>
<trk>
<name>Test</name>
<desc>Test tracj</desc>
<type>TrackStyle_3a96ffc8</type>
<trkseg>
<trkpt lat="52.370918" lon="4.892436"><ele>100.018</ele>
<name>Amsterdam</name>
<sym></sym>
<time>2016-09-16T07:48:34.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-31.950197" lon="115.860381"><ele>100.018</ele>
<name>Perth</name>
<sym>plane</sym>
<desc>A description for Perth</desc>
<time>2016-09-16T07:48:34.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-34.934987773899998" lon="138.60000484099999"><ele>103.632</ele>
<name>Adelaide</name>
<sym>car</sym>
<desc>A description for Adelaide</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-37.820031312300003" lon="144.975016235"><ele>103.632</ele>
<name>Melbourne</name>
<sym>car</sym>
<desc>A description for Melbourne</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-33.9200109672" lon="151.185179809"><ele>103.632</ele>
<name>Sydney</name>
<sym>car</sym>
<desc>A description for Sydney</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-43.535021595389999" lon="172.63002315616001"><ele>103.632</ele>
<name>Christchurch</name>
<sym>plane</sym>
<desc>A description for Christchurch</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-36.848054894930002" lon="174.76302698708"><ele>103.632</ele>
<name>Auckland</name>
<sym>car</sym>
<desc>A description for Auckland</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-8.732067" lon="115.167623"><ele>103.632</ele>
<name>Denpasar</name>
<sym>plane</sym>
<desc>A description for Denpasar</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-8.348356" lon="116.038438"><ele>103.632</ele>
<name>Gili T</name>
<sym>boat</sym>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="3.583907" lon="98.672585"><ele>103.632</ele>
<name>Medan</name>
<sym>plane</sym>
<desc>A description for Medan</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
</trkseg>
</trk>
</gpx>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment