Add destination time difference and live local times on trip pages
Closes #208
This commit is contained in:
parent
016039e78f
commit
ec413ac310
2 changed files with 168 additions and 0 deletions
|
|
@ -99,6 +99,31 @@
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{# <div>Countries: {{ trip.countries_str }}</div> #}
|
{# <div>Countries: {{ trip.countries_str }}</div> #}
|
||||||
<div>Locations: {{ trip.locations_str }}</div>
|
<div>Locations: {{ trip.locations_str }}</div>
|
||||||
|
{% if destination_times %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Destination time zones</strong>
|
||||||
|
<table class="table table-sm table-hover w-auto mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Destination</th>
|
||||||
|
<th>Timezone</th>
|
||||||
|
<th>Difference from UK</th>
|
||||||
|
<th>Current local time</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in destination_times %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.location }} ({{ item.country_name }}) {{ item.country_flag if trip.show_flags }}</td>
|
||||||
|
<td>{{ item.timezone or "Unknown" }}</td>
|
||||||
|
<td class="destination-offset" {% if item.timezone %}data-timezone="{{ item.timezone }}"{% endif %}>{{ item.offset_display }}</td>
|
||||||
|
<td class="destination-time" {% if item.timezone %}data-timezone="{{ item.timezone }}"{% endif %}>{{ item.current_time or "Unknown" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if total_distance %}
|
{% if total_distance %}
|
||||||
<div>Total distance:
|
<div>Total distance:
|
||||||
|
|
@ -465,5 +490,46 @@ var routes = {{ routes | tojson }};
|
||||||
|
|
||||||
build_map("map", coordinates, routes);
|
build_map("map", coordinates, routes);
|
||||||
|
|
||||||
|
function timezoneOffsetLabel(offsetMinutes) {
|
||||||
|
if (offsetMinutes === 0) {
|
||||||
|
return "Same time as Bristol";
|
||||||
|
}
|
||||||
|
const sign = offsetMinutes > 0 ? "+" : "-";
|
||||||
|
const abs = Math.abs(offsetMinutes);
|
||||||
|
const hours = String(Math.floor(abs / 60)).padStart(2, "0");
|
||||||
|
const minutes = String(abs % 60).padStart(2, "0");
|
||||||
|
return `${sign}${hours}:${minutes} vs Bristol`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOffsetMinutes(timeZone) {
|
||||||
|
const now = new Date();
|
||||||
|
const localInZone = new Date(now.toLocaleString("en-US", { timeZone }));
|
||||||
|
const localInBristol = new Date(now.toLocaleString("en-US", { timeZone: "Europe/London" }));
|
||||||
|
return Math.round((localInZone - localInBristol) / 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDestinationTimes() {
|
||||||
|
for (const el of document.querySelectorAll(".destination-time[data-timezone]")) {
|
||||||
|
const tz = el.dataset.timezone;
|
||||||
|
el.textContent = new Intl.DateTimeFormat("en-GB", {
|
||||||
|
timeZone: tz,
|
||||||
|
weekday: "short",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false
|
||||||
|
}).format(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const el of document.querySelectorAll(".destination-offset[data-timezone]")) {
|
||||||
|
const tz = el.dataset.timezone;
|
||||||
|
const offset = getOffsetMinutes(tz);
|
||||||
|
el.textContent = timezoneOffsetLabel(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDestinationTimes();
|
||||||
|
setInterval(updateDestinationTimes, 1000);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
102
web_view.py
102
web_view.py
|
|
@ -11,10 +11,13 @@ import os.path
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import typing
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
import pytz
|
||||||
import werkzeug
|
import werkzeug
|
||||||
import werkzeug.debug.tbtools
|
import werkzeug.debug.tbtools
|
||||||
import yaml
|
import yaml
|
||||||
|
|
@ -761,6 +764,104 @@ def get_prev_current_and_next_trip(
|
||||||
return (prev_trip, current_trip, next_trip)
|
return (prev_trip, current_trip, next_trip)
|
||||||
|
|
||||||
|
|
||||||
|
def _timezone_name_from_datetime(value: typing.Any) -> str | None:
|
||||||
|
"""Get IANA timezone name from a datetime value if available."""
|
||||||
|
if not isinstance(value, datetime) or value.tzinfo is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
key = getattr(value.tzinfo, "key", None)
|
||||||
|
if isinstance(key, str):
|
||||||
|
return key
|
||||||
|
|
||||||
|
zone = getattr(value.tzinfo, "zone", None)
|
||||||
|
if isinstance(zone, str):
|
||||||
|
return zone
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_offset_from_bristol(offset_minutes: int) -> str:
|
||||||
|
"""Format offset from Bristol in +/-HH:MM."""
|
||||||
|
if offset_minutes == 0:
|
||||||
|
return "Same time as Bristol"
|
||||||
|
sign = "+" if offset_minutes > 0 else "-"
|
||||||
|
hours, mins = divmod(abs(offset_minutes), 60)
|
||||||
|
return f"{sign}{hours:02d}:{mins:02d} vs Bristol"
|
||||||
|
|
||||||
|
|
||||||
|
def get_destination_timezones(trip: Trip) -> list[StrDict]:
|
||||||
|
"""Build destination timezone metadata for the trip page."""
|
||||||
|
per_location: dict[tuple[str, str], list[str]] = defaultdict(list)
|
||||||
|
for item in trip.accommodation + trip.conferences + trip.events:
|
||||||
|
location = item.get("location")
|
||||||
|
country = item.get("country")
|
||||||
|
if not isinstance(location, str) or not isinstance(country, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (location, country.lower())
|
||||||
|
timezone_name = item.get("timezone")
|
||||||
|
if isinstance(timezone_name, str):
|
||||||
|
per_location[key].append(timezone_name)
|
||||||
|
|
||||||
|
for field in (
|
||||||
|
"from",
|
||||||
|
"to",
|
||||||
|
"date",
|
||||||
|
"start",
|
||||||
|
"end",
|
||||||
|
"attend_start",
|
||||||
|
"attend_end",
|
||||||
|
):
|
||||||
|
candidate = _timezone_name_from_datetime(item.get(field))
|
||||||
|
if candidate:
|
||||||
|
per_location[key].append(candidate)
|
||||||
|
|
||||||
|
home_now = datetime.now(ZoneInfo("Europe/London"))
|
||||||
|
destination_times: list[StrDict] = []
|
||||||
|
|
||||||
|
for location, country in trip.locations():
|
||||||
|
country_code = country.alpha_2.lower()
|
||||||
|
key = (location, country_code)
|
||||||
|
timezone_name = None
|
||||||
|
|
||||||
|
for candidate in per_location.get(key, []):
|
||||||
|
try:
|
||||||
|
ZoneInfo(candidate)
|
||||||
|
timezone_name = candidate
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not timezone_name:
|
||||||
|
country_timezones = pytz.country_timezones.get(country_code, [])
|
||||||
|
if len(country_timezones) == 1:
|
||||||
|
timezone_name = country_timezones[0]
|
||||||
|
|
||||||
|
offset_display = "Timezone unknown"
|
||||||
|
current_time = None
|
||||||
|
if timezone_name:
|
||||||
|
dest_now = datetime.now(ZoneInfo(timezone_name))
|
||||||
|
dest_offset = dest_now.utcoffset()
|
||||||
|
home_offset = home_now.utcoffset()
|
||||||
|
if dest_offset is not None and home_offset is not None:
|
||||||
|
offset_minutes = int((dest_offset - home_offset).total_seconds() // 60)
|
||||||
|
offset_display = _format_offset_from_bristol(offset_minutes)
|
||||||
|
current_time = dest_now.strftime("%a %H:%M:%S")
|
||||||
|
|
||||||
|
destination_times.append(
|
||||||
|
{
|
||||||
|
"location": location,
|
||||||
|
"country_name": country.name,
|
||||||
|
"country_flag": country.flag,
|
||||||
|
"timezone": timezone_name,
|
||||||
|
"offset_display": offset_display,
|
||||||
|
"current_time": current_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return destination_times
|
||||||
|
|
||||||
|
|
||||||
@app.route("/trip/<start>")
|
@app.route("/trip/<start>")
|
||||||
def trip_page(start: str) -> str:
|
def trip_page(start: str) -> str:
|
||||||
"""Individual trip page."""
|
"""Individual trip page."""
|
||||||
|
|
@ -802,6 +903,7 @@ def trip_page(start: str) -> str:
|
||||||
format_list_with_ampersand=format_list_with_ampersand,
|
format_list_with_ampersand=format_list_with_ampersand,
|
||||||
holidays=agenda.holidays.get_trip_holidays(trip),
|
holidays=agenda.holidays.get_trip_holidays(trip),
|
||||||
school_holidays=agenda.holidays.get_trip_school_holidays(trip),
|
school_holidays=agenda.holidays.get_trip_school_holidays(trip),
|
||||||
|
destination_times=get_destination_timezones(trip),
|
||||||
human_readable_delta=agenda.utils.human_readable_delta,
|
human_readable_delta=agenda.utils.human_readable_delta,
|
||||||
trip_weather=trip_weather,
|
trip_weather=trip_weather,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue