Last active
June 18, 2020 20:40
-
-
Save armollica/68e0c75e28ebe60f6aefbd4ce45daf40 to your computer and use it in GitHub Desktop.
Isometric map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
height: 960 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mkdir -p zip shp | |
if [ ! -e zip/wi-osm.zip ]; then | |
curl -o zip/wi-osm.zip 'http://download.geofabrik.de/north-america/us/wisconsin-latest-free.shp.zip' | |
fi | |
if [ ! -e shp/gis.osm_roads_free_1.shp ]; then | |
unzip -o -d shp zip/wi-osm.zip | |
fi | |
if [ ! -e zip/counties.zip ]; then | |
curl -o zip/counties.zip 'ftp://ftp2.census.gov/geo/tiger/TIGER2017/COUNTY/tl_2017_us_county.zip' | |
fi | |
if [ ! -e shp/tl_2017_us_county.shp ]; then | |
unzip -o -d shp zip/counties.zip | |
fi | |
BBOX=-88.266907,42.726839,-87.537689,43.363129 | |
ROADS='["primary", "motorway", "motorway_link", "trunk"]' | |
mapshaper \ | |
-i shp/gis.osm_roads_free_1.shp \ | |
shp/gis.osm_water_a_free_1.shp \ | |
shp/tl_2017_us_county.shp \ | |
combine-files \ | |
-rename-layers roads,water,counties \ | |
-filter "$ROADS.indexOf(fclass) !== -1" target=roads \ | |
-clip bbox=$BBOX target=* \ | |
-erase water remove-slivers target=counties \ | |
-simplify 2% \ | |
-o topo.json target=* format=topojson force | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<link href="https://fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet"> | |
<style> | |
html, body { | |
font-family: 'Ubuntu Mono', monospace; | |
} | |
.county { | |
fill: #ebeae0; | |
stroke: #cdcbb1; | |
} | |
.road { | |
fill: none; | |
stroke: #b566ff; | |
} | |
.road.motorway { | |
stroke-width: 1px; | |
} | |
.road.trunk, | |
.road.motorway_link, | |
.road.primary { | |
stroke-width: 0.5px; | |
} | |
.water { | |
fill: #e3e3ff; | |
stroke: #d7d7ff; | |
} | |
.label line { | |
stroke: #333; | |
stroke-width: 1px; | |
} | |
.label text { | |
text-shadow: -1px -1px 1px #fff, | |
-1px 0px 1px #fff, | |
-1px 1px 1px #fff, | |
0px -1px 1px #fff, | |
0px 1px 1px #fff, | |
1px -1px 1px #fff, | |
1px 0px 1px #fff, | |
1px 1px 1px #fff; | |
} | |
.label.extra-large { | |
font-size: 18px; | |
} | |
.label.large { | |
font-size: 15px; | |
} | |
.label.medium { | |
font-size: 12px; | |
} | |
.label.small { | |
font-size: 10px; | |
} | |
</style> | |
</head> | |
<body> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://unpkg.com/topojson@3"></script> | |
<script> | |
var width = 960, | |
height = 960; | |
var svg = d3.select('body').append('svg') | |
.attr('width', width) | |
.attr('height', height) | |
.append('g') | |
.attr('transform', 'translate(' + (width / 2) + ',' + (-100) + ')') | |
var projection = d3.geoMercator(); | |
var isoprojection = isometricProjection(); | |
var path = isometricPath() | |
.projection(projection) | |
.isoprojection(isoprojection); | |
d3.json('topo.json', function(error, topo) { | |
if (error) throw error; | |
var countyData = topojson.feature(topo, topo.objects.counties), | |
roadData = topojson.feature(topo, topo.objects.roads), | |
waterData = topojson.feature(topo, topo.objects.water); | |
projection.fitExtent([[40, 40], [width - 40, height - 40]], countyData); | |
var water = svg.append('g').attr('class', 'water') | |
.selectAll('.water').data(waterData.features) | |
.enter().append('path') | |
.attr('class', 'water') | |
.attr('d', path.height(-20)); | |
var county = svg.append('g').attr('class', 'counties') | |
.selectAll('.county').data(countyData.features) | |
.enter().append('path') | |
.attr('class', 'county') | |
.attr('d', path.height(0)); | |
var road = svg.append('g').attr('class', 'roads') | |
.selectAll('.road').data(roadData.features) | |
.enter().append('path') | |
.attr('class', function(d) { return 'road ' + d.properties.fclass; }) | |
.attr('d', path.height(20)); | |
var label = svg.append('g').attr('class', 'labels') | |
.selectAll('label').data(labelData()) | |
.enter().append('g') | |
.attr('class', function(d) { return 'label ' + d.className; }); | |
label.append('line'); | |
label.append('text') | |
.style('text-anchor', 'middle') | |
.attr('dy', '-0.33em') | |
.text(function(d) { return d.label; }); | |
label | |
.each(function(d) { | |
var p = projection(d.location), | |
l0 = isoprojection([p[0], -p[1], 20]), | |
l1 = isoprojection([p[0], -p[1], 20 + d.height]); | |
d3.select(this).select('line') | |
.attr('x1', l0[0]) | |
.attr('y1', l0[1]) | |
.attr('x2', l1[0]) | |
.attr('y2', l1[1]); | |
d3.select(this).select('text') | |
.attr('x', l1[0]) | |
.attr('y', l1[1]); | |
}); | |
function update() { | |
water.attr('d', path.height(-20)); | |
county.attr('d', path.height(0)); | |
road.attr('d', path.height(20)); | |
label | |
.each(function(d) { | |
var p = projection(d.location), | |
l0 = isoprojection([p[0], -p[1], 20]), | |
l1 = isoprojection([p[0], -p[1], 20 + d.height]); | |
d3.select(this).select('line') | |
.attr('x1', l0[0]) | |
.attr('y1', l0[1]) | |
.attr('x2', l1[0]) | |
.attr('y2', l1[1]); | |
d3.select(this).select('text') | |
.attr('x', l1[0]) | |
.attr('y', l1[1]); | |
}); | |
} | |
d3.interval(function(elapsed) { | |
var t = elapsed / 15000 - Math.PI / 4, | |
pitch = ((Math.sin(t) + 1) / 2) * (Math.PI / 12) + (Math.PI / 6); | |
yaw = ((Math.sin(t) + 1) / 2) * (Math.PI / 12) - (Math.PI / 4); | |
isoprojection | |
.pitch(pitch) | |
.yaw(yaw); | |
update(); | |
}, 33); | |
}); | |
function isometricProjection() { | |
var sin = Math.sin, | |
cos = Math.cos, | |
asin = Math.asin, | |
tan = Math.atan, | |
PI = Math.PI; | |
var pitch = PI / 6, | |
yaw = PI / 4, | |
alpha = asin(tan(pitch)), | |
beta = yaw; | |
// See https://en.wikipedia.org/wiki/Isometric_projection | |
// TODO: Figure out why ax, ay and az needed to be flipped around. | |
function project(point) { | |
var ax = point[1], | |
ay = -point[2], | |
az = point[0]; | |
var x = cos(beta) * ax - sin(beta) * az, | |
y = cos(alpha) * ay + sin(alpha) * | |
(sin(beta) * ax + cos(beta) * az); | |
return [x, y]; | |
} | |
project.pitch = function(x) { | |
if (!arguments.length) return alpha; | |
pitch = x; | |
alpha = Math.asin(Math.tan(pitch)); | |
return project; | |
}; | |
project.yaw = function(x) { | |
if (!arguments.length) return beta; | |
yaw = x; | |
beta = yaw; | |
return project; | |
}; | |
return project; | |
} | |
function isometricPath() { | |
var projection, | |
isoprojection, | |
height = 0; | |
function path(feature) { | |
if (feature.geometry) { | |
return { | |
Polygon: polygon, | |
MultiPolygon: multipolygon, | |
LineString: linestring, | |
}[feature.geometry.type](feature.geometry.coordinates); | |
} else { | |
return null; | |
} | |
} | |
path.isoprojection = function(x) { | |
if (!arguments.length) return isoprojection; | |
isoprojection = x; | |
return path; | |
}; | |
path.projection = function(x) { | |
if (!arguments.length) return projection; | |
projection = x; | |
return path; | |
}; | |
path.height = function(x) { | |
if (!arguments.length) return height; | |
height = x; | |
return path; | |
}; | |
function project(point) { | |
var p = projection(point), | |
d = [p[0], -p[1], height]; | |
return isoprojection(d); | |
} | |
function multipolygon(coordinates) { | |
return 'M' + coordinates.map(function(multipolygon) { | |
var outerPolygon = multipolygon[0], | |
innerPolygons = multipolygon.slice(1); | |
var pathString = outerPolygon.map(function(point) { | |
return project(point).join(','); | |
}).join('L'); | |
if (innerPolygons.length > 0) { | |
pathString += innerPolygons.map(function(polygon) { | |
return 'M' + polygon.map(function(point) { | |
return project(point).join(','); | |
}).join('L'); | |
}); | |
}; | |
return pathString; | |
}).join('M'); | |
} | |
function polygon(coordinates) { | |
var outerPolygon = coordinates[0], | |
innerPolygons = coordinates.slice(1); | |
var pathString = 'M' + outerPolygon.map(function(point) { | |
return project(point).join(','); | |
}).join('L'); | |
if (innerPolygons.length > 0) { | |
pathString += innerPolygons.map(function(polygon) { | |
return 'M' + polygon.map(function(point) { | |
return project(point).join(','); | |
}).join('L'); | |
}); | |
}; | |
return pathString; | |
} | |
function linestring(coordinates) { | |
return 'M' + coordinates.map(function(point) { | |
return project(point).join(','); | |
}); | |
} | |
return path; | |
} | |
function radiansToDegrees(radians) { return radians * 180 / Math.PI; } | |
function degreesToRadians(degrees) { return degrees * Math.PI / 180; } | |
function labelData() { | |
return [ | |
{ | |
label: 'Milwaukee', | |
location: [-87.909665, 43.041331], | |
height: 50, | |
className: 'extra-large' | |
}, | |
{ | |
label: 'Wauwautosa', | |
location: [-88.010879, 43.050316], | |
height: 20, | |
className: 'medium' | |
}, | |
{ | |
label: 'West Allis', | |
location: [-88.007216, 43.016242], | |
height: 20, | |
className: 'medium' | |
}, | |
{ | |
label: 'Waukesha', | |
location: [-88.233588, 43.013146], | |
height: 40, | |
className: 'large' | |
}, | |
{ | |
label: 'Brookfield', | |
location: [-88.106788, 43.061748], | |
height: 20, | |
className: 'small' | |
}, | |
{ | |
label: 'Oak Creek', | |
location: [-87.864665, 42.884469], | |
height: 20, | |
className: 'medium' | |
}, | |
{ | |
label: 'Cedarburg', | |
location: [-87.989352, 43.296658], | |
height: 20, | |
className: 'small' | |
}, | |
{ | |
label: 'Menomonee Falls', | |
location: [-88.106047, 43.184513], | |
height: 20, | |
className: 'small' | |
}, | |
{ | |
label: 'Muskego', | |
location: [-88.141417, 42.904828], | |
height: 20, | |
className: 'small' | |
} | |
]; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment