ID
diff --git a/every_election/apps/elections/views/general.py b/every_election/apps/elections/views/general.py
index f1558f6b4..6d1a50bdc 100644
--- a/every_election/apps/elections/views/general.py
+++ b/every_election/apps/elections/views/general.py
@@ -8,6 +8,7 @@
from elections.constants import ELECTION_TYPES
from elections.forms import NoticeOfElectionForm
from elections.models import ElectionType, Election, Document
+from og_images.svg_maker import SVGGenerator
class ElectionTypesView(ListView):
@@ -117,6 +118,8 @@ def get_context_data(self, **kwargs):
)
context["form"] = form
context["user_can_upload_docs"] = user_is_moderator(self.request.user)
+ if not self.object.group_type:
+ context["svg"] = SVGGenerator(self.object).svg()
return context
def post(self, *args, **kwargs):
diff --git a/every_election/apps/og_images/__init__.py b/every_election/apps/og_images/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/every_election/apps/og_images/admin.py b/every_election/apps/og_images/admin.py
new file mode 100644
index 000000000..8c38f3f3d
--- /dev/null
+++ b/every_election/apps/og_images/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/every_election/apps/og_images/apps.py b/every_election/apps/og_images/apps.py
new file mode 100644
index 000000000..3d5d2995e
--- /dev/null
+++ b/every_election/apps/og_images/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class OgImagesConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "og_images"
diff --git a/every_election/apps/og_images/management/__init__.py b/every_election/apps/og_images/management/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/every_election/apps/og_images/management/commands/__init__.py b/every_election/apps/og_images/management/commands/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/every_election/apps/og_images/management/commands/create_svg_thumbs.py b/every_election/apps/og_images/management/commands/create_svg_thumbs.py
new file mode 100644
index 000000000..7ad8c7568
--- /dev/null
+++ b/every_election/apps/og_images/management/commands/create_svg_thumbs.py
@@ -0,0 +1,9 @@
+from django.core.management import BaseCommand
+
+from og_images.svg_maker import SVGGenerator
+
+
+class Command(BaseCommand):
+ def handle(self, *args, **options):
+ svg = SVGGenerator(21888)
+ svg.write_svg()
diff --git a/every_election/apps/og_images/management/commands/og_images_create_layers.py b/every_election/apps/og_images/management/commands/og_images_create_layers.py
new file mode 100644
index 000000000..73e6065a8
--- /dev/null
+++ b/every_election/apps/og_images/management/commands/og_images_create_layers.py
@@ -0,0 +1,104 @@
+import subprocess
+from pathlib import Path
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+from django.db import connection
+
+layers = {
+ "openmap-local": {
+ "buildings": {"file_glob": "*_Building.shp"},
+ "roads": {"file_glob": "*_Road.shp"},
+ "surface_water": {"file_glob": "*_SurfaceWater_Area.shp"},
+ "tidal": {"file_glob": "*_TidalWater.shp"},
+ "stations": {"file_glob": "*_RailwayStation.shp"},
+ "railway_track": {"file_glob": "*_RailwayTrack.shp"},
+ "railway_tunnel": {"file_glob": "*_RailwayTunnel.shp"},
+ "roundabout": {"file_glob": "*_Roundabout.shp"},
+ },
+ "greenspaces": {"greenspaces": {"file_glob": "*_GreenspaceSite.shp"}},
+}
+
+
+class Command(BaseCommand):
+ help = "My shiny new management command."
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "data_dir",
+ action="store",
+ help="Path to OS Open Map Local data directory",
+ type=Path,
+ )
+ parser.add_argument(
+ "--product", default="openmap-local", action="store", choices=layers.keys()
+ )
+
+ def handle(self, *args, **options):
+ self.cursor = connection.cursor()
+
+ self.data_dir = options["data_dir"]
+
+ for layer, layer_data in layers[options["product"]].items():
+ self.create_table(layer, layer_data)
+
+ def table_name_from_layer_name(self, layer):
+ return f"og_images_layer_{layer}".replace("-", "_")
+
+ def layer_files(self, layer, layer_data):
+ return list(self.data_dir.glob(f"**/{layer_data['file_glob']}"))
+
+ def create_table(self, layer, layer_data):
+ """
+ Drop the old table
+ Generate layer SQL
+ Create the new table
+ """
+ self.cursor.execute(
+ f"""
+ DROP TABLE IF EXISTS {self.table_name_from_layer_name(layer)}
+ """,
+ )
+ layer_files = self.layer_files(layer, layer_data)
+ result = subprocess.run(
+ [
+ "shp2pgsql",
+ "-p",
+ "-I",
+ layer_files[0],
+ self.table_name_from_layer_name(layer),
+ ],
+ stdout=subprocess.PIPE,
+ )
+ self.cursor.execute(result.stdout)
+
+ import tempfile
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ for i, filename in enumerate(layer_files):
+ result = subprocess.run(
+ [
+ "shp2pgsql",
+ "-D", # Dump format
+ "-s", # Transform the SRID
+ "27700:4326", # From:to
+ "-a", # Append data, don't create table
+ filename,
+ self.table_name_from_layer_name(layer),
+ ],
+ stdout=subprocess.PIPE,
+ )
+ temp_path = Path(tmpdir) / f"{i}.dump"
+ with open(temp_path, "wb") as f:
+ f.write(result.stdout)
+ database = settings.DATABASES["default"]
+ connection_string = f"postgresql://{database['USER']}:{database['PASSWORD']}@{database['HOST']}/{database['NAME']}"
+ subprocess.run(
+ [
+ "psql",
+ connection_string,
+ "-f", # File
+ temp_path,
+ ],
+ stdout=subprocess.DEVNULL,
+ )
diff --git a/every_election/apps/og_images/migrations/__init__.py b/every_election/apps/og_images/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/every_election/apps/og_images/models.py b/every_election/apps/og_images/models.py
new file mode 100644
index 000000000..71a836239
--- /dev/null
+++ b/every_election/apps/og_images/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/every_election/apps/og_images/svg_maker.py b/every_election/apps/og_images/svg_maker.py
new file mode 100644
index 000000000..daebc6273
--- /dev/null
+++ b/every_election/apps/og_images/svg_maker.py
@@ -0,0 +1,319 @@
+from django.db import connection
+
+from elections.models import Election
+
+
+class SVGGenerator:
+ def __init__(self, ballot: Election):
+ if ballot.group_type:
+ raise ValueError(
+ f"Can only make a SVG for a ballot not {ballot.group_type=}"
+ )
+ self.ballot = ballot
+ if self.ballot.division:
+ self.geography_object_type = "organisations_divisiongeography"
+ self.area_object = self.ballot.division
+ else:
+ self.geography_object_type = "organisations_organisationeography"
+ self.area_object = self.ballot.organisation
+
+ self.divisionset_id = self.area_object.divisionset_id
+
+ self.cursor = connection.cursor()
+ self.bounding_box = self.get_bounding_box_geom()
+
+ self.svg_layers = []
+
+ self.object_svg = self.get_object_svg()
+ self.svg_layers.append(self.style_division(self.object_svg))
+
+ self.touching_svg = self.get_touching_svg()
+ self.svg_layers.insert(0, self.style_touching(self.touching_svg))
+ self.svg_layers += self.get_buildings()
+ self.svg_layers += self.get_roads()
+ self.svg_layers += self.get_roundabout()
+ self.svg_layers += self.get_surface_water()
+ self.svg_layers += self.get_tidal()
+ self.svg_layers += self.get_greenspace()
+ self.svg_layers += self.get_railway()
+ self.svg_layers += self.get_railway_tunnel()
+ self.svg_layers += self.get_stations()
+
+ def style_touching(self, touching):
+ return "\n".join(
+ [
+ f"""
"""
+ for path in touching
+ ]
+ )
+
+ def style_division(self, division):
+ return "\n".join(
+ [
+ f"""
"""
+ for path in division
+ ]
+ )
+
+ def get_viewbox(self):
+ self.cursor.execute(
+ """
+ select
+ ST_XMin(extent) ,
+ -ST_YMax(extent) ,
+ ST_XMax(extent) - ST_XMin(extent) as w,
+ ST_YMax(extent) - ST_YMin(extent) as h
+ FROM (
+ SELECT ST_Extent(ST_TRansform(%s::geometry, 27700)
+ ) AS extent)
+ AS bounding_box
+
+ """,
+ [self.bounding_box],
+ )
+
+ extent = self.cursor.fetchone()
+ self.width = extent[2]
+ return " ".join([str(x) for x in extent])
+
+ def svg(self):
+ viewbox = self.get_viewbox()
+ paths = "\n".join(
+ path.format(width=self.width * 0.001) for path in self.svg_layers
+ )
+ return f"""
+
+ """
+
+ def get_bounding_box_geom(self):
+ sql_str = f"""
+ SELECT st_transform(
+ st_envelope(
+ ST_MinimumBoundingCircle(
+ st_envelope(
+ ST_Transform(geog_table.geography, 27700)
+ )
+ )
+ )
+ , 4326)
+ FROM {self.geography_object_type} geog_table
+ WHERE division_id = %s;
+ """
+
+ self.cursor.execute(
+ sql_str,
+ [
+ self.area_object.pk,
+ ],
+ )
+ return self.cursor.fetchone()[0]
+
+ def get_touching_svg(self):
+ sql = f"""
+ SELECT
+ st_assvg(
+ ST_Transform(
+ st_intersection(
+ %s::geometry,
+ geom_table.geography)
+ , 27700)
+ ) AS gg
+ FROM {self.geography_object_type} geom_table
+ WHERE ST_Overlaps(
+ %s::geometry,
+ geom_table.geography
+ )
+ """
+ self.cursor.execute(
+ sql,
+ [
+ self.bounding_box,
+ self.bounding_box,
+ ],
+ )
+ svg_list = [row[0] for row in self.cursor.fetchall()]
+ return svg_list
+
+ def get_object_svg(self):
+ sql_str = f"""
+ SELECT
+ st_assvg(st_transform(geom_table.geography, 27700))
+ FROM {self.geography_object_type} geom_table
+ WHERE division_id = %s
+ """
+
+ self.cursor.execute(sql_str, [self.area_object.pk])
+ svg_list = [row[0] for row in self.cursor.fetchall()]
+ return svg_list
+
+ def get_buildings(self):
+ sql = f"""
+ SELECT st_assvg(ST_Transform(geom, 27700))
+ FROM og_images_layer_buildings
+ WHERE
+ st_coveredby(geom::geometry,
+ %s::geometry) AND
+ st_coveredby(
+ geom,
+ (SELECT geog_table.geography
+ FROM {self.geography_object_type} geog_table
+ WHERE
+
+ division_id = %s)
+ )
+ """
+ self.cursor.execute(
+ sql,
+ [
+ self.bounding_box,
+ self.area_object.pk,
+ ],
+ )
+ svg_list = [
+ f"""
"""
+ for row in self.cursor.fetchall()
+ ]
+
+ return svg_list
+
+ def get_roads(self):
+ sql = f"""
+ SELECT
+ drawlevel, st_assvg(
+ ST_Transform(
+-- st_intersection(
+-- %s::geometry,
+ geom
+-- )
+ , 27700)
+ ) AS gg
+ FROM og_images_layer_roads
+ WHERE
+ st_intersects(
+ geom::geometry,
+ %s::geometry
+
+ )
+ """
+ self.cursor.execute(
+ sql,
+ [
+ self.bounding_box,
+ self.bounding_box,
+ ],
+ )
+ svg_list = []
+ for row in self.cursor.fetchall():
+ scale = int(row[0]) + 1
+ scale = pow(scale, 2)
+ svg_list.append(
+ f"""
"""
+ )
+ return svg_list
+
+ def get_surface_water(self):
+ attrs = """
+ fill="rgba(0,206,209,0.2)" style="stroke-width:0.001;stroke:#000;stroke-opacity:0.9"
+ """
+ return self.add_bb_layer("surface_water", attrs)
+
+ def get_tidal(self):
+ attrs = """
+ fill="rgba(0,206,209,0.2)" style="stroke-width:0.001;stroke:#000;stroke-opacity:0.9"
+ """
+ return self.add_bb_layer("tidal", attrs)
+
+ def get_greenspace(self):
+ attrs = """
+ fill="rgba(0,100,0,0.2)"
+ style="stroke-width:0.001;stroke:#000;stroke-opacity:0.5"
+ """
+ return self.add_bb_layer("greenspaces", attrs)
+
+ def get_railway(self):
+ attrs = """
+ fill="rgba(0,100,0,0.2)"
+ style="stroke-width:9;stroke:#000;stroke-opacity:0.5"
+ """
+ return self.add_bb_layer("railway_track", attrs, covers_func="st_intersects")
+
+ def get_railway_tunnel(self):
+ attrs = """
+ fill="none"
+ style="stroke-width:11;stroke:#000;stroke-opacity:0.3"
+ """
+ return self.add_bb_layer("railway_tunnel", attrs, covers_func="st_intersects")
+
+ def get_stations(self):
+ attrs = """r="20" fill="rgba(255,0,0,0.6)" """
+ layer = self.add_bb_layer("stations", attrs, tag_name="circle")
+ return layer
+
+ def get_roundabout(self):
+ attrs = """r="10" fill="rgba(0,0,0,0.8)" """
+ layer = self.add_bb_layer("roundabout", attrs, tag_name="circle")
+ return layer
+
+ def add_bb_layer(
+ self, layer_name, attrs, tag_name="path", covers_func="st_coveredby"
+ ):
+ """
+ Adds a layer that will cover the entire bounding box
+
+ """
+
+ sql = f"""
+ SELECT
+ st_assvg(
+ ST_Transform(geom, 27700)
+ ) AS gg
+ FROM og_images_layer_{layer_name}
+ WHERE {covers_func}(
+ geom::geometry,
+ %s::geometry
+ )
+ """
+ self.cursor.execute(
+ sql,
+ [
+ self.bounding_box,
+ ],
+ )
+
+ svg_list = []
+ for row in self.cursor.fetchall():
+ if tag_name == "path":
+ data = f"""d="{row[0]}" """
+ else:
+ data = f"""{row[0]}"""
+
+ svg_list.append(f"""<{tag_name} {data} {attrs} />""")
+ return svg_list
diff --git a/every_election/apps/og_images/tests.py b/every_election/apps/og_images/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/every_election/apps/og_images/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/every_election/apps/og_images/views.py b/every_election/apps/og_images/views.py
new file mode 100644
index 000000000..91ea44a21
--- /dev/null
+++ b/every_election/apps/og_images/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/every_election/settings/base.py b/every_election/settings/base.py
index 2745daafd..cab5ca4c4 100644
--- a/every_election/settings/base.py
+++ b/every_election/settings/base.py
@@ -74,6 +74,7 @@ def str_bool_to_bool(str_bool):
"django_extensions",
"election_snooper",
"dc_utils",
+ "og_images",
]
INSTALLED_APPS += PROJECT_APPS