From 130cffaa2621a6e54209914a0c3c28318e34891b Mon Sep 17 00:00:00 2001
From: XANTRONIX Industrial <xan@xantronix.com>
Date: Mon, 24 Mar 2025 21:31:37 -0400
Subject: [PATCH] Initial implementation of map.py

---
 lib/xmet/map.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)
 create mode 100644 lib/xmet/map.py

diff --git a/lib/xmet/map.py b/lib/xmet/map.py
new file mode 100644
index 0000000..f848639
--- /dev/null
+++ b/lib/xmet/map.py
@@ -0,0 +1,65 @@
+import cairo
+import shapely
+import gi
+
+gi.require_version('Rsvg', '2.0')
+
+from gi.repository import Rsvg
+
+MAP_BOUNDS = (
+     49.8, # North
+    -66.5, # East
+     24.2, # South
+   -125.5  # West
+)
+
+MAP_DIMENSIONS = (MAP_BOUNDS[0] - MAP_BOUNDS[2],
+                  MAP_BOUNDS[1] - MAP_BOUNDS[3])
+
+MAP_SCREEN_DIMENSIONS = (1859.0, 968.0)
+
+class EquirectMap():
+    def __init__(self,
+                 width: float,
+                 height: float,
+                 bounds: tuple[float, float, float, float]):
+        self.width      = width
+        self.height     = height
+        self.bounds     = bounds
+        self.map_width  = bounds[1] - bounds[3]
+        self.map_height = bounds[0] - bounds[2]
+
+    def draw_from_file(self, cr: cairo.Context, path: str):
+        svg = Rsvg.Handle.new_from_file(path)
+
+        rect = Rsvg.Rectangle()
+        rect.x      = 0
+        rect.y      = 0
+        rect.width  = self.width
+        rect.height = self.height
+
+        svg.render_layer(cr, None, rect)
+
+    def screen_to_map(self, x: float, y: float) -> shapely.Point:
+        lon = self.bounds[3] + (x / self.width)  * self.width
+        lat = self.bounds[0] + (y / self.height) * self.height
+
+        return shapely.Point(lon, lat)
+
+    def map_to_screen(self, point: shapely.Point) -> tuple[float, float]:
+        x = ((point.x - self.bounds[3]) / self.map_width)  * self.width
+        y = self.height - ((point.y - self.bounds[2]) / self.map_height) * self.height
+
+        return x, y
+
+    def draw_polygon(self, cr: cairo.Context, poly: shapely.Polygon):
+        first = True
+
+        for point in poly.exterior.coords:
+            x, y = self.map_to_screen(shapely.Point(*point))
+
+            if first:
+                cr.move_to(x, y)
+                first = False
+            else:
+                cr.line_to(x, y)