skip to Main Content

The short version: How can I add SVG paths to a Leaflet map in such a way that the paths will update when the map coordinates change (e.g. on zoom change or slide)?

The long version: Hello, I have a topographic image that contains building outlines. After geo-rectifying the image I used Photoshop to convert the raster data to SVG. I know the geo-coordinates of the bounding box that describe the SVG perimeter, and know the internal coordinates of SVG path elements. I’m wondering the best way to now add the buildings described in the path elements of the SVG above to a Leaflet map.

Here’s a fiddle that shows the bounding box of the SVG image in red and the buildings in blue: http://jsfiddle.net/duhaime/4vL925Lj/ As you can see, the buildings are not yet properly oriented with respect to the bounding box.

My initial plan to align the buildings was to use a one-time script to convert the path elements from the SVG coordinate system into lat, long coordinates, then draw the buildings on the map using the polyline function I used to draw the bounding box:

var polyline = L.polyline(
  [upperLeft, upperRight, lowerRight, lowerLeft, upperLeft], 
  {color: 'red', className: 'bounding-box', weight: 2}
).addTo(map); 

The trouble with this approach is that Leaflet polylines can’t draw Bezier curves, which are present in the SVG path elements above. As a workaround, I thought I could use linear approximations for the Bezier curves, though this might become a decent amount of work.

Eventually I realized the SVG for the bounding box in the fiddle above uses Bezier curves, which gave me the idea that I might instead use matrix transforms to transpose the coordinate space of the building SVGs into the Leaflet coordinate space. The fiddle above uses a sample matrix transform css rule to transform the building layer.

Before going further down this rabbit hole, I wanted to ask: What do others think is the best way to add the paths that describe the buildings in the SVG above to the Leaflet map in the fiddle? I would be very grateful for any advice others can offer on this question!

PROGRESS: I decided to simplify this problem and figure out how to use a matrix transform to transform one div (“A”) into the aspect ratio of another div (“B”). In doing so, I made a small Python script that takes as input the input div A’s pixel coordinates and the desired output div B’s pixel coordinates. This script generates the transform matrix X such that AX=B. The script is documented internally and has an accompanying fiddle.

I also made a gist that derives the transform matrix to project points from the SVG space into proper lat, lng coords. Worst case scenario, I can partition the SVG path elements, take the dot product of each point with the transform matrix, and draw polylines with leaflet to plot the buildings. That will of lose the Bezier curves though…

3

Answers


  1. Chosen as BEST ANSWER

    This took quite a bit of thinking, but I've found a solution.

    After reading around, I realized that one can transpose points from one coordinate space (e.g. the SVG coordinate space) to another (e.g. and the lat/long coordinate space) by identifying a transform matrix, then by multiplying each point in the input space (SVG) by that transform matrix. This operation will transpose the given point into the appropriate location in the map.

    I wrote this script to calculate the required transform matrix. The script takes as parameters the SVG's bounding box coordinates and the bounding box coordinates of the georectified geotiff from which the SVG elements were extracted. The script generates the transform matrix and shows how one can multiply points in the SVG space by the matrix to find their appropriate lat/long coordinates.

    There's a catch--one needs to have the points in the SVG represented without any kind of CSS transformation. To get a straightforward representation of a SVG point's location within the SVG, I converted path elements in the SVG to polygon elements using this tool, for which the source is openly available.

    In case others need to accomplish a similar task, here was the full workflow I used:

    1. Find a raster map (jpg/tiff) of interest.
    2. Georectify the map with QGIS, ArcGIS, or MapWarper. This produces a geotiff.
    3. Download and install GDAL, a powerful geospatial library with Python bindings.
    4. Run an image trace on a feature of interest in your Geotiff (e.g. buildings) in Adobe Illustrator. This produces a vector layer; save the vector layer as a SVG file.
    5. If there are any <rect> or other geometric shapes in your saved SVG, convert them to paths and re-save.
    6. Identify the bounding box coordinates of your SVG and the Geotiff you fed as input to Illustrator. The bounding box of the latter can be obtained from GDAL by running gdalinfo {your-geotiff-file.tif}
    7. Inline those bounding box coordinates in the script referenced above. Then partition your SVG into an array of <polygon> elements, and for each, split the polygon into an array of points. Multiply each point by the transform matrix to find the point's lat/long position.
    8. Save each point of each shape to an appropriate geojson format so you can load the data into the client.

    For what it's worth, the script I'm using for generating the matrix transform and for transposing <polygon> element points into lat/long space is here. Please note some paths in the script would need to be updated for your situation--e.g. the script pushes the output geojson to an S3 bucket my lab manages :)

    I hope this helps someone else who finds themselves confronted with this task! I'm frankly amazed this took so much effort, and am pretty sure there must be a more elegant workflow...


  2. The following example uses your polyline values to demonstrate the the approach as it would apply to svg paths and other shapes during zoom/pan of the Leaflet map.
    Basically an SVG layer is created and all svg elements are added therein.

    <head>
      <title>Untitled</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js" ></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css" />
    <style type="text/css">
    <!--
    #map {
      width: 500px;
      height: 500px;
    }
    
    -->
    </style>
    
    </head>
    
    <body>
    <div id="map"></div>
    
    
    
    </body>
    <script>
    
    // create the map object itself
    centerCoordinates = new L.LatLng(41.307, -72.928);
    
    var map = new L.Map("map", {
      center: centerCoordinates,
      zoom: 14,
      zoomControl: false
    });
    
    // position the zoom controls in the bottom right hand corner
    L.control.zoom({
      position: 'bottomright',
      zoom: 14,
      maxZoom: 20,
      minZoom: 12,
    }).addTo(map);
    
    
    
    map.addLayer(new L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="http://cartodb.com/attributions">CartoDB</a>',
      subdomains: 'abcd',
      maxZoom: 19
    }));
    
    // specify the coordinates of the overlay's bounding box
    var upperLeft = L.latLng(41.329785, -72.927220);
    var lowerLeft = L.latLng(41.304414, -72.945686);
    var upperRight = L.latLng(41.319186, -72.903268);
    var lowerRight = L.latLng(41.293816, -72.921718);
    /*
    // create a red polyline from an array of LatLng points
    var polyline = L.polyline(
      [upperLeft, upperRight, lowerRight, lowerLeft, upperLeft], {
        color: 'red',
        className: 'bounding-box',
        weight: 2
      }
    ).addTo(map);
    */
    
      //---CREATE SVG---
        map._initPathRoot() //---creates an svg layer---
        var MySVG=document.querySelector("svg") //---access svg element---
    
        var NS="http://www.w3.org/2000/svg"
        //---place svg elems in here---
        var SvgElemG=document.createElementNS(NS,"g")
        MySVG.appendChild(SvgElemG)
    
         //---zooming the map's SVG elements---
        map.on("viewreset", adjustSVGElements);
    
    //---add svg polygon---
    var polygon=document.createElementNS(NS,"polyline")
    polygon.setAttribute("stroke-width",1)
    polygon.setAttribute("fill","none")
    polygon.setAttribute("stroke","red")
     //---convert latLng to x,y---
    var xyUL=map.latLngToLayerPoint(upperLeft)
    var xyLL=map.latLngToLayerPoint(lowerLeft)
    var xyLR=map.latLngToLayerPoint(lowerRight)
    var xyUR=map.latLngToLayerPoint(upperRight)
    var points=[xyUL.x,xyUL.y,xyLL.x,xyLL.y,xyLR.x,xyLR.y,xyUR.x,xyUR.y,xyUL.x,xyUL.y].toString()
    polygon.setAttribute('points',points)
    
    //--required for zoom---
    var svgPnt=L.point(0,0) //--reference for translate--
    var latLng=map.layerPointToLatLng(svgPnt)
    var lat=latLng.lat
    var lng=latLng.lng
    polygon.setAttribute("lat",lat)
    polygon.setAttribute("lng",lng)
    //---retain the zoom level at its creation--
    polygon.setAttribute('initZoom',map.getZoom())
    
    SvgElemG.appendChild(polygon)
    
    //--- on map zoom - fired via map event: viewreset---
    function adjustSVGElements()
    {
    	var mapZoom=map.getZoom()
    
    	var svgElems=SvgElemG.childNodes
    	for(var k=0;k<svgElems.length;k++)
    	{
            var svgElem=svgElems.item(k)
            var lat=parseFloat(svgElem.getAttribute("lat"))
            var lng=parseFloat(svgElem.getAttribute("lng"))
            var latLng= new  L.latLng(lat, lng)
            var transX=map.latLngToLayerPoint(latLng).x
            var transY=map.latLngToLayerPoint(latLng).y
            //---trash previous transform---
            svgElem.setAttribute("transform","") //---required for IE
            svgElem.removeAttribute("transform")
    
            var transformRequestObj=MySVG.createSVGTransform()
            var animTransformList=svgElem.transform
            //---get baseVal to access/place object transforms
            var transformList=animTransformList.baseVal
            //---translate----
            transformRequestObj.setTranslate( transX,  transY)
            transformList.appendItem(transformRequestObj)
            transformList.consolidate()
           //---scale---
            var initZoom=parseFloat(svgElem.getAttribute("initZoom"))
            var scale = (Math.pow(2, mapZoom)/2)/(Math.pow(2, initZoom)/2);
            transformRequestObj.setScale(scale,scale)
            transformList.appendItem(transformRequestObj)
            transformList.consolidate()
        }
    
    
    }
    
    </script>
    Login or Signup to reply.
  3. I added this feature to work I’ve previously done on Leaflet Maps. This may apply to your application.
    See: http://www.svgdiscovery.com/K/K04A.htm

    This uses two key points that are common to both the Leaflet Map and the imported SVG paths.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search