如果我正确理解了这个问题,我相信我有一个解决方案。但是,没有任何代码,这个问题非常广泛,因此,我的答案将相当广泛,但应该清楚地概述实现预期结果的方法。
根据图像,问题似乎是:如何将形状适合地理边界框,其中边界框投影到平面空间并可能具有旋转。
如果这是准确的,那么我的回答应该很有用。为了清楚起见,当我使用形状这个词时,我指的是非地理多边形。
解决方案的一般模式是:
- 获取参考多边形的方向
- 旋转地图 - 而不是形状
- 使用 geoIdentity,使形状适合地理边界框
一个。完成与开始时相反的旋转,
湾。旋转整个 svg/canvas 以对抗初始旋转;或者,
C。保留旋转的地图。
获取地理矩形的方向
您需要建立地理特征相对于投影平面的旋转,这似乎是问题的症结所在(您说明了球体的旋转矩形区域,但区域在投影空间中只能是矩形,并且基于图像,这种解释似乎是正确的)。
D3 在这方面可能有点挑战性,因为它不会沿着笛卡尔坐标渲染路径,而是插入很大的圆距离。对于国家或大陆规模的地理特征,这将是一个更大的问题,但在城市规模上应该可以忽略不计。
如果从左上角 ( [x1,y1
]) 开始并使用右下角 ( [x2,x1]
),您应该能够确定这条线相对于它应该是什么的角度:一条垂直线从第一个坐标开始,到第二。鉴于缠绕顺序对 d3 很重要,如果您有一个点,则第二个坐标将始终对应于矩形的同一顶点。
获得这个角度的方法相当简单:
var p1 = [long,lat]; // geographic coordinate space for the two points
var p2 = [long,lat];
var x1 = projection(p1)[0]; // projected svg coordinate space for the two points
var x2 = projection(p2)[0];
var y1 = projection(p1)[1];
var y2 = projection(p2)[1];
var dx = x1 - x2; // run
var dy = y1 - y2; // rise
var angle = Math.atan(dx/dy); // in radians, multiply by 180/π to get degrees
我们投影两个坐标,计算 x 和 y 坐标的投影差异,以像素为单位。通过 Math.atan() 运行它,我们就有了一个角度。
但是等等,还有更多。
旋转地图
我们用来计算角度的方法很好,但是我们需要修改它来旋转地图。如果我们旋转地图,它将围绕[0,0]
(long,lat) 旋转,这是大多数投影的默认旋转中心。我们需要将地图居中在[-x,-y]
x 和 y 表示用于计算方向的两个点的中点处,在地理坐标空间(未投影)中测量。
在计算角度之前,我们需要通过旋转使地图居中,因为这会改变角度。为此,我们需要 d3.geoInterpolate 来计算从 p1 (0) 到 p2 (1) 的点,我们需要中间点,所以我们输入 0.5:
var pMid = d3.geoInterpolate(p1, p2)(0.5);
现在我们可以在计算角度之前将其应用于投影:
projection.rotate([-pMid[0],-pMid[1]]);
为什么是负值?我们在我们脚下移动地球
现在我们可以进行角度计算。一旦我们有了角度,我们就可以应用它:
projection.rotate([-pMid[0],-pMid[1],-angle]) // angle in degrees
我们到了一半,使用下图,我们使用 A 和 B 的地理坐标来确定地理中心 C。然后使用 A 和 B 的投影坐标,我们确定角度 α,然后我们用它来旋转投影坐标,使 AB 线在地图上垂直。
使形状适合地理边界框
所以我们已经解决了一半的问题,现在我们需要投影形状。我们将为形状使用第二个投影,一个普通的 geoIdentity 就可以了,它允许我们在投影坐标时使用 fitSize 或 fitExtent 方法而不进行变换。请注意,您可能希望在 y 轴上翻转此功能:svg y 值从顶部 0 开始,更标准的笛卡尔 y 值从底部开始。
我们将要使用 fitExtent,这将允许我们为形状设置一个边界矩形。proejction.fitExtent([[x,y],[x,y]],feature)
接受一个包含边界框左上角和右下角的数组(在 svg 坐标中)来保存一个特征(geojson 特征)。
请记住,我们纠正的投影地理特征将更矩形,地理(未投影)尺寸越小,较大的区域可能具有较少的正方形属性,特别是相对于矩形的右侧。对于较大的地理区域,您可能需要修改旋转计算,但在某些时候投影的矩形与球体上的“矩形”不对齐。
要获得 fitExtent 的边界框,我们可以使用 path.bounds():
返回指定 GeoJSON 对象的投影平面边界框(通常以像素为单位)。边界框由一个二维数组表示:[[x₀, y₀], [x₁, y₁]],其中x₀是最小x坐标,y₀是最小y坐标,x₁是最大x坐标, y₁ 是最大 y 坐标。(API 文档)
太好了,现在我们有两个应该具有相同点的边界框,我们使用:
projection2.fitExtent(path.bounds(geoRectangle));
现在我们已经在地理特征上覆盖了形状:
var feature = { "type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [ [ [-81.62841796875,24.307053283225915],[-75.9375,21.88188980762927],[ -77.3876953125,18.8543103618898],[ -83.1884765625,21.268899719967695 ], [-81.62841796875,24.307053283225915]]]}};
var triangle = {"type": "Polygon","coordinates": [[[0, 0], [10, 10], [20, 0], [0, 0]]]};
var width = 500; var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var projection = d3.geoMercator().scale(500/Math.PI/2).translate([width/2,height/2]);
var path = d3.geoPath().projection(projection);
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
if (error) throw error;
var p1 = feature.geometry.coordinates[0][1]; // first point in geojson
var p2 = feature.geometry.coordinates[0][2]; // second point in geojson
var pMid = d3.geoInterpolate(p1, p2)(0.5); // halfway point between both points
projection.rotate([-pMid[0],-pMid[1]]); // rotate the projection to center on the mid point
projection.fitExtent([[135,135],[width-135,height-135]],feature) // optional: scale the projected feature, may offer benefits for very small features
var dx = projection(p1)[0] - projection(p2)[0]; // run, difference between projected points x values
var dy = projection(p1)[1] - projection(p2)[1]; // rise, difference between projected points y values
var a = Math.atan(dx/dy) * 180 / Math.PI; // get angle and convert to degrees
projection.rotate([-pMid[0],-pMid[1],-a]); // adjust rotation to straighten feature
projection.fitExtent([[135,135],[width-135,height-135]],feature) // scale and translate the feature.
// draw world map, draw feature
svg.append("path")
.attr("d",path(topojson.mesh(world)))
.attr("fill","none")
.attr("stroke","black")
svg.append("path")
.attr("d", path(feature))
.attr("fill","none")
.attr("stroke","steelblue");
// set up the projection and path for the shape
var projection2 = d3.geoIdentity().reflectY(true);
var path2 = d3.geoPath().projection(projection2);
// scale the shape's projection for the shape using the bounds of the geographic feature
projection2.fitExtent(path.bounds(feature),triangle);
// draw the shape
svg.append("path")
.attr("d", path2(triangle));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
这是一个手绘多边形,我在显示器上放了一张名片并追踪它,因此瑕疵大于正常
结束游戏
现在你已经绘制了一个形状并覆盖在一些地理特征之上,希望相当体面。怎么办?从技术上讲,这解决了关键问题,但如果您不想要倾斜的地图,我们已经创建了一个新问题。解决此问题的选项是旋转整个 svg/canvas 或获取形状中的每个点并重新投影它们并将它们转换为地理坐标,而不是无聊的旧笛卡尔坐标。还有其他的,但这两个似乎最直接。
如果处理图像,您可以逐个像素地重新投影图像,但不要指望这会很快,请参阅此块。这个答案着眼于图像并将它们拟合到投影,如果它们已知边界,墨卡托应该在非常小的距离内作为投影正常工作。
重投影点和 GeoIdentity
如评论中所示, d3.geoIdentity 如果遵循以下模式,则无法进行重新投影:var latlong = projection.invert(projection2([x,y]))
,因为 geoIdentity 不返回函数,而只是一个对象。相反,我们可以定义行为(翻转 y,并使用恒等比例和变换):
projection2.project = function([x,y]) {
var s = this.scale();
var t = this.translate();
return [(x * s) + t[0] , -y * s + t[1]]
}
在粗略的测试中,它似乎返回了投影点,这是 geoIdentity 中不包含的 geoProjection 的标准行为。使用模式应如下所示:
projection.invert(projection2.project([x,y]))