Py.Cafe

AIPHeX/

solara-ipyleaflet-click-map

Interactive Map and Click Tracker with Solara and ipyleaflet

DocsPricing
  • app.py
  • coords.npy
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import ipyleaflet
import solara
import numpy as np
import math

# --- load coordinates ---
coords = np.load("coords.npy")  # shape (113, 2) -> [lat, lon]

# --- build lavender markers (fixed, non-reactive) ---
#marker_style = dict(color="#b57edc", fill_color="#b57edc", fill_opacity=0.9, size=1)
markers = [
    ipyleaflet.Marker.element(location=(float(lat), float(lon)),fill_color="#b57edc")#**marker_style)
    for lat, lon in coords
]

# --- square bounds around centroid (+5% padding on the max span) ---
lats = coords[:, 0]; lons = coords[:, 1]
lat_c = float(lats.mean()); lon_c = float(lons.mean())
lat_span = float(lats.max() - lats.min())
lon_span = float(lons.max() - lons.min())
max_span = max(lat_span, lon_span, 1e-6)            # avoid zero-size
pad = 0.05 * max_span
half = max_span / 2 + pad

# square bounds in degrees around centroid
bounds = ((lat_c - half, lon_c - half), (lat_c + half, lon_c + half))
center = (lat_c, lon_c)

# simple zoom estimate to cover the square bounds on load
def estimate_zoom(b):
    (s, w), (n, e) = b
    lat_span_deg = n - s
    lon_span_deg = e - w
    mean_lat = (n + s) / 2.0
    lat_km = lat_span_deg * 111.32
    lon_km = lon_span_deg * 111.32 * math.cos(math.radians(mean_lat))
    span_km = max(lat_km, lon_km)
    if span_km <= 2: return 14
    if span_km <= 5: return 13
    if span_km <= 10: return 12
    if span_km <= 20: return 11
    if span_km <= 40: return 10
    if span_km <= 80: return 9
    if span_km <= 160: return 8
    if span_km <= 320: return 7
    if span_km <= 640: return 6
    return 5

zoom = estimate_zoom(bounds)

# --- basemap approximating "orange city plan with white roads, blue rivers" ---
# Using CARTO Voyager (no labels) -> white roads, blue rivers; land has a warm tint on many devices.
custom_tiles_url = "https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png"
maps = {
    "Orange city plan": custom_tiles_url,
    "OpenStreetMap.Mapnik": ipyleaflet.basemaps.OpenStreetMap.Mapnik.build_url(),
    "OpenTopoMap": ipyleaflet.basemaps.OpenTopoMap.build_url(),
    "Esri.WorldTopoMap": ipyleaflet.basemaps.Esri.WorldTopoMap.build_url(),
}
map_name = solara.reactive("Orange city plan")

@solara.component
def Page():
    with solara.Column(style={"width": "100%", "height": "500px"}):
        solara.Select(label="Map", value=map_name, values=list(maps))

        # Isolation prevents overlap on narrow screens
        with solara.Column(style={"isolation": "isolate"}):
            ipyleaflet.Map.element(  # type: ignore
                center=center,                 # fixed
                zoom=zoom,                     # fixed
                max_bounds=bounds,             # keep panning within the square bounds
                max_bounds_viscosity=1.0,
                scroll_wheel_zoom=True,
                layers=[
                    ipyleaflet.TileLayer.element(url=maps[map_name.value]),
                    *markers,                  # 113 lavender markers
                ],
            )