# Python GUI (Tkinter + tkintermapview)

This guide shows how to build a desktop Python application with an interactive map and Yoro code support using `tkintermapview` — a pure Python map widget.

## What you'll build

A standalone desktop app where users can:

- View a map with different tile providers
- Click to encode a location → see the Yoro code
- Enter a code → jump to that location
- See the cell bounds drawn on the map

## Install

```bash
pip install yoro tkintermapview
```

No Django or web server needed — this is a pure desktop application.

## Complete Application

```python
"""
Yoro Desktop Map — standalone GUI application.

Click on the map to get a Yoro code, or enter a code to jump to its location.
"""

import tkinter as tk
from tkinter import ttk
import yoro

try:
    from tkintermapview import TkinterMapView
except ImportError:
    print("Install tkintermapview: pip install tkintermapview")
    raise


class YoroMapApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Yoro — Geographic Addressing")
        self.root.geometry("1000x700")

        self.domain = "CI"
        self.precision = 12
        self.current_marker = None
        self.current_polygon = None

        self._build_ui()

    def _build_ui(self):
        # Top bar
        top = ttk.Frame(self.root, padding=8)
        top.pack(fill="x")

        # Domain selector
        ttk.Label(top, text="Domain:").pack(side="left")
        self.domain_var = tk.StringVar(value="CI")
        domain_combo = ttk.Combobox(
            top, textvariable=self.domain_var,
            values=sorted(yoro.DOMAINS.keys()), width=5, state="readonly"
        )
        domain_combo.pack(side="left", padx=(4, 16))
        domain_combo.bind("<<ComboboxSelected>>", lambda e: self._update_domain())

        # Precision selector
        ttk.Label(top, text="Precision:").pack(side="left")
        levels = yoro.precision_levels(domain=self.domain)
        precision_values = [f"p={lv['precision']} ({lv['code_length']} chars, ~{lv['resolution_m']:.0f}m)" for lv in levels]
        self.precision_var = tk.StringVar(value=precision_values[4])  # default p=12
        precision_combo = ttk.Combobox(
            top, textvariable=self.precision_var,
            values=precision_values, width=25, state="readonly"
        )
        precision_combo.pack(side="left", padx=(4, 16))
        precision_combo.bind("<<ComboboxSelected>>", lambda e: self._update_precision())
        self.precision_values = precision_values
        self.precision_levels = levels

        # Code input
        ttk.Label(top, text="Code:").pack(side="left")
        self.code_var = tk.StringVar()
        code_entry = ttk.Entry(top, textvariable=self.code_var, width=15, font=("Courier", 12))
        code_entry.pack(side="left", padx=4)
        code_entry.bind("<Return>", lambda e: self._decode_code())
        ttk.Button(top, text="Go", command=self._decode_code).pack(side="left")

        # Map widget
        self.map_widget = TkinterMapView(self.root, corner_radius=0)
        self.map_widget.pack(fill="both", expand=True)
        self.map_widget.set_position(7.5, -5.5)  # Cote d'Ivoire
        self.map_widget.set_zoom(7)

        # Click handler
        self.map_widget.add_right_click_menu_command(
            "Get Yoro Code", self._on_map_click, pass_coords=True
        )

        # Info bar
        self.info_var = tk.StringVar(value="Right-click on the map to get a Yoro code")
        ttk.Label(self.root, textvariable=self.info_var, padding=8,
                  font=("Courier", 11)).pack(fill="x")

    def _update_domain(self):
        self.domain = self.domain_var.get()
        # Update precision options for this domain
        levels = yoro.precision_levels(domain=self.domain)
        self.precision_levels = levels

    def _update_precision(self):
        idx = self.precision_values.index(self.precision_var.get())
        self.precision = self.precision_levels[idx]["precision"]

    def _on_map_click(self, coords):
        lat, lon = coords
        try:
            code = yoro.encode(lat, lon, precision=self.precision, domain=self.domain)
            decoded = yoro.decode(code)
            res = yoro.resolution(decoded["precision"], domain=self.domain)

            self.code_var.set(code)
            self.info_var.set(
                f"{code}  |  {decoded['lat']:.6f}, {decoded['lon']:.6f}  |  "
                f"~{res:.1f}m  |  p={decoded['precision']}"
            )

            self._draw_cell(decoded)

        except ValueError as e:
            self.info_var.set(f"Error: {e}")

    def _decode_code(self):
        code = self.code_var.get().strip()
        if not code:
            return
        try:
            decoded = yoro.decode(code)
            res = yoro.resolution(decoded["precision"], domain=decoded["domain"])
            self.info_var.set(
                f"{code.upper()}  |  {decoded['lat']:.6f}, {decoded['lon']:.6f}  |  "
                f"~{res:.1f}m  |  p={decoded['precision']}"
            )
            self.map_widget.set_position(decoded["lat"], decoded["lon"])
            self.map_widget.set_zoom(15)
            self._draw_cell(decoded)

        except ValueError as e:
            self.info_var.set(f"Error: {e}")

    def _draw_cell(self, decoded):
        # Remove previous marker/polygon
        if self.current_marker:
            self.current_marker.delete()
        if self.current_polygon:
            self.current_polygon.delete()

        b = decoded["bounds"]

        # Draw cell rectangle
        self.current_polygon = self.map_widget.set_polygon(
            [(b["lat_min"], b["lon_min"]),
             (b["lat_min"], b["lon_max"]),
             (b["lat_max"], b["lon_max"]),
             (b["lat_max"], b["lon_min"])],
            outline_color="red",
            fill_color=None,
        )

        # Place marker at center
        self.current_marker = self.map_widget.set_marker(
            decoded["lat"], decoded["lon"],
            text=decoded.get("domain", "") + "-" + str(decoded.get("precision", ""))
        )


if __name__ == "__main__":
    root = tk.Tk()
    app = YoroMapApp(root)
    root.mainloop()
```

## Running

```bash
python yoro_gui.py
```

Right-click on the map to encode a location. Use the top bar to change domain or precision. Type a code and press Enter to jump to it.

## Adapting for Other GUI Frameworks

### PyQt / PySide

Replace `tkintermapview` with [QtWebEngineWidgets](https://doc.qt.io/qtforpython/) embedding a Leaflet map:

```python
from PySide6.QtWebEngineWidgets import QWebEngineView

class MapWidget(QWebEngineView):
    def __init__(self):
        super().__init__()
        # Load Leaflet HTML template
        self.setHtml(LEAFLET_HTML)

    def encode_position(self, lat, lon):
        code = yoro.encode(lat, lon, domain="CI")
        # Call JavaScript to update the map
        self.page().runJavaScript(f"showCode('{code}')")
```

### Streamlit

```python
import streamlit as st
import folium
from streamlit_folium import st_folium
import yoro

st.title("Yoro Code Explorer")

col1, col2 = st.columns([3, 1])

with col2:
    domain = st.selectbox("Domain", sorted(yoro.DOMAINS.keys()), index=list(sorted(yoro.DOMAINS.keys())).index("CI"))
    code_input = st.text_input("Decode a code", placeholder="CI-PV9XD")
    precision = st.select_slider("Precision", options=[lv["precision"] for lv in yoro.precision_levels(domain=domain)], value=12)

with col1:
    m = folium.Map(location=[7.5, -5.5], zoom_start=7)

    if code_input:
        try:
            decoded = yoro.decode(code_input)
            b = decoded["bounds"]
            folium.Rectangle(
                bounds=[[b["lat_min"], b["lon_min"]], [b["lat_max"], b["lon_max"]]],
                color="red", weight=2, fill=True, fill_opacity=0.1,
            ).add_to(m)
            folium.Marker([decoded["lat"], decoded["lon"]], popup=code_input).add_to(m)
            m.location = [decoded["lat"], decoded["lon"]]
            m.zoom_start = 14
        except ValueError as e:
            st.error(str(e))

    result = st_folium(m, height=500)

    if result and result.get("last_clicked"):
        lat = result["last_clicked"]["lat"]
        lon = result["last_clicked"]["lng"]
        code = yoro.encode(lat, lon, precision=precision, domain=domain)
        st.success(f"**{code}**")
        decoded = yoro.decode(code)
        res = yoro.resolution(decoded["precision"], domain=domain)
        st.caption(f"{decoded['lat']:.6f}, {decoded['lon']:.6f} — ~{res:.1f}m resolution")
```

!!! info "All approaches work offline"
    Since `yoro.encode()` and `yoro.decode()` are pure math with zero dependencies, they work in any Python environment — desktop, mobile (Kivy/BeeWare), embedded, even WebAssembly (via Pyodide).
