Compare commits

...

1 commit

Author SHA1 Message Date
d119faa804
feat: add spaceap directory as an alternative data source, and optimize output for lasercutting
All checks were successful
/ test (push) Successful in 2m18s
2024-10-25 03:42:21 +02:00
3 changed files with 186 additions and 28 deletions

View file

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

View file

@ -13,6 +13,10 @@ rect.background {
stroke: none;
}
rect.frame {
display: none;
}
polygon.country {
fill: #c3c3c3;
stroke: #ffffff;

67
style/laser.css Normal file
View 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;
}