block by armollica 68e0c75e28ebe60f6aefbd4ce45daf40

Isometric map

Full Screen

index.html

<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>

build.sh


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