矩阵变换:将SVG路径坐标转换为传单坐标系

Matrix transform: Converting SVG path coordinates to Leaflet coordinate system

本文关键字:转换 坐标系 坐标 路径 变换 SVG      更新时间:2023-09-26

简短的版本:我如何添加SVG路径到一个传单地图这样的方式,路径将更新时,地图坐标的变化(例如在缩放变化或幻灯片)?

长版本:你好,我有一张包含建筑物轮廓的地形图。在对图像进行地理校正后,我使用Photoshop将光栅数据转换为SVG。我知道描述SVG周长的边界框的地理坐标,也知道SVG路径元素的内部坐标。我想知道现在最好的方法是将上面SVG的路径元素中描述的建筑物添加到传单地图中。

这是一个显示SVG图像的红色边界框和蓝色建筑物的小提琴:http://jsfiddle.net/duhaime/4vL925Lj/正如您所看到的,建筑物还没有正确地朝向边界框。

我对齐建筑物的最初计划是使用一次性脚本将路径元素从SVG坐标系统转换为纬度,长坐标,然后使用我用来绘制边界框的折线函数在地图上绘制建筑物:

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

这种方法的问题是,传单折线不能绘制贝塞尔曲线,这是存在于上面的SVG路径元素。作为一种变通方法,我认为我可以对Bezier曲线使用线性近似,尽管这可能会成为一个相当大的工作量。

最终我意识到上面的边界框的SVG使用了Bezier曲线,这让我想到我可以使用矩阵变换将建筑SVG的坐标空间转置到传单坐标空间中。上面的代码使用了一个示例矩阵变换css规则来变换建筑层。

在进一步深入这个兔子洞之前,我想问:其他人认为将上面SVG中描述建筑物的路径添加到小提琴中的传单地图中的最佳方法是什么?我将非常感谢任何人可以提供关于这个问题的任何建议!

PROGRESS:我决定简化这个问题,并找出如何使用矩阵变换将一个div("a")转换为另一个div("B")的宽高比。为此,我编写了一个小Python脚本,将输入div a的像素坐标和所需输出div B的像素坐标作为输入。这个脚本生成变换矩阵X,使得AX=B。该脚本在内部进行了文档化,并配有伴奏。

我还提出了一个要点,即导出转换矩阵以将SVG空间中的点投影到适当的纬度、lng坐标中。在最坏的情况下,我可以划分SVG路径元素,用变换矩阵取每个点的点积,并用折线绘制建筑物。这样会失去贝塞尔曲线

这花了我不少心思,但我找到了一个解决方案。

阅读后,我意识到可以通过识别变换矩阵,然后通过将输入空间(SVG)中的每个点乘以变换矩阵,将点从一个坐标空间(例如SVG坐标空间)转置到另一个坐标空间(例如,后期/长坐标空间)。这个操作将把给定的点转换成地图上相应的位置。

我写了这个脚本来计算所需的变换矩阵。该脚本将SVG的边界框坐标和从中提取SVG元素的geocorrected geotiff的边界框坐标作为参数。该脚本生成转换矩阵,并展示如何将SVG空间中的点与矩阵相乘以找到相应的纬度/长度坐标。

这里有一个问题——需要在没有任何CSS转换的情况下表示SVG中的点。为了在SVG中直接表示SVG点的位置,我使用这个工具将SVG中的路径元素转换为多边形元素,该工具的源代码是公开的。

如果其他人需要完成类似的任务,下面是我使用的完整工作流程:

  1. 查找感兴趣的栅格图(jpg/tiff)。
  2. 使用QGIS、ArcGIS或MapWarper对地图进行地理校正。这将生成一个geotiff。
  3. 下载并安装GDAL,这是一个强大的地理空间库,带有Python绑定。
  4. 在Adobe Illustrator中对Geotiff中感兴趣的特征(例如建筑物)运行图像跟踪。这就产生了一个矢量层;保存矢量图层为SVG文件。
  5. 如果您保存的SVG中有任何<rect>或其他几何形状,请将它们转换为路径并重新保存。
  6. 标识SVG的边界框坐标和作为Illustrator输入的Geotiff。后者的边界框可以通过运行gdalinfo {your-geotiff-file.tif}
  7. 从GDAL得到。
  8. 在上面引用的脚本中内联这些边界框坐标。然后将SVG划分为<polygon>元素数组,并将每个多边形划分为点数组。将每个点乘以变换矩阵以找到点的后期/长位置。
  9. 将每个形状的每个点保存为适当的geojson格式,以便您可以将数据加载到客户端。

对于它的价值,我用来生成矩阵变换和转换<polygon>元素点到后期/长空间的脚本在这里。请注意,脚本中的一些路径需要根据您的情况进行更新。脚本将输出geojson推送到我的实验室管理的S3桶:)

我希望这能帮助那些发现自己面临这个任务的人!坦率地说,我很惊讶这需要这么多的努力,我很确定一定有一个更优雅的工作流程……

我把这个功能添加到我以前在传单地图上做的工作中。这可能适用于您的应用程序。参见:www.svgdiscovery.com/K/K04A.htm

这使用了两个关键点,这两个关键点对于传单地图和导入的SVG路径都是通用的。

下面的示例使用您的折线值来演示该方法,因为它将应用于svg路径和其他形状在缩放/平移传单地图。基本上就是创建一个SVG层,并在其中添加所有SVG元素。

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