|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset='utf-8' /> |
|
<title>Dynamic Map 1:50m</title> |
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js'></script> |
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/topojson/3.0.0/topojson.min.js'></script> |
|
</head> |
|
|
|
<style> |
|
body { |
|
margin: 0; |
|
overflow: hidden; |
|
} |
|
|
|
</style> |
|
|
|
<body> |
|
<div id='map'></div> |
|
<script> |
|
var style = { |
|
'background': { |
|
'fill-color': '#000' |
|
}, |
|
'countries': { |
|
'fill-color': '#555', |
|
'line-color': '#333', |
|
'line-width': 1 |
|
}, |
|
'states': { |
|
'line-color': '#777' |
|
}, |
|
'places': { |
|
'circle-color': '#999', |
|
'circle-radius': 2.5, |
|
'circle-stroke-color': '#000', |
|
'circle-stroke-width': 1, |
|
'text-color': '#999', |
|
'text-stroke-color': '#000', |
|
'text-stroke-width': 1, |
|
'text-font': 'Helvetica', |
|
'text-size': '13px', |
|
'text-justify': 'center', |
|
'text-baseline': 'bottom' |
|
} |
|
}; |
|
|
|
const convertZoomLevelToMercator = (zoomLevel) => |
|
Math.pow(2, 8 + zoomLevel) / 2 / Math.PI; |
|
|
|
const convertZoomLevelFromMercator = (zoomLevelInMercator) => |
|
Math.log(zoomLevelInMercator * 2 * Math.PI) / Math.LN2 - 8; |
|
|
|
var bbox; |
|
|
|
var width = window.innerWidth, |
|
height = window.innerHeight; |
|
|
|
var dpr = window.devicePixelRatio; |
|
|
|
var canvas = d3.select('#map').append('canvas') |
|
.attr('width', width * dpr) |
|
.attr('height', height * dpr) |
|
.style('width', width + 'px') |
|
.style('height', height + 'px'); |
|
|
|
var ctx = canvas.node().getContext('2d'); |
|
ctx.scale(dpr, dpr); |
|
|
|
var minZ; |
|
|
|
var transform = d3.geoIdentity() |
|
.clipExtent([[0, 0], [width, height]]); |
|
|
|
var simplify = d3.geoTransform({ |
|
point: function(x, y, z) { |
|
if (z >= minZ) { |
|
this.stream.point(x, y); |
|
} |
|
} |
|
}); |
|
|
|
var zoom = d3.zoom() |
|
.on('zoom', zoomed); |
|
|
|
var path = d3.geoPath() |
|
.projection({ |
|
stream: function(s) { |
|
return simplify.stream(transform.stream(s)); |
|
} |
|
}) |
|
.context(ctx); |
|
|
|
var pathPlace = d3.geoPath() |
|
.pointRadius(style.places['circle-radius']) |
|
.projection(transform) |
|
.context(ctx); |
|
|
|
var land, |
|
border, |
|
states, |
|
places; |
|
|
|
d3.json('world-50m.json', function(error, data) { |
|
if (error) throw error; |
|
bbox = topojson.bbox(data), |
|
bbox.width = bbox[2] - bbox[1], |
|
bbox.height = bbox[3] - bbox[1]; |
|
|
|
data = topojson.presimplify(data); |
|
land = topojson.merge(data, data.objects.countries.geometries); |
|
border = topojson.mesh(data, data.objects.countries, function(a, b) { return a !== b; }); |
|
|
|
d3.json('states-50m.json', function(error, data) { |
|
if (error) throw error; |
|
data = topojson.presimplify(data); |
|
states = topojson.mesh(data, data.objects.states, function(a, b) { return a !== b; }); |
|
|
|
d3.json('places-10m.json', function(error, data) { |
|
if (error) throw error; |
|
places = topojson.feature(data, data.objects.places); |
|
|
|
let p = d3.geoMercator()([0, 0]), |
|
s = height / bbox.height; |
|
|
|
zoom |
|
.scaleExtent([s, 1 << 10]); |
|
|
|
canvas |
|
.call(zoom) |
|
.call(zoom.transform, d3.zoomIdentity |
|
.translate(width / 2, height / 2) |
|
.scale(s) |
|
.translate(-p[0], -p[1])); |
|
}); |
|
}); |
|
}); |
|
|
|
function zoomed() { |
|
let t = d3.event.transform; |
|
let z = Math.floor(convertZoomLevelFromMercator(t.k * 256 / Math.PI)); |
|
minZ = 1 / (t.k * t.k); |
|
t.y = (bbox.height * t.k >= height) ? Math.min(Math.max(-(bbox.height * t.k - height), t.y), 0) |
|
: Math.max(Math.min(height - bbox.height * t.k, t.y), 0); |
|
|
|
transform.translate([t.x, t.y]).scale(t.k); |
|
ctx.clearRect(0, 0, width, height); |
|
ctx.fillStyle = style.background['fill-color']; |
|
ctx.fillRect(0, 0, width, height); |
|
drawMap(z); |
|
if (z >= 4) drawPlaces(z) |
|
} |
|
|
|
function drawMap(zoom) { |
|
ctx.fillStyle = style.countries['fill-color']; |
|
ctx.lineWidth = style.countries['line-width']; |
|
ctx.beginPath(); |
|
path(land); |
|
ctx.fill(); |
|
|
|
if (zoom >= 4) { |
|
ctx.strokeStyle = style.states['line-color']; |
|
path(states); |
|
ctx.stroke(); |
|
} |
|
|
|
ctx.strokeStyle = style.countries['line-color']; |
|
ctx.beginPath(); |
|
path(border); |
|
ctx.stroke(); |
|
} |
|
|
|
function drawPlaces(zoom) { |
|
let rank = { 4: 1, 5: 3, 6: 5 }; |
|
rank = (rank[zoom] && zoom >= 4 ) ? rank[zoom] : zoom; |
|
|
|
ctx.font = style.places['text-size'] + ' ' + style.places['text-font']; |
|
ctx.textAlign = style.places['text-justify']; |
|
ctx.textBaseline = style.places['text-baseline']; |
|
ctx.fillStyle = style.places['text-color']; |
|
ctx.lineWidth = style.places['text-stroke-width']; |
|
ctx.strokeStyle = style.places['text-stroke-color']; |
|
ctx.beginPath(); |
|
places.features.filter(d => d.properties.scalerank <= rank).forEach(drawPlace); |
|
ctx.fillStyle = style.places['circle-color']; |
|
ctx.lineWidth = style.places['circle-stroke-width']; |
|
ctx.strokeStyle = style.places['circle-stroke-color']; |
|
ctx.fill(); |
|
ctx.stroke(); |
|
} |
|
|
|
function drawPlace(data) { |
|
let p = pathPlace.centroid(data); |
|
|
|
pathPlace(data); |
|
ctx.strokeText(data.properties.name, p[0], p[1] - 5); |
|
ctx.fillText(data.properties.name, p[0], p[1] - 5); |
|
} |
|
|
|
</script> |
|
</body> |
|
</html> |