feat: add spaceap directory as an alternative data source, and optimize output for lasercutting
All checks were successful
/ test (push) Successful in 2m18s
All checks were successful
/ test (push) Successful in 2m18s
This commit is contained in:
parent
fc17f0cf9b
commit
d119faa804
3 changed files with 186 additions and 28 deletions
131
generate_map.py
131
generate_map.py
|
@ -37,6 +37,7 @@ class CachePaths:
|
|||
self.shapes_countries = os.path.join(path, 'shapes_countries')
|
||||
self.erfa_info = os.path.join(path, 'erfa-info.json')
|
||||
self.chaostreff_info = os.path.join(path, 'chaostreff-info.json')
|
||||
self.spaceapi_info = os.path.join(path, 'spaceapi-info.json')
|
||||
|
||||
|
||||
class OutputPaths:
|
||||
|
@ -63,7 +64,7 @@ class CoordinateTransform:
|
|||
self._proj = pyproj.Transformer.from_crs('epsg:4326', projection)
|
||||
self.setup()
|
||||
|
||||
def setup(self, scalex=1.0, scaley=1.0, bbox=[(-180, -90), (180, 90)]):
|
||||
def setup(self, scalex=1.0, scaley=1.0, bbox=[(-180, -90), (180, 90)], longedge=None):
|
||||
self._scalex = scalex
|
||||
self._scaley = scaley
|
||||
west = min(bbox[0][0], bbox[1][0])
|
||||
|
@ -71,6 +72,17 @@ class CoordinateTransform:
|
|||
east = max(bbox[0][0], bbox[1][0])
|
||||
south = min(bbox[0][1], bbox[1][1])
|
||||
self._ox, self._oy = self._proj.transform(west, north)
|
||||
if longedge is not None:
|
||||
self._scalex = 130
|
||||
self._scaley = 200
|
||||
w, h = self(east, south)
|
||||
if w > h:
|
||||
self._scalex = 130 * longedge / w
|
||||
self._scaley = 200 * longedge / w
|
||||
else:
|
||||
self._scalex = 130 * longedge / h
|
||||
self._scaley = 200 * longedge / h
|
||||
print(self._scalex, self(east, south))
|
||||
return self(east, south)
|
||||
|
||||
def __call__(self, lon, lat):
|
||||
|
@ -413,6 +425,47 @@ class Chaostreff(Erfa):
|
|||
SVG_LABEL = False
|
||||
|
||||
|
||||
class SpaceApiSpace(Erfa):
|
||||
|
||||
SVG_CLASS = 'chaostreff'
|
||||
SVG_DATA = 'data-chaostreff'
|
||||
SVG_DOTSIZE_ATTR = 'dotsize_treff'
|
||||
SVG_LABEL = True
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, ns, target, radius):
|
||||
directory_url = 'https://directory.spaceapi.io/'
|
||||
directory_req = urllib.request.urlopen(directory_url)
|
||||
directory_resp = json.loads(directory_req.read().decode())
|
||||
|
||||
spaces = []
|
||||
for name, url in directory_resp.items():
|
||||
print(name, url)
|
||||
try:
|
||||
url_req = urllib.request.urlopen(url, timeout=15)
|
||||
api = json.loads(url_req.read().decode())
|
||||
if 'open' not in api['state'] or api['state']['open'] is None:
|
||||
continue
|
||||
if 'lastchange' in api['state'] and api['state']['lastchange'] < 1700000000:
|
||||
continue
|
||||
space = SpaceApiSpace(ns,
|
||||
name,
|
||||
name,
|
||||
api['location']['lon'],
|
||||
api['location']['lat'],
|
||||
name,
|
||||
url,
|
||||
radius)
|
||||
spaces.append(space)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Save to cache
|
||||
with open(target, 'w') as f:
|
||||
json.dump([s.to_dict() for s in spaces], f)
|
||||
return spaces
|
||||
|
||||
|
||||
class BoundingBox:
|
||||
|
||||
def __init__(self, left, top, right=None, bottom=None, *, width=None, height=None, meta=None, base_weight=0):
|
||||
|
@ -591,7 +644,12 @@ def compute_bbox(ns):
|
|||
|
||||
print('Computing map bounding box')
|
||||
bounds = []
|
||||
for path in tqdm.tqdm([ns.cache_directory.erfa_info, ns.cache_directory.chaostreff_info]):
|
||||
sources = []
|
||||
if ns.erfa_source == 'doku':
|
||||
sources = [ns.cache_directory.erfa_info, ns.cache_directory.chaostreff_info]
|
||||
elif ns.erfa_source == 'spaceapi':
|
||||
sources = [ns.cache_directory.spaceapi_info]
|
||||
for path in tqdm.tqdm(sources):
|
||||
erfas = Erfa.from_cache(ns, path, radius=0)
|
||||
for e in erfas:
|
||||
if len(bounds) == 0:
|
||||
|
@ -762,19 +820,24 @@ def create_svg(ns):
|
|||
|
||||
# Convert from WGS84 lon, lat to chosen projection
|
||||
bbox = compute_bbox(ns)
|
||||
svg_box = ns.projection.setup(ns.scale_x, ns.scale_y, bbox=bbox)
|
||||
svg_box = ns.projection.setup(ns.scale_x, ns.scale_y, bbox=bbox, longedge=ns.long_edge)
|
||||
rectbox = [0, 0, svg_box[0], svg_box[1]]
|
||||
|
||||
# Load everything from cached JSON files
|
||||
countries = Country.from_cache(ns, ns.cache_directory.shapes_countries)
|
||||
countries = [c for c in countries if c.filter_boundingbox(bbox)]
|
||||
states = FederalState.from_cache(ns, ns.cache_directory.shapes_states)
|
||||
erfas = Erfa.from_cache(ns, ns.cache_directory.erfa_info, ns.dotsize_erfa)
|
||||
chaostreffs = Chaostreff.from_cache(ns, ns.cache_directory.chaostreff_info, ns.dotsize_treff)
|
||||
# There is an edge case where when a space changed states between Erfa and Chaostreff, the
|
||||
# Semantic MediaWiki engine returns this space as both an Erfa and a Chaostreff, resulting
|
||||
# in glitches in the rendering. As a workaround, here we simply assume that it's an Erfa.
|
||||
chaostreffs = [c for c in chaostreffs if c not in erfas]
|
||||
erfas = []
|
||||
chaostreffs = []
|
||||
if ns.erfa_source == 'doku':
|
||||
erfas = Erfa.from_cache(ns, ns.cache_directory.erfa_info, ns.dotsize_erfa)
|
||||
chaostreffs = Chaostreff.from_cache(ns, ns.cache_directory.chaostreff_info, ns.dotsize_treff)
|
||||
# There is an edge case where when a space changed states between Erfa and Chaostreff, the
|
||||
# Semantic MediaWiki engine returns this space as both an Erfa and a Chaostreff, resulting
|
||||
# in glitches in the rendering. As a workaround, here we simply assume that it's an Erfa.
|
||||
chaostreffs = [c for c in chaostreffs if c not in erfas]
|
||||
elif ns.erfa_source == 'spaceapi':
|
||||
erfas = SpaceApiSpace.from_cache(ns, ns.cache_directory.spaceapi_info, ns.dotsize_treff)
|
||||
|
||||
for c in states + countries:
|
||||
for x, y in c.polygon:
|
||||
|
@ -819,9 +882,12 @@ def create_svg(ns):
|
|||
bg.set('class', 'background')
|
||||
svg.append(bg)
|
||||
|
||||
# This can take some time, especially if lots of candidates are generated
|
||||
print('Layouting labels')
|
||||
texts = optimize_text_layout(ns, font, erfas, chaostreffs, width=svg_box[0], svg=svg)
|
||||
if not ns.without_labels:
|
||||
# This can take some time, especially if lots of candidates are generated
|
||||
print('Layouting labels')
|
||||
texts = optimize_text_layout(ns, font, erfas, chaostreffs, width=svg_box[0], svg=svg)
|
||||
else:
|
||||
texts = {}
|
||||
|
||||
# Render shortest shapes last s.t. Berlin, Hamburg and Bremen are rendered on top of their surrounding states
|
||||
states = sorted(states, key=lambda x: -len(x))
|
||||
|
@ -833,6 +899,15 @@ def create_svg(ns):
|
|||
for erfa in erfas + chaostreffs:
|
||||
erfa.render(svg, texts)
|
||||
|
||||
frame = etree.Element('rect',
|
||||
id='frame',
|
||||
x='0',
|
||||
y='0',
|
||||
width=str(svg_box[0]),
|
||||
height=str(svg_box[1]))
|
||||
frame.set('class', 'frame')
|
||||
svg.append(frame)
|
||||
|
||||
# Generate SVG, PNG and HTML output files
|
||||
|
||||
print('Writing SVG')
|
||||
|
@ -885,6 +960,7 @@ def main():
|
|||
ap.add_argument('--output-directory', type=OutputPaths, default='out', help='Path to the output directory')
|
||||
ap.add_argument('--update-borders', action='store_true', default=False, help='Update country borders from Wikidata')
|
||||
ap.add_argument('--update-erfalist', action='store_true', default=False, help='Update Erfa/Chaostreff list from doku.ccc.de')
|
||||
ap.add_argument('--erfa-source', choices=['doku', 'spaceapi'], default='doku', help='Data source for the list of Erfas, either doku.ccc.de or spaceapi.ccc.de')
|
||||
ap.add_argument('--bbox', type=float, nargs=4, metavar=('LON', 'LAT', 'LON', 'LAT') ,default=None, help='Override map bounding box')
|
||||
ap.add_argument('--bbox-margin', type=float, default=0.3, help='Margin around the inferred bounding box. Ignored with --bbox')
|
||||
ap.add_argument('--stylesheet', type=str, default='style/erfamap.css', help='Stylesheet for the generated SVG')
|
||||
|
@ -899,10 +975,13 @@ def main():
|
|||
ap.add_argument('--projection', type=CoordinateTransform, default='epsg:4258', help='Map projection to convert the WGS84 coordinates to')
|
||||
ap.add_argument('--scale-x', type=float, default=130, help='X axis scale to apply after projecting')
|
||||
ap.add_argument('--scale-y', type=float, default=200, help='Y axis scale to apply after projecting')
|
||||
ap.add_argument('--long-edge', type=float, default=None, help='Compute scale-x and scale-y based on a set value for the longest edge. Feature sizes are not scaled.')
|
||||
ap.add_argument('--png-scale', type=float, default=1.0, help='Scale of the PNG image')
|
||||
ap.add_argument('--debug', action='store_true', default=False, help='Add debug information to the produced SVG')
|
||||
ap.add_argument('--without-labels', action='store_true', default=False, help='Disable labels placement')
|
||||
|
||||
ns = ap.parse_args(sys.argv[1:])
|
||||
print(ns.long_edge)
|
||||
|
||||
if ns.update_borders or not os.path.isdir(ns.cache_directory.shapes_countries):
|
||||
if os.path.isdir(ns.cache_directory.shapes_countries):
|
||||
|
@ -916,17 +995,25 @@ def main():
|
|||
print('Retrieving state border shapes')
|
||||
FederalState.fetch(target=ns.cache_directory.shapes_states)
|
||||
|
||||
if ns.update_erfalist or not os.path.isfile(ns.cache_directory.erfa_info):
|
||||
if os.path.exists(ns.cache_directory.erfa_info):
|
||||
os.unlink(ns.cache_directory.erfa_info)
|
||||
print('Retrieving Erfas information')
|
||||
Erfa.fetch(ns, target=ns.cache_directory.erfa_info, radius=ns.dotsize_erfa)
|
||||
if ns.erfa_source == 'doku':
|
||||
if ns.update_erfalist or not os.path.isfile(ns.cache_directory.erfa_info):
|
||||
if os.path.exists(ns.cache_directory.erfa_info):
|
||||
os.unlink(ns.cache_directory.erfa_info)
|
||||
print('Retrieving Erfas information')
|
||||
Erfa.fetch(ns, target=ns.cache_directory.erfa_info, radius=ns.dotsize_erfa)
|
||||
|
||||
if ns.update_erfalist or not os.path.isfile(ns.cache_directory.chaostreff_info):
|
||||
if os.path.exists(ns.cache_directory.chaostreff_info):
|
||||
os.unlink(ns.cache_directory.chaostreff_info)
|
||||
print('Retrieving Chaostreffs information')
|
||||
Chaostreff.fetch(ns, target=ns.cache_directory.chaostreff_info, radius=ns.dotsize_treff)
|
||||
if ns.update_erfalist or not os.path.isfile(ns.cache_directory.chaostreff_info):
|
||||
if os.path.exists(ns.cache_directory.chaostreff_info):
|
||||
os.unlink(ns.cache_directory.chaostreff_info)
|
||||
print('Retrieving Chaostreffs information')
|
||||
Chaostreff.fetch(ns, target=ns.cache_directory.chaostreff_info, radius=ns.dotsize_treff)
|
||||
|
||||
elif ns.erfa_source == 'spaceapi':
|
||||
if ns.update_erfalist or not os.path.isfile(ns.cache_directory.spaceapi_info):
|
||||
if os.path.exists(ns.cache_directory.spaceapi_info):
|
||||
os.unlink(ns.cache_directory.spaceapi_info)
|
||||
print('Retrieving SpaceAPI information')
|
||||
SpaceApiSpace.fetch(ns, target=ns.cache_directory.spaceapi_info, radius=ns.dotsize_treff)
|
||||
|
||||
create_svg(ns)
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@ rect.background {
|
|||
stroke: none;
|
||||
}
|
||||
|
||||
rect.frame {
|
||||
display: none;
|
||||
}
|
||||
|
||||
polygon.country {
|
||||
fill: #c3c3c3;
|
||||
stroke: #ffffff;
|
||||
|
|
67
style/laser.css
Normal file
67
style/laser.css
Normal file
|
@ -0,0 +1,67 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
rect.background {
|
||||
fill: white;
|
||||
z-index: -3;
|
||||
}
|
||||
|
||||
rect.frame {
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
polygon.country {
|
||||
stroke: blue;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
polygon.state {
|
||||
stroke: blue;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
circle.erfa {
|
||||
z-index: 1;
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
circle.chaostreff {
|
||||
stroke: none;
|
||||
fill: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
text.erfalabel {
|
||||
font-family: "Concert One";
|
||||
font-size: 6px;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
fill: red;
|
||||
}
|
||||
|
||||
rect.debugleft {
|
||||
stroke: red;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
rect.debugright {
|
||||
stroke: green;
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
z-index: 1000;
|
||||
}
|
Loading…
Reference in a new issue