Skip to content

Instantly share code, notes, and snippets.

@armollica
Last active June 18, 2020 20:40
Show Gist options
  • Save armollica/68e0c75e28ebe60f6aefbd4ce45daf40 to your computer and use it in GitHub Desktop.
Save armollica/68e0c75e28ebe60f6aefbd4ce45daf40 to your computer and use it in GitHub Desktop.
Isometric map
height: 960
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
<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>
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment