You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

420 lines
14 KiB
Python

10 years ago
#!/usr/bin/env python
10 years ago
#
3 years ago
# Copyright (C) 2014 - 2021 Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>
10 years ago
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
10 years ago
10 years ago
import math
10 years ago
from math import sqrt
import numpy as np
import matplotlib.pyplot as plt
from scipy import interpolate
from itertools import tee
10 years ago
from matplotlib.patches import Polygon
from matplotlib.collections import PatchCollection
import matplotlib
10 years ago
from PIL import Image
10 years ago
def y2lat(a):
return (
180.0
/ math.pi
* (2.0 * math.atan(math.exp(a * math.pi / 180.0)) - math.pi / 2.0)
)
10 years ago
def lat2y(a):
return (
180.0
/ math.pi
* math.log(math.tan(math.pi / 4.0 + a * (math.pi / 180.0) / 2.0))
)
10 years ago
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2,s3), ..."
a, b = tee(iterable, 2)
next(b, None)
return zip(a, b)
10 years ago
def triplewise(iterable):
"s -> (s0,s1,s2), (s1,s2,s3), (s2,s3,s4), ..."
a, b, c = tee(iterable, 3)
10 years ago
next(b, None)
next(c, None)
next(c, None)
return zip(a, b, c)
10 years ago
# using barycentric coordinates
def ptInTriangle(p, p0, p1, p2):
A = 0.5 * (
-p1[1] * p2[0]
+ p0[1] * (-p1[0] + p2[0])
+ p0[0] * (p1[1] - p2[1])
+ p1[0] * p2[1]
)
sign = -1 if A < 0 else 1
s = (
p0[1] * p2[0] - p0[0] * p2[1] + (p2[1] - p0[1]) * p[0] + (p0[0] - p2[0]) * p[1]
) * sign
t = (
p0[0] * p1[1] - p0[1] * p1[0] + (p0[1] - p1[1]) * p[0] + (p1[0] - p0[0]) * p[1]
) * sign
return s >= 0 and t >= 0 and (s + t) <= 2 * A * sign
10 years ago
def getxing(p0, p1, p2, p3):
ux = p1[0] - p0[0]
uy = p1[1] - p0[1]
vx = p2[0] - p3[0]
vy = p2[1] - p3[1]
10 years ago
# get multiplicity of u at which u meets v
a = vy * ux - vx * uy
10 years ago
if a == 0:
# lines are parallel and never meet
return None
s = (vy * (p3[0] - p0[0]) + vx * (p0[1] - p3[1])) / a
10 years ago
if 0.0 < s < 1.0:
return (p0[0] + s * ux, p0[1] + s * uy)
10 years ago
else:
return None
10 years ago
# the line p0-p1 is the upper normal to the path
# the line p2-p3 is the lower normal to the path
#
# | | |
# p0--------|--------p1
# | | |
# | | |
# p3--------|--------p2
# | | |
def ptInQuadrilateral(p, p0, p1, p2, p3):
# it might be that the two normals cross at some point
# in that case the two triangles are created differently
cross = getxing(p0, p1, p2, p3)
10 years ago
if cross:
return ptInTriangle(p, p0, cross, p3) or ptInTriangle(p, p2, cross, p1)
else:
return ptInTriangle(p, p0, p1, p2) or ptInTriangle(p, p2, p3, p0)
10 years ago
def get_st(Ax, Ay, Bx, By, Cx, Cy, Dx, Dy, Xx, Xy):
d = Bx - Ax - Cx + Dx
e = By - Ay - Cy + Dy
l = Dx - Ax
g = Dy - Ay
h = Cx - Dx
m = Cy - Dy
i = Xx - Dx
j = Xy - Dy
n = g * h - m * l
10 years ago
# calculation for s
a1 = m * d - h * e
b1 = n - j * d + i * e
c1 = l * j - g * i
10 years ago
# calculation for t
a2 = g * d - l * e
b2 = n + j * d - i * e
c2 = h * j - m * i
10 years ago
s = []
if a1 == 0:
s.append(-c1 / b1)
10 years ago
else:
r1 = b1 * b1 - 4 * a1 * c1
10 years ago
if r1 >= 0:
r11 = (-b1 + sqrt(r1)) / (2 * a1)
10 years ago
if -0.0000000001 <= r11 <= 1.0000000001:
s.append(r11)
r12 = (-b1 - sqrt(r1)) / (2 * a1)
10 years ago
if -0.0000000001 <= r12 <= 1.0000000001:
s.append(r12)
t = []
if a2 == 0:
t.append(-c2 / b2)
10 years ago
else:
r2 = b2 * b2 - 4 * a2 * c2
10 years ago
if r2 >= 0:
r21 = (-b2 + sqrt(r2)) / (2 * a2)
10 years ago
if -0.0000000001 <= r21 <= 1.0000000001:
t.append(r21)
r22 = (-b2 - sqrt(r2)) / (2 * a2)
10 years ago
if -0.0000000001 <= r22 <= 1.0000000001:
t.append(r22)
if not s or not t:
return [], []
10 years ago
if len(s) == 1 and len(t) == 2:
s = [s[0], s[0]]
10 years ago
if len(s) == 2 and len(t) == 1:
t = [t[0], t[0]]
10 years ago
return s, t
def main(x, y, width, smoothing, subdiv):
halfwidth = width / 2.0
tck, u = interpolate.splprep([x, y], s=smoothing)
unew = np.linspace(0, 1.0, subdiv + 1)
out = interpolate.splev(unew, tck)
10 years ago
heights = []
offs = []
height = 0.0
for (ax, ay), (bx, by) in pairwise(list(zip(*out))):
s = ax - bx
t = ay - by
l = sqrt(s * s + t * t)
10 years ago
offs.append(height)
height += l
heights.append(l)
# the border of the first segment is just perpendicular to the path
cx = -out[1][1] + out[1][0]
cy = out[0][1] - out[0][0]
cl = sqrt(cx * cx + cy * cy) / halfwidth
dx = out[1][1] - out[1][0]
dy = -out[0][1] + out[0][0]
dl = sqrt(dx * dx + dy * dy) / halfwidth
px = [out[0][0] + cx / cl]
py = [out[1][0] + cy / cl]
qx = [out[0][0] + dx / dl]
qy = [out[1][0] + dy / dl]
for (ubx, uby), (ux, uy), (uax, uay) in triplewise(list(zip(*out))):
10 years ago
# get adjacent line segment vectors
ax = ux - ubx
ay = uy - uby
bx = uax - ux
by = uay - uy
10 years ago
# normalize length
al = sqrt(ax * ax + ay * ay)
bl = sqrt(bx * bx + by * by)
ax = ax / al
ay = ay / al
bx = bx / bl
by = by / bl
10 years ago
# get vector perpendicular to sum
cx = -ay - by
cy = ax + bx
cl = sqrt(cx * cx + cy * cy) / halfwidth
px.append(ux + cx / cl)
py.append(uy + cy / cl)
10 years ago
# and in the other direction
dx = ay + by
dy = -ax - bx
dl = sqrt(dx * dx + dy * dy) / halfwidth
qx.append(ux + dx / dl)
qy.append(uy + dy / dl)
10 years ago
# the border of the last segment is just perpendicular to the path
cx = -out[1][-1] + out[1][-2]
cy = out[0][-1] - out[0][-2]
cl = sqrt(cx * cx + cy * cy) / halfwidth
dx = out[1][-1] - out[1][-2]
dy = -out[0][-1] + out[0][-2]
dl = sqrt(dx * dx + dy * dy) / halfwidth
px.append(out[0][-1] + cx / cl)
py.append(out[1][-1] + cy / cl)
qx.append(out[0][-1] + dx / dl)
qy.append(out[1][-1] + dy / dl)
10 years ago
quads = []
patches = []
for (p3x, p3y, p2x, p2y), (p0x, p0y, p1x, p1y) in pairwise(
list(zip(px, py, qx, qy))
):
quads.append(((p0x, p0y), (p1x, p1y), (p2x, p2y), (p3x, p3y)))
polygon = Polygon(((p0x, p0y), (p1x, p1y), (p2x, p2y), (p3x, p3y)), True)
10 years ago
patches.append(polygon)
10 years ago
containingquad = []
for pt in zip(x, y):
10 years ago
# for each point, find the quadrilateral that contains it
found = []
for i, (p0, p1, p2, p3) in enumerate(quads):
if ptInQuadrilateral(pt, p0, p1, p2, p3):
10 years ago
found.append(i)
10 years ago
if found:
10 years ago
if len(found) > 1:
print("point found in two quads")
10 years ago
return None
containingquad.append(found[0])
10 years ago
else:
containingquad.append(None)
10 years ago
# check if the only points for which no quad could be found are in the
# beginning or in the end
# find the first missing ones:
for i, q in enumerate(containingquad):
10 years ago
if q != None:
break
# find the last missing ones
for j, q in zip(range(len(containingquad) - 1, -1, -1), reversed(containingquad)):
10 years ago
if q != None:
break
# remove the first and last missing ones
if i != 0 or j != len(containingquad) - 1:
containingquad = containingquad[i : j + 1]
x = x[i : j + 1]
y = y[i : j + 1]
10 years ago
# check if there are any remaining missing ones:
if None in containingquad:
print("cannot find quad for point")
10 years ago
return None
for off, h in zip(offs, heights):
targetquad = ((0, off + h), (width, off + h), (width, off), (0, off))
patches.append(Polygon(targetquad, True))
10 years ago
tx = []
ty = []
assert len(containingquad) == len(x) == len(y)
assert (
len(out[0])
== len(out[1])
== len(px)
== len(py)
== len(qx)
== len(qy)
== len(quads) + 1
== len(heights) + 1
== len(offs) + 1
)
for (rx, ry), i in zip(list(zip(x, y)), containingquad):
10 years ago
if i == None:
10 years ago
continue
(ax, ay), (bx, by), (cx, cy), (dx, dy) = quads[i]
s, t = get_st(ax, ay, bx, by, cx, cy, dx, dy, rx, ry)
# if more than one solution, take second
10 years ago
# TODO: investigate if this is always the right solution
10 years ago
if len(s) != 1 or len(t) != 1:
10 years ago
s = s[1]
t = t[1]
else:
s = s[0]
t = t[0]
u = s * width
v = offs[i] + t * heights[i]
10 years ago
tx.append(u)
ty.append(v)
# sx = []
# sy = []
# for ((x1,y1),(x2,y2)),((ax,ay),(bx,by),(cx,cy),(dx,dy)),off,h in zip(pairwise(zip(*out)),quads,offs,heights):
10 years ago
# s,t = get_st(ax,ay,bx,by,cx,cy,dx,dy,x1,y1)
# if len(s) != 1 or len(t) != 1:
# return None
# u = s[0]*width
# v = off+t[0]*h
# sx.append(u)
# sy.append(v)
# s,t = get_st(ax,ay,bx,by,cx,cy,dx,dy,x2,y2)
# if len(s) != 1 or len(t) != 1:
# return None
# u = s[0]*width
# v = off+t[0]*h
# sx.append(u)
# sy.append(v)
# create map with
# python -c 'import logging; logging.basicConfig(level=logging.DEBUG); from landez import ImageExporter; ie = ImageExporter(tiles_url="http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png"); ie.export_image(bbox=(8.0419921875,51.25160146817652,10.074462890625,54.03681240523652), zoomlevel=14, imagepath="image.png")'
10 years ago
im = Image.open("map.png")
bbox = [8.0419921875, 51.25160146817652, 10.074462890625, 54.03681240523652]
10 years ago
# apply mercator projection
10 years ago
bbox[1] = lat2y(bbox[1])
bbox[3] = lat2y(bbox[3])
iw, ih = im.size
10 years ago
data = []
for i, (off, h, (p0, p1, p2, p3)) in enumerate(zip(offs, heights, quads)):
10 years ago
# first, account for the offset of the input image
p0 = p0[0] - bbox[0], p0[1] - bbox[1]
p1 = p1[0] - bbox[0], p1[1] - bbox[1]
p2 = p2[0] - bbox[0], p2[1] - bbox[1]
p3 = p3[0] - bbox[0], p3[1] - bbox[1]
10 years ago
# PIL expects coordinates in counter clockwise order
p1, p3 = p3, p1
10 years ago
# x lon
# ----- = -----
# w bbox[2]-bbox[0]
# translate to pixel coordinates
p0 = (iw * p0[0]) / (bbox[2] - bbox[0]), (ih * p0[1]) / (bbox[3] - bbox[1])
p1 = (iw * p1[0]) / (bbox[2] - bbox[0]), (ih * p1[1]) / (bbox[3] - bbox[1])
p2 = (iw * p2[0]) / (bbox[2] - bbox[0]), (ih * p2[1]) / (bbox[3] - bbox[1])
p3 = (iw * p3[0]) / (bbox[2] - bbox[0]), (ih * p3[1]) / (bbox[3] - bbox[1])
10 years ago
# PIL starts coordinate system at the upper left corner, swap y coord
p0 = int(p0[0]), int(ih - p0[1])
p1 = int(p1[0]), int(ih - p1[1])
p2 = int(p2[0]), int(ih - p2[1])
p3 = int(p3[0]), int(ih - p3[1])
box = (
0,
int(ih * (height - off - h) / (bbox[3] - bbox[1])),
int(iw * width / (bbox[2] - bbox[0])),
int(ih * (height - off) / (bbox[3] - bbox[1])),
)
quad = (p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1])
data.append((box, quad))
im_out = im.transform(
(int(iw * width / (bbox[2] - bbox[0])), int(ih * height / (bbox[3] - bbox[1]))),
Image.MESH,
data,
Image.BICUBIC,
)
10 years ago
im_out.save("out.png")
# np.random.seed(seed=0)
# colors = 100*np.random.rand(len(patches)//2)+100*np.random.rand(len(patches)//2)
# p = PatchCollection(patches, cmap=matplotlib.cm.jet, alpha=0.4)
# p.set_array(np.array(colors))
# plt.figure()
# plt.axes().set_aspect('equal')
##plt.axhspan(0, height, xmin=0, xmax=width)
# fig, ax = plt.subplots()
##ax.add_collection(p)
# ax.set_aspect('equal')
# plt.axis((0,width,0,height))
# plt.imshow(np.asarray(im_out),extent=[0,width,0,height])
# plt.imshow(np.asarray(im),extent=[bbox[0],bbox[2],bbox[1],bbox[3]])
# plt.plot(x,y,out[0],out[1],px,py,qx,qy,tx,ty)
# plt.show()
10 years ago
return True
10 years ago
if __name__ == "__main__":
10 years ago
x = []
y = []
import sys
if len(sys.argv) != 5:
print("usage: %s data.csv width smoothing N" % sys.argv[0])
print("")
print(
" data.csv whitespace delimited lon/lat pairs of points along the path"
)
print(" width width of the resulting map in degrees")
print(
" smoothing curve smoothing from 0 (exact fit) to higher values (looser fit)"
)
print(" N amount of quads to split the path into")
print("")
print(" example usage:")
print(" %s Weser-Radweg-Hauptroute.csv 0.286 6 20" % sys.argv[0])
exit(1)
10 years ago
with open(sys.argv[1]) as f:
for l in f:
a, b = l.split()
10 years ago
# apply mercator projection
b = lat2y(float(b))
x.append(float(a))
y.append(b)
width = float(sys.argv[2])
smoothing = float(sys.argv[3])
N = int(sys.argv[4])
main(x, y, width, smoothing, N)
# for smoothing in [1,2,4,8,12]:
10 years ago
# for subdiv in range(10,30):
# if main(x,y,width,smoothing,subdiv):
# print width,smoothing,subdiv