Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active October 20, 2016 15:38
Show Gist options
  • Save bellbind/0df729bca4ad7f11fabc to your computer and use it in GitHub Desktop.
Save bellbind/0df729bca4ad7f11fabc to your computer and use it in GitHub Desktop.
[browser] drawing Japan map (with canvas)

Howto draw Japan map with JavaScript

(Map drawing algorithm itself is in script.js)

Acquiring the map source of Japan Prefectures

Get map path data source for rendering from the public-domain Natural Earth -> "Download" tab -> "Cultural" of "Large Scale data 1:10m" -> "Download states and provinces" of "Admin 1 - States Provinces"

$ wget http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/1
0m/cultural/ne_10m_admin_1_states_provinces.zip

The zip file includes several files of "shapefile" format:

$ unzip -t ne_10m_admin_1_states_provinces.zip 
Archive:  ne_10m_admin_1_states_provinces.zip
testing: ne_10m_admin_1_states_provinces.README.html   OK
testing: ne_10m_admin_1_states_provinces.VERSION.txt   OK
testing: ne_10m_admin_1_states_provinces.cpg   OK
testing: ne_10m_admin_1_states_provinces.dbf   OK
testing: ne_10m_admin_1_states_provinces.prj   OK
testing: ne_10m_admin_1_states_provinces.shp   OK
testing: ne_10m_admin_1_states_provinces.shx   OK
No errors detected in compressed data of ne_10m_admin_1_states_provinces.zip.

Processing map formats to GeoJSON with "gdal" utilities

Using "shape" files in the zip includes area data (prefectures all over the world)

$ unzip ne_10m_admin_1_states_provinces.zip

Pickup Japan prefectures to store as GeoJSON format with a command in gdal

$ brew install gdal
$ ogr2ogr -f GeoJSON -where `geonunit="Japan"` japan.geo.json \
        ne_10m_admin_1_states_provinces.shp

Overview of Prefectures GeoJSON structure

Japan Prefectures GeoJSON structure is:

{
  type: "FeatureCollection", 
  features: [ // 48 objects, 0 is "Japan minor island" (most props are null)
    {
      type: "Feature",
      properties: {
        ...
        name: "Hiroshima", //nul
        name_local: "広島県",//null
        type: "Ken", // null, 
        region: "Chugoku", //null, 
        gn_name: "Hiroshima-ken",
        provnum_ne: 11, // 0, 2-49 ?
        gn_a1_code: "JP.11", //null, JP.01 - JP.47
        gns_adm1: "JA11", //null, JA01-JA47
        ...
      },
      geometry:  {
        type: "MultiPolygon",
        coordinates: [
          [
            [
              [longitude, latitude], ... //0th and last are same (closed path)
            ] // 1 element?
          ],
          ...
        ]
      }
    },
    ...
    {
       type: "Feature", 
       properties: {...},
       geometry: {
         type: "Polygon",
         coordinates: [
           [
             [longitude, latitude], ... //0th and last are same (closed path)
           ] // 1 element?
         ]
       }
    }
  ]// 48
}
  • NOTE: features[20].properties.name_local is null (should fix as "静岡県")
  • NOTE: In "states and provinces" data, only "MultiPolygon" and "Polygon" are used.
  • See: GeoJSON spec of the geometry structures

Reference

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="script.js"></script>
</head>
<body><body>
</html>
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
"use strict";
window.addEventListener("load", ev => {
const canvas = document.createElement("canvas");
canvas.style.backgroundColor = "hsl(240, 20%, 20%)";
canvas.width = 600, canvas.height = 600;
document.body.appendChild(canvas);
const c2d = canvas.getContext("2d");
// set viewport at lat 15-55 and long 115-155
const viewport = {lngC: 135, latC: 35, lngW: 40, latH: 40};
function viewportTrans() {
// border {l:0, t:0, r:600, b:600} => {l:0, t:0, r:40, b:-40}
c2d.scale(canvas.width / viewport.lngW,
-canvas.height / viewport.latH);
// border {k:0, t:0, r:40, b:-40} => {l:115, t:55, r:155. b:15}
c2d.translate(-(viewport.lngC - viewport.lngW / 2),
-(viewport.latC + viewport.latH / 2));
c2d.lineWidth *= 40 / canvas.width;
//c2d.strokeRect(115, 15, 40, 40); // check bound box
}
// projections to viewport
function mercator(lnglat) {
// mercator mapping
const y = (lat) => Math.atanh(Math.sin(lat * 2 * Math.PI/ 360));
const maxv = viewport.latC + viewport.latH / 2;
const minv = viewport.latC - viewport.latH / 2;
const maxy = y(maxv), miny = y(minv);
const scaley = viewport.latH / (maxy - miny);
const projy = (y(lnglat[1]) - miny) * scaley + minv;
return [lnglat[0], projy];
}
function equidistant(lnglat) {
const r = (grad) => grad / 180 * Math.PI;
const {latC, lngC} = viewport;
const sLatC = Math.sin(r(latC)), cLatC = Math.cos(r(latC));
const eqdist = (lng, lat) => {
const lngT = lng - lngC, latT = lat - latC;
const sLat = Math.sin(r(lat)), cLat = Math.cos(r(lat));
const cLng = Math.cos(r(lngT)), sLng = Math.sin(r(lngT));
const rho = Math.acos(sLatC * sLat + cLatC * cLat * cLng);
const theta = Math.atan2(cLat * sLng,
cLatC * sLat - sLatC * cLat * cLng);
return [rho * Math.sin(theta), rho * Math.cos(theta)];
};
const ew = eqdist(viewport.lngC, viewport.latC + viewport.latH / 2)[1];
const scale = viewport.latH / 2 / ew;
const exy = eqdist(lnglat[0], lnglat[1]);
return [exy[0] * scale + viewport.lngC,
exy[1] * scale + viewport.latC];
}
// drawing shapes
function drawPolygon(polygon, color) {
c2d.save();
viewportTrans();
c2d.fillStyle = color;
//polygon.map(path => path.map(mercator)).forEach(path =>{
polygon.map(path => path.map(equidistant)).forEach(path =>{
c2d.beginPath();
c2d.moveTo(path[0][0], path[0][1]);
path.slice(1).forEach(point => c2d.lineTo(point[0], point[1]));
c2d.closePath();
c2d.fill();
//c2d.stroke();
});
c2d.restore();
}
function drawMultiPolygon(multi, color) {
multi.forEach(polygon => drawPolygon(polygon, color));
}
function prefColor(props) {
const buf = new TextEncoder().encode(props.name || "", "utf-8");
const hue = Array.from(buf).reduce((s, v) => s + v, 0) % 360;
//console.log(hue, props.name_local);
return `hsl(${hue}, 50%, 50%)`;
}
fetch("japan.geo.json").then(res => res.json()).then(geo => {
//patch to fix data
geo.features[0].properties.name_local = "Islands";
geo.features[0].properties.name_local = "その他諸島";
geo.features[20].properties.name_local = "静岡県";
geo.features.forEach(f => {
const {type, coordinates} = f.geometry;
const color = prefColor(f.properties);
switch (type) {
case "Polygon": return drawPolygon(coordinates, color);
case "MultiPolygon": return drawMultiPolygon(coordinates, color);
default: throw Error(`not supported type: ${type}`);
}
});
}).catch(err => console.log(err));
}, false);
@bellbind
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment