Spaces:
Sleeping
Sleeping
File size: 5,524 Bytes
60d3409 |
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
import ipyleaflet as L
from faicons import icon_svg
from geopy.distance import geodesic, great_circle
from shared import BASEMAPS, CITIES
from shiny import reactive
from shiny.express import input, render, ui
from shinywidgets import render_widget
city_names = sorted(list(CITIES.keys()))
ui.page_opts(title="Location Distance Calculator", fillable=True)
{"class": "bslib-page-dashboard"}
with ui.sidebar():
ui.input_selectize("loc1", "Location 1", choices=city_names, selected="New York")
ui.input_selectize("loc2", "Location 2", choices=city_names, selected="London")
ui.input_selectize(
"basemap",
"Choose a basemap",
choices=list(BASEMAPS.keys()),
selected="WorldImagery",
)
ui.input_dark_mode(mode="dark")
with ui.layout_column_wrap(fill=False):
with ui.value_box(showcase=icon_svg("globe"), theme="gradient-blue-indigo"):
"Great Circle Distance"
@render.text
def great_circle_dist():
circle = great_circle(loc1xy(), loc2xy())
return f"{circle.kilometers.__round__(1)} km"
with ui.value_box(showcase=icon_svg("ruler"), theme="gradient-blue-indigo"):
"Geodisic Distance"
@render.text
def geo_dist():
dist = geodesic(loc1xy(), loc2xy())
return f"{dist.kilometers.__round__(1)} km"
with ui.value_box(showcase=icon_svg("mountain"), theme="gradient-blue-indigo"):
"Altitude Difference"
@render.text
def altitude():
try:
return f'{loc1()["altitude"] - loc2()["altitude"]} m'
except TypeError:
return "N/A (altitude lookup failed)"
with ui.card():
ui.card_header("Map (drag the markers to change locations)")
@render_widget
def map():
return L.Map(zoom=4, center=(0, 0))
# Reactive values to store location information
loc1 = reactive.value()
loc2 = reactive.value()
# Update the reactive values when the selectize inputs change
@reactive.effect
def _():
loc1.set(CITIES.get(input.loc1(), loc_str_to_coords(input.loc1())))
loc2.set(CITIES.get(input.loc2(), loc_str_to_coords(input.loc2())))
# When a marker is moved, the input value gets updated to "lat, lon",
# so we decode that into a dict (and also look up the altitude)
def loc_str_to_coords(x: str) -> dict:
latlon = x.split(", ")
if len(latlon) != 2:
return {}
lat = float(latlon[0])
lon = float(latlon[1])
try:
import requests
query = f"https://api.open-elevation.com/api/v1/lookup?locations={lat},{lon}"
r = requests.get(query).json()
altitude = r["results"][0]["elevation"]
except Exception:
altitude = None
return {"latitude": lat, "longitude": lon, "altitude": altitude}
# Convenient way to get the lat/lons as a tuple
@reactive.calc
def loc1xy():
return loc1()["latitude"], loc1()["longitude"]
@reactive.calc
def loc2xy():
return loc2()["latitude"], loc2()["longitude"]
# Add marker for first location
@reactive.effect
def _():
update_marker(map.widget, loc1xy(), on_move1, "loc1")
# Add marker for second location
@reactive.effect
def _():
update_marker(map.widget, loc2xy(), on_move2, "loc2")
# Add line and fit bounds when either marker is moved
@reactive.effect
def _():
update_line(map.widget, loc1xy(), loc2xy())
# If new bounds fall outside of the current view, fit the bounds
@reactive.effect
def _():
l1 = loc1xy()
l2 = loc2xy()
lat_rng = [min(l1[0], l2[0]), max(l1[0], l2[0])]
lon_rng = [min(l1[1], l2[1]), max(l1[1], l2[1])]
new_bounds = [
[lat_rng[0], lon_rng[0]],
[lat_rng[1], lon_rng[1]],
]
b = map.widget.bounds
if len(b) == 0:
map.widget.fit_bounds(new_bounds)
elif (
lat_rng[0] < b[0][0]
or lat_rng[1] > b[1][0]
or lon_rng[0] < b[0][1]
or lon_rng[1] > b[1][1]
):
map.widget.fit_bounds(new_bounds)
# Update the basemap
@reactive.effect
def _():
update_basemap(map.widget, input.basemap())
# ---------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------
def update_marker(map: L.Map, loc: tuple, on_move: object, name: str):
remove_layer(map, name)
m = L.Marker(location=loc, draggable=True, name=name)
m.on_move(on_move)
map.add_layer(m)
def update_line(map: L.Map, loc1: tuple, loc2: tuple):
remove_layer(map, "line")
map.add_layer(
L.Polyline(locations=[loc1, loc2], color="blue", weight=2, name="line")
)
def update_basemap(map: L.Map, basemap: str):
for layer in map.layers:
if isinstance(layer, L.TileLayer):
map.remove_layer(layer)
map.add_layer(L.basemap_to_tiles(BASEMAPS[input.basemap()]))
def remove_layer(map: L.Map, name: str):
for layer in map.layers:
if layer.name == name:
map.remove_layer(layer)
def on_move1(**kwargs):
return on_move("loc1", **kwargs)
def on_move2(**kwargs):
return on_move("loc2", **kwargs)
# When the markers are moved, update the selectize inputs to include the new
# location (which results in the locations() reactive value getting updated,
# which invalidates any downstream reactivity that depends on it)
def on_move(id, **kwargs):
loc = kwargs["location"]
loc_str = f"{loc[0]}, {loc[1]}"
choices = city_names + [loc_str]
ui.update_selectize(id, selected=loc_str, choices=choices)
|