Search code examples
javascriptd3.jsgeojsontile

D3.js can't zoom or pan tiles


I've been trying for about 10 days to use D3.js to create map displaying vector data (geojson) and raster tiles that I can zoom and pan, but can't just get it to work. I can zoom and pan the vector data but not the tiles.

I noticed that when I call tile(transform) I get the exact same x, y and z for the tiles despite having what I think are meaningful numbers in the transform (e.g. {"k": 1, "x": -35, "y": -37}). What am I messing up? I'm assuming the problem lies somewhere in the snippet below, though I also have the whole code further down.

    const tile = d3.tile()
        .extent([[0, 0], [width, height]])

    let zoom = d3.zoom()
        .on('zoom', () => zoomed(d3.event.transform))
    svg.call(zoom)

    function zoomed(transform) {
        const tiles = tile(transform);

        lines.attr('transform', transform.toString())

        images = images.data(tiles, d => d)
            .join('image')
            .attr('xlink:href', d => 'https://tiles.wmflabs.org/bw-mapnik/' + d[2] + '/' + d[0] + '/' + d[1] + '.png')
            .attr('x', ([x]) => (x + tiles.translate[0]) * tiles.scale)
            .attr('y', ([, y]) => (y + tiles.translate[1]) * tiles.scale)
            .attr('width', tiles.scale)
            .attr('height', tiles.scale);
    }

Below are the data and the full script.

{
"type": "FeatureCollection",
"name": "lines",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "id": 3}, "geometry": { "type": "LineString", "coordinates": [ [ -74.201304101157845, 40.033790926216739 ], [ -74.201226425025339, 40.033761910802717 ], [ -74.201164135201353, 40.033738641825124 ] ] } },
{ "type": "Feature", "properties": { "id": 4}, "geometry": { "type": "LineString", "coordinates": [ [ -74.200521185229846, 40.034804885753857 ], [ -74.200535458528648, 40.034780636493231 ], [ -74.200698022608137, 40.034504451003734 ], [ -74.200932444446437, 40.034106179618831 ], [ -74.201017665586349, 40.033961391736824 ] ] } },
{ "type": "Feature", "properties": { "id": 5}, "geometry": { "type": "LineString", "coordinates": [ [ -74.243246789888346, 40.038535382572938 ], [ -74.243221882547942, 40.038496558418132 ], [ -74.243182498790546, 40.038435169141529 ], [ -74.243137176965064, 40.038364523771932 ], [ -74.243057495263045, 40.038240319569326 ], [ -74.242984178076739, 40.038126035633333 ], [ -74.242983803896664, 40.038125452375041 ], [ -74.242970932683164, 40.03810547276553 ], [ -74.242927580863451, 40.038038178902433 ] ] } }
]
}
    let width = 900,
        height = 500,
        initialScale = 1 << 17,
        initialCenter = [-74.20465,39.98783]

    let svg = d3.select('body')
        .append('svg')
        .attr('height', height)
        .attr('width', width)

    let map = svg.append('g')
    let images = map.append('g')
        .attr('pointer-events', 'none')
        .selectAll('image');

    let glines = map.append('g')
    let lines = glines.append('g')

    let projection = d3.geoMercator()
        .scale(initialScale)
        .center(initialCenter)
        .translate([width / 2, height / 2])

    let path = d3.geoPath(projection)

    const tile = d3.tile()
        .extent([[0, 0], [width, height]])

    let zoom = d3.zoom()
        .on('zoom', () => zoomed(d3.event.transform))
    svg.call(zoom)

    function zoomed(transform) {
        const tiles = tile(transform);

        lines.attr('transform', transform.toString())

        images = images.data(tiles, d => d)
            .join('image')
            .attr('xlink:href', d => 'https://tiles.wmflabs.org/bw-mapnik/' + d[2] + '/' + d[0] + '/' + d[1] + '.png')
            .attr('x', ([x]) => (x + tiles.translate[0]) * tiles.scale)
            .attr('y', ([, y]) => (y + tiles.translate[1]) * tiles.scale)
            .attr('width', tiles.scale)
            .attr('height', tiles.scale);
    }

    function drawMap(data) {
        let tiles = tile
            .scale(projection.scale() * 2 * Math.PI)
            .translate(projection([0, 0]))();

        lines.selectAll('path')
            .data(data.features)
            .enter()
            .append('path')
            .attr('class', 'line')
            .attr('d', path)

        images = images.data(tiles, d => d)
            .enter()
            .append('image')
            .attr('xlink:href', d => 'https://tiles.wmflabs.org/bw-mapnik/' + d[2] + '/' + d[0] + '/' + d[1] + '.png')
            .attr('x', d => (d[0] + tiles.translate[0]) * tiles.scale)
            .attr('y', d => (d[1] + tiles.translate[1]) * tiles.scale)
            .attr('width', tiles.scale)
            .attr('height', tiles.scale);
    }

    d3.json('test.geojson').then(drawMap)

Solution

  • I found an easier way of putting lines on top of a basemap, by using D3 + Leaflet. See https://observablehq.com/@sfu-iat355/intro-to-leaflet-d3-interactivity . Works wonders.