Search code examples
javascriptd3.jssvgappendzooming

Zooming and panning 2 corresponding images in d3.js by same amount?


I have an SVG image side by side with GeoJSON. There are corresponding objects in both the images. I have added both of them using d3. I am currently able to zoom and pan them both separately. I am also able to get the scale and x, y upon zooming or panning over any of the two images. Now I want to synchronize the zoom/pan of the two images such that if the zoom/pan is done on one image the same amount of zoom/pan would be done on the other image. I guess that could be done if I call the zoom.translateBy and zoom.scaleBy functions but I don't know how to apply it. Here is the code:

<!doctype html>
<html lang="en">    
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <title>Sketch-To-Metric Map Alignment by Qualitative Spatial Constraint Matching</title>

    <!-- the data of the map -->
    <script src="img/sketchmap_ids.json"></script>

    <!--Add support for earlier versions of Internet Explorer -->
    <!--[if lt IE 9]>
    <script src="lib/js/html5shiv.js"></script>
    <![endif]-->

    <filter id="offset" x="-20%" y="-10%" height="130%">
     <feGaussianBlur  in="SourceAlpha" stdDeviation="5"/>
      <feOffset dx="5" dy="5" result="B"/>
     <feMerge>
      <feMergeNode in="B"/>
      <feMergeNode in="SourceGraphic"/>
      </feMerge>
    </filter>

    <filter id="dropshadow_2" height="130%">
      <feGaussianBlur in="SourceAlpha" stdDeviation="3"/> <!-- stdDeviation is how much to blur -->
      <feOffset dx="2" dy="2" result="offsetblur"/> <!-- how much to offset -->
      <feMerge> 
        <feMergeNode/> <!-- this contains the offset blurred image -->
        <feMergeNode in="SourceGraphic"/> <!-- this contains the element that the filter is applied to -->
      </feMerge>
    </filter>
    <!-- circle r="10" style="filter:url(#dropshadow)"/ -->

    <filter id="dropshadow" height="130%">
      <feGaussianBlur in="SourceAlpha" stdDeviation="3"/> 
      <feOffset dx="2" dy="2" result="offsetblur"/>
      <feComponentTransfer>
        <feFuncA type="linear" slope="0.2"/>
      </feComponentTransfer>
      <feMerge> 
        <feMergeNode/>
        <feMergeNode in="SourceGraphic"/> 
      </feMerge>
    </filter>

    <style>
        .shadow {
            -webkit-filter: drop-shadow( 5px 5px 5px 10px);
                    filter: drop-shadow( 5px 5px 2px); /* Same syntax as box-shadow */

        }
        .reveal section img { background:none; border:none; box-shadow:none; }
        .service {
            stroke-width: 4;
            stroke: #999999;
            fill: none;
        }
        .residential-line {
            stroke-width: 8;
            stroke: #b3b3b3;
            fill: none;
        }
        .fence,.footway,.cycleway,.track,.path,.pedestrian,.steps {
            stroke-width: 2;
            stroke: #2b1100;
            stroke-dasharray: 3,2,3;
            fill: none;
        }
        .primary {
            stroke-width: 18;
            stroke: #b3b3b3;
            fill: none;
        }
        .tertiary {
            stroke-width: 12;
            stroke: #b3b3b3;
            fill: none;
        }
        .bridge {
            stroke-width: 12;
            stroke: #cd853f;
            fill: none;
        }
        .graveyard {
            stroke-width: 2;
            stroke: #444444;
            stroke-dasharray: 3,2,3;
            fill: #668000;
        }
        .waterway-line,.river-line,.weir {
            stroke-width: 5;
            stroke: #2A7FFF;
            fill: none;
        }
        .water {
            stroke-width: 1;
            stroke: #0066FF;
            fill: #2A7FFF;;
        }
        .scrub,.forest {
            stroke-width: 1;
            stroke: #008033;
            fill: #008033;
        }
        .grass,.village_green,.greenhouse {
            stroke-width: 1;
            stroke: #5FD35F;
            fill: #5FD35F;
        }
        .residential,.commercial {
            stroke-width: 1;
            stroke: #FFF6D5;
            fill: #FFF6D5;
        }
        .parking,.bicycle_parking {
            stroke-width: 1;
            stroke: #FFE680;
            fill: #FFE680;
        }
        .university,.civic,.yes,.house,.school {
            stroke-width: 0;
            fill: #916F6F;
        }
        .map-panel {
            width: 100%;   
            border: 1px solid #444444;
        }
        #sketch3,#sketch4,#sketch5,#sketch6,#sketch10,#sketchgraph11,#sketchgraph12 {
            width: 45%;
            float: left;
            padding-bottom:0px;
        }

        #metric3,#metric4,#metric5,#metric6,#metric10,#metricgraph11,#metricgraph12 {
            width: 45%;
            float: right;
            padding-bottom:0px;
        }

        .text-pane-left {
            width: 45%;
            float: right;
            padding-bottom:0px;
        }

        .text-pane-right {
            width: 45%;
            float: right;
            padding-bottom:0px;
        }

        #mynetwork {
            width: 854px;    
            height: 480px; 
            position: relative; 
            border: 1px solid #444444;
            background-color: #dddddd;
        }

        #othernetwork {
            width: 800px;    
            height: 600px; 
            position: relative; 
            border: 1px solid #444444;
            background-color: #dddddd;
        }

        #visualization {
            width: 800px;    
            height: 600px; 
            position: relative; 
            border: 1px solid #444444;
            background-color: #dddddd;
        }

        body {
            color: #d3d3d3;
            font: 12pt arial;
            background-color: #ffffff;
        }
    </style>  
</head>

<body>

    <div class="map-panel" id="panel">
        <div class="map-pane-left" id="sketch4"></div>
        <div class="map-pane-right" id="metric4"></div>
    </div>                                                  

    <script src="d3/d3.min.js"></script>
    <script src="d3/topojson.min.js"></script>
    <script src="https://d3js.org/d3.v4.min.js"></script>





    <script>
        //https://datascience.blog.wzb.eu/2016/08/29/bringing-svg-to-life-with-d3-js/


        //load sketch map

        d3.xml("img/sketch_all_in_components_web.svg").mimeType("image/svg+xml").get(function(error, xml)           {
            if (error) throw error;

            function sketchMapZoomed() {
                    svg.selectAll("*").attr("transform", "translate(" + d3.event.translate + ")" + "                        scale(" + d3.event.scale + ")")
                    var currentScale = d3.event.scale;
                    var currentTranslate = d3.event.translate;
                    console.log(currentScale, currentTranslate);

            }

      //var copy = xml.documentElement.cloneNode(true);
            document.getElementById('sketch4').appendChild(xml.documentElement);
            d3.select('#sketch4').selectAll('g,                                                                     path').on('mouseenter',animateMapTargets).on('mouseleave',removeFilter);      
            var svg = d3.select("svg")
            .call(d3.behavior.zoom().on("zoom", sketchMapZoomed ))
        });



        //load metric map




        var width = document.getElementById("sketch4").clientWidth;
        height = (width * 0.71053942806206943134954217685331);

        function metricMapZoomed () {
            svg.attr("transform", d3.event.transform)
            var transformMetric = d3.zoomTransform(this)
            console.log(transformMetric);
            //.call(d3.zoom.translateBy("sketch#4", transformMetric.x, transformMetric.y)) 
            //var t = d3.select("#sketch4").attr("transform", "translate(" + transformMetric.x + "," + transformMetric.y + ") scale(" + transformMetric.k + ")");       
      }

        var svg = d3.select("#metric4").append("svg")
            .attr("width", width)
            .attr("height", height)
            .attr("id", "metricmap")
            .call(d3.zoom().on("zoom", metricMapZoomed ))
            .append("g");

        //create a place holder rectangle to keep floats inline
        var svgContainer = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", width)
            .attr("id", "placeholder");

        d3.json("img/defense_data.json", function(error, map) {
            if (error) return console.error(error);

            var projection = d3.geo.mercator()
                .center([7.612337603149424, 51.96211781909236])
                .scale(3000000);
                //.translate(0,0);

            var path = d3.geo.path()
                .projection(projection);

            svg.selectAll(".city_block_landmarks")
                .data(topojson.feature(map, map.objects.city_block_landmarks).features)
                .enter().append("path")
                .attr("class", function(d){
                    //console.log(d.properties.class)
                    return d.properties.classfeature;
                })
                .attr("id", function(d){
                    //console.log(d.id)
                    return d.id;
                })
                .attr("d", path);

            svg.selectAll(".hidden_landmarks")
                .data(topojson.feature(map, map.objects.hidden_landmarks).features)
                .enter().append("path")
                .attr("class", function(d){
                    //console.log(d.properties.class)
                    return d.properties.classfeature;
                })
                .attr("id", function(d){
                    //console.log(d.id)
                    return d.id;
                })
                .attr("d", path);

            svg.selectAll(".botanica_mid_polygons")
                .data(topojson.feature(map, map.objects.botanica_mid_polygons).features)
                .enter().append("path")
                .attr("class", function(d){
                    //console.log(d.properties.class)
                    return d.properties.classfeature;
                })
                .attr("id", function(d){
                    //console.log(d.id)
                    return d.id;
                })
                .attr("d", path);

            svg.selectAll(".streets_linear_features")
                .data(topojson.feature(map, map.objects.streets_linear_features).features)
                .enter().append("path")
                .attr("class", function(d){
                    //console.log(d.properties.class)
                    return d.properties.classfeature;
                })
                .attr("id", function(d){
                    //console.log(d.id)
                    return d.id;
                })
                .attr("d", path);   
        });

        //animate objects on mouse on and remove animation on mouse off
        function animateMapTargets() {          
            if (this.id)
                if (targets[this.id])
                    targets[this.id].forEach(animateTargetNode);
        };

        function animateTargetNode(obj, i) {
            //console.log(d3.select('#'+obj)./*node().*/style('filter','url(#offset)'));
            //d3.select('#'+obj).style('filter','url(#offset)');
            //console.log(d3.select('#'+obj).classed("shadow", true));//
            d3.select('#'+obj).classed("shadow", true);
                //.attr()  style="filter:url(#dropshadow)"
                //.attr();
        }

        function removeFilter() {           
            if (this.id)
                if (targets[this.id])
                    targets[this.id].forEach(function animateTargetNode(obj, i) {
                        //d3.select('#'+obj).style('filter','none');
                        d3.select('#'+obj).classed("shadow", false);
                    });
        };




    </script>   
</body>
</html>

Solution

  • I guess that could be done if I call the zoom.translateBy and zoom.scaleBy

    Both of these are relative, i.e. they are added to / multiplied by the current transform, they don't set it. So if the current scale was 0.5 and you use scaleBy with a value of 1.0, you would still get a scale of 0.5 (because 0.5 * 1.0 == 0.5).

    To "sync" the absolute values between two zooms you would instead use zoom.translateTo and zoom.scaleTo respectively.

    Instead of setting the individual components of the zoom transform, a more direct approach would be to set the whole transform. For this you would use zoomTransform on the source node (or use the event.transform of a zoom event) to get the current transform and then set that transform on the target with zoom.transform.