Skip to content

Commit

Permalink
Inital import.
Browse files Browse the repository at this point in the history
  • Loading branch information
Björn Mosler committed Nov 28, 2022
0 parents commit 19acf59
Show file tree
Hide file tree
Showing 18 changed files with 3,448 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
@@ -0,0 +1,9 @@
*.pyc
node_modules/
.vscode/
.env/
*.geojson
geojson/
*.db
shapes/
*.zip
24 changes: 24 additions & 0 deletions Makefile
@@ -0,0 +1,24 @@
setup-map:
cd map && \
npm install && \
ln -s ../exporter/geojson .

setup-exporter:
cd exporter && \
./get-shapes.sh && \
./setup-venv.sh && \
./setup-db.sh && \
mkdir -p geojson

setup: setup-map setup-exporter

image-exporter:
cd exporter && \
podman build -t wlw-partner-exporter .

image-map:
cd map && \
podman build -t wlw-partner-map .

images: image-map image-exporter

22 changes: 22 additions & 0 deletions README.md
@@ -0,0 +1,22 @@
# wLw-Partner-Map

Automating updates to [wLw](https://wir-lernen-weiter.ch)'s [partner map](https://wir-lernen-weiter.ch/partnergemeinden/).

Contributed as part of [DINAcon HACKnights 2022](https://hacknight.dinacon.ch/event/8).

## Run locally

```sh
make setup
cd exporter
./run.sh
cd ../map
npm start
```

## Run on k8s

```sh
make images
# TODO
```
5 changes: 5 additions & 0 deletions exporter/.dockerignore
@@ -0,0 +1,5 @@
shapes/
db/
geojson/
*.zip
.venv/
15 changes: 15 additions & 0 deletions exporter/Dockerfile
@@ -0,0 +1,15 @@
FROM python:3.11-slim

RUN apt-get update && apt-get install -y curl unzip

WORKDIR /opt/wlw-geojson-exporter

VOLUME ./geojson

ADD . .

RUN ./get-shapes.sh

RUN pip install virtualenv && ./setup-venv.sh

CMD ["./run.sh"]
6 changes: 6 additions & 0 deletions exporter/get-shapes.sh
@@ -0,0 +1,6 @@
#!/bin/sh
set -o errexit
curl https://data.geo.admin.ch/ch.swisstopo.swissboundaries3d/swissboundaries3d_2022-05/swissboundaries3d_2022-05_2056_5728.shp.zip > swissboundaries3d.zip
mkdir -p shapes
unzip -u swissboundaries3d.zip -d shapes
rm swissboundaries3d.zip
103 changes: 103 additions & 0 deletions exporter/main.py
@@ -0,0 +1,103 @@
import argparse
import json
import logging
import sqlite3
from dataclasses import dataclass
from typing import List, Union

import numpy as np
import shapefile
from pyproj import Transformer
from shapely.geometry import Polygon

parser = argparse.ArgumentParser()
parser.add_argument('-o',
'--outfile',
required=True,
help="where to write GeoJSON data")
args = parser.parse_args()

ESPG_LV95 = 'EPSG:2056'
EPSG_WGS84 = 'EPSG:4326'
transformer = Transformer.from_crs(ESPG_LV95, EPSG_WGS84, always_xy=True)

logger = logging.getLogger('partner-geojson-exporter')
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())

con = sqlite3.connect('db/partners.db')


@dataclass
class Partner:
zip_code: int
status: str
url: str
comment: str


def find_partner(zip_code: int) -> Union[Partner, None]:
with con:
cursor = con.execute(
"SELECT zip_code, status, url, comment FROM partners WHERE zip_code = ?",
(zip_code, ))

result = cursor.fetchone()
if result:
return Partner(result[0], result[1], result[2], result[3])


def simplify(points: List[tuple[float, float]]) -> List[tuple[float, float]]:
return [
p for p in Polygon(np.array(points)).simplify(
0.0025, preserve_topology=True).exterior.coords
]


def transform(points: List[tuple[float, float]]) -> List[tuple[float, float]]:
return [transformer.transform(x, y) for x, y in points]


logger.info("reading shapes")

# schema
# [('DeletionFlag', 'C', 1, 0), ['UUID', 'C', 38, 0], ['DATUM_AEND', 'D', 8, 0],
# ['DATUM_ERST', 'D', 8, 0], ['ERSTELL_J', 'N', 10, 0],
# ['ERSTELL_M', 'C', 20, 0], ['REVISION_J', 'N', 10, 0],
# ['REVISION_M', 'C', 20, 0], ['GRUND_AEND', 'C', 20, 0],
# ['HERKUNFT', 'C', 20, 0], ['HERKUNFT_J', 'N', 10, 0],
# ['HERKUNFT_M', 'C', 20, 0], ['OBJEKTART', 'C', 20, 0],
# ['BEZIRKSNUM', 'N', 10, 0], ['SEE_FLAECH', 'N', 31, 15],
# ['REVISION_Q', 'C', 100, 0], ['NAME', 'C', 254, 0], ['KANTONSNUM', 'N', 10, 0],
# ['ICC', 'C', 20, 0], ['EINWOHNERZ', 'N', 10, 0], ['BFS_NUMMER', 'N', 10, 0],
# ['GEM_TEIL', 'C', 20, 0], ['GEM_FLAECH', 'N', 31, 15], ['SHN', 'C', 20, 0]]
sf = shapefile.Reader(
'shapes/SHAPEFILE_LV95_LN02/swissBOUNDARIES3D_1_3_TLM_HOHEITSGEBIET')

fields = ('NAME', 'BFS_NUMMER')
collection = {'type': 'FeatureCollection', 'features': []}

logger.info("transform and merge")

for shape in sf.shapeRecords(fields=fields):
geojson = shape.__geo_interface__
new_coords = simplify(transform(geojson['geometry']['coordinates'][0]))
geojson['geometry']['coordinates'][0] = new_coords

partner = find_partner(geojson['properties']['BFS_NUMMER'])

if partner:
geojson['properties']['status'] = partner.status
geojson['properties']['url'] = partner.url
geojson['properties']['comment'] = partner.comment

collection['features'].append(geojson)

con.close()

logger.info("writing to %s", args.outfile)

with open(args.outfile, 'w') as outfile:
json.dump(collection, outfile)

logger.info("done")
4 changes: 4 additions & 0 deletions exporter/requirements.txt
@@ -0,0 +1,4 @@
numpy==1.23.5
pyproj==3.4.0
pyshp==2.3.1
Shapely==1.8.5.post1
2 changes: 2 additions & 0 deletions exporter/run.sh
@@ -0,0 +1,2 @@
#!/bin/sh
.venv/bin/python main.py -o geojson/partners.geojson
18 changes: 18 additions & 0 deletions exporter/setup-db.sh
@@ -0,0 +1,18 @@
#!/bin/sh
mkdir -p db

if [ ! -f db/partners.db ]; then
sqlite3 db/partners.db 'CREATE TABLE "partners" (
"zip_code" INTEGER NOT NULL,
"status" TEXT CHECK("status" IN (NULL, "P", "K", "I")),
"url" TEXT,
"comment" TEXT,
"canton" TEXT NOT NULL,
"district" TEXT NOT NULL,
"municipality" TEXT NOT NULL,
"partnership_started_at" TEXT,
"partnership_cancelled_at" TEXT,
"updated_at" INTEGER,
PRIMARY KEY("zip_code")
)'
fi;
4 changes: 4 additions & 0 deletions exporter/setup-venv.sh
@@ -0,0 +1,4 @@
#!/bin/sh
set -o errexit
python -m virtualenv .venv
.venv/bin/pip install -r requirements.txt
2 changes: 2 additions & 0 deletions map/.dockerignore
@@ -0,0 +1,2 @@
node_modules/
geojson/
13 changes: 13 additions & 0 deletions map/Dockerfile
@@ -0,0 +1,13 @@
FROM nodejs:18-alpine as build

WORKDIR /opt/wlw-partner-map

ADD . .

RUN npm install

FROM nginx:alpine

COPY --from=build /opt/wlw-partner-map /usr/share/nginx/html

VOLUME ./usr/share/nginx/html/geojson
1 change: 1 addition & 0 deletions map/geojson
26 changes: 26 additions & 0 deletions map/index.html
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>wLw-Partnergemeinden</title>
<link
rel="stylesheet"
href="node_modules/leaflet/dist/leaflet.css"
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
crossorigin=""
/>
<style>
#map {
width: 100%;
height: 100vh;
}
body {
margin: 0;
padding: 0;
}
</style>
<script type="module" src="index.js"></script>
</head>
<body>
<div id="map"></div>
</body>
</html>
37 changes: 37 additions & 0 deletions map/index.js
@@ -0,0 +1,37 @@
import 'leaflet'

const resp = await fetch('/geojson/partners.geojson')
const data = await resp.json()

const merenschwandCenter = [47.25941, 8.37492]
const initialZoomLevel = 10
const map = L.map('map').setView(merenschwandCenter, initialZoomLevel)

const renderPopup = layer => {
const { NAME, url, comment, BFS_NUMMER } = layer.feature.properties
return `${NAME} ${BFS_NUMMER}
${url ? `<br/><a target="_blank" href="${url}">${url}</a>` : ''}
${comment ? `<br/>${comment}` : ''}`
}

const style = feature => {
const color =
feature.properties.status === 'P'
? 'green'
: feature.properties.status === 'I'
? 'red'
: '#BBB'
return { color }
}

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution:
'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}).addTo(map)

L.geoJSON(data, {
style,
})
.bindPopup(renderPopup)
.addTo(map)

0 comments on commit 19acf59

Please sign in to comment.