Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c001248d1b | |||
| f1c777d4ef | |||
| 931488c288 | |||
| ab897fd848 |
23
PROJECT.org
23
PROJECT.org
@ -88,7 +88,7 @@ fetching and simple saving.
|
|||||||
*** Metadata sources
|
*** Metadata sources
|
||||||
**** Scraper
|
**** Scraper
|
||||||
|
|
||||||
* Backlog [0/22] :vrobbler:project:personal:
|
* Backlog [0/23] :vrobbler:project:personal:
|
||||||
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
|
:ID: 702462cf-d54b-48c6-8a7c-78b8de751deb
|
||||||
@ -604,6 +604,27 @@ independent of the email flow it was originally creatdd for
|
|||||||
|
|
||||||
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
|
** TODO [#B] Is there way to create unique slugs for media instances :media_types:
|
||||||
|
|
||||||
|
** TODO [#C] What would it look like to add an MCP server to expose scrobbles and media items? :mcpserver:feature:
|
||||||
|
* Version 56.4 [3/3]
|
||||||
|
** DONE [#B] Add ability to do reverse address lookup on lat-long pairs :geolocations:feature:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 86c071ff-7638-41ba-6b65-1382df1cb5aa
|
||||||
|
:END:
|
||||||
|
** DONE [#B] Add address fields to GeoLocation :addresses:geolocation:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: a55ae508-07ab-ccdd-e453-846bd3fca6fb
|
||||||
|
:END:
|
||||||
|
** DONE [#B] Add better detail template for Disc Golf Courses :discgolf:templates:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 12cee67c-f723-0fa3-848a-cbc6e4d65fc3
|
||||||
|
:END:
|
||||||
|
|
||||||
|
* Version 56.3 [1/1]
|
||||||
|
** DONE [#A] Fix bug in importer script from discgolf being added :bug:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: c3733f96-18f1-eef8-f5d9-edaf97e35623
|
||||||
|
:END:
|
||||||
|
|
||||||
* Version 56.2 [1/1]
|
* Version 56.2 [1/1]
|
||||||
** DONE [#A] Fix bug in creating people when importing course plays :discgolf:bug:
|
** DONE [#A] Fix bug in creating people when importing course plays :discgolf:bug:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vrobbler"
|
name = "vrobbler"
|
||||||
version = "56.2"
|
version = "56.4"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Colin Powell <colin@unbl.ink>"]
|
authors = ["Colin Powell <colin@unbl.ink>"]
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from scrobbles.admin import ScrobbleInline
|
|||||||
@admin.register(DiscGolfCourse)
|
@admin.register(DiscGolfCourse)
|
||||||
class DiscGolfCourseAdmin(admin.ModelAdmin):
|
class DiscGolfCourseAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
list_display = ("title", "layout_name", "number_of_holes", "par_total")
|
list_display = ("title", "layout_name", "number_of_holes", "par_total", "pdga_slug", "udisc_id")
|
||||||
raw_id_fields = ("trail",)
|
raw_id_fields = ("trail",)
|
||||||
search_fields = ("title", "layout_name")
|
search_fields = ("title", "layout_name")
|
||||||
inlines = [
|
inlines = [
|
||||||
|
|||||||
@ -3,6 +3,9 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
|
|
||||||
class DiscGolfCourseSerializer(serializers.HyperlinkedModelSerializer):
|
class DiscGolfCourseSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
pdga_link = serializers.ReadOnlyField()
|
||||||
|
udisc_link = serializers.ReadOnlyField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.DiscGolfCourse
|
model = models.DiscGolfCourse
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-06-21 04:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("discgolf", "0003_discgolfcourse_par_per_hole"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="discgolfcourse",
|
||||||
|
name="pdga_slug",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="discgolfcourse",
|
||||||
|
name="udisc_id",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -39,6 +39,20 @@ class DiscGolfCourse(ScrobblableMixin):
|
|||||||
trail = models.ForeignKey(
|
trail = models.ForeignKey(
|
||||||
"trails.Trail", on_delete=models.DO_NOTHING, **BNULL
|
"trails.Trail", on_delete=models.DO_NOTHING, **BNULL
|
||||||
)
|
)
|
||||||
|
pdga_slug = models.CharField(max_length=255, **BNULL)
|
||||||
|
udisc_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pdga_link(self) -> str:
|
||||||
|
if self.pdga_slug:
|
||||||
|
return f"https://www.pdga.com/course-directory/course/{self.pdga_slug}/"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def udisc_link(self) -> str:
|
||||||
|
if self.udisc_id:
|
||||||
|
return f"https://udisc.com/courses/{self.udisc_id}/"
|
||||||
|
return ""
|
||||||
|
|
||||||
def get_absolute_url(self) -> str:
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("discgolf:course_detail", kwargs={"slug": self.uuid})
|
return reverse("discgolf:course_detail", kwargs={"slug": self.uuid})
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from django.apps import apps
|
||||||
|
|
||||||
from discgolf.models import DiscGolfCourse
|
from discgolf.models import DiscGolfCourse
|
||||||
|
|
||||||
from scrobbles.views import (
|
from scrobbles.views import (
|
||||||
@ -13,3 +15,18 @@ class DiscGolfCourseListView(ScrobbleableListView):
|
|||||||
|
|
||||||
class DiscGolfCourseDetailView(ScrobbleableDetailView, ChartContextMixin):
|
class DiscGolfCourseDetailView(ScrobbleableDetailView, ChartContextMixin):
|
||||||
model = DiscGolfCourse
|
model = DiscGolfCourse
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
Scrobble = apps.get_model("scrobbles", "Scrobble")
|
||||||
|
context["trail_gpx_url"] = None
|
||||||
|
latest = (
|
||||||
|
Scrobble.objects.filter(
|
||||||
|
trail=self.object.trail, gpx_file__isnull=False
|
||||||
|
)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if latest and latest.gpx_file:
|
||||||
|
context["trail_gpx_url"] = latest.gpx_file.url
|
||||||
|
return context
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from locations.models import GeoLocation
|
from locations.models import GeoLocation
|
||||||
|
|
||||||
@ -14,9 +17,29 @@ class GeoLocationAdmin(admin.ModelAdmin):
|
|||||||
"lon",
|
"lon",
|
||||||
"title",
|
"title",
|
||||||
"altitude",
|
"altitude",
|
||||||
|
"city",
|
||||||
|
"state_province",
|
||||||
|
"country",
|
||||||
)
|
)
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
search_fields = ("title",)
|
search_fields = ("title",)
|
||||||
|
actions = ["reverse_geocode_selected"]
|
||||||
inlines = [
|
inlines = [
|
||||||
ScrobbleInline,
|
ScrobbleInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@admin.action(description="Reverse geocode selected locations")
|
||||||
|
def reverse_geocode_selected(self, request: HttpRequest, queryset):
|
||||||
|
updated = 0
|
||||||
|
errors = 0
|
||||||
|
for i, location in enumerate(queryset.iterator()):
|
||||||
|
if location.reverse_geocode():
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
errors += 1
|
||||||
|
if i < queryset.count() - 1:
|
||||||
|
time.sleep(1.1)
|
||||||
|
msg = f"Reverse geocoded {updated} locations"
|
||||||
|
if errors:
|
||||||
|
msg += f", {errors} failed"
|
||||||
|
self.message_user(request, msg)
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 4.2.29 on 2026-06-21 04:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("locations", "0010_clean_start"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="geolocation",
|
||||||
|
name="city",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="geolocation",
|
||||||
|
name="country",
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="geolocation",
|
||||||
|
name="postal_code",
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="geolocation",
|
||||||
|
name="state_province",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="geolocation",
|
||||||
|
name="street",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -45,6 +45,11 @@ class GeoLocation(ScrobblableMixin):
|
|||||||
truncated_lat = models.FloatField(**BNULL)
|
truncated_lat = models.FloatField(**BNULL)
|
||||||
truncated_lon = models.FloatField(**BNULL)
|
truncated_lon = models.FloatField(**BNULL)
|
||||||
altitude = models.FloatField(**BNULL)
|
altitude = models.FloatField(**BNULL)
|
||||||
|
street = models.TextField(**BNULL)
|
||||||
|
city = models.CharField(max_length=255, **BNULL)
|
||||||
|
state_province = models.CharField(max_length=255, **BNULL)
|
||||||
|
postal_code = models.CharField(max_length=20, **BNULL)
|
||||||
|
country = models.CharField(max_length=100, **BNULL)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [["lat", "lon", "altitude"]]
|
unique_together = [["lat", "lon", "altitude"]]
|
||||||
@ -55,6 +60,11 @@ class GeoLocation(ScrobblableMixin):
|
|||||||
|
|
||||||
return f"{self.lat} x {self.lon}"
|
return f"{self.lat} x {self.lon}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_address(self) -> str:
|
||||||
|
parts = [self.street, self.city, self.state_province, self.postal_code, self.country]
|
||||||
|
return ", ".join(p for p in parts if p)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("locations:geolocation_detail", kwargs={"slug": self.uuid})
|
return reverse("locations:geolocation_detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
@ -121,6 +131,17 @@ class GeoLocation(ScrobblableMixin):
|
|||||||
|
|
||||||
return fetch_current_weather(self.lat, self.lon)
|
return fetch_current_weather(self.lat, self.lon)
|
||||||
|
|
||||||
|
def reverse_geocode(self) -> bool:
|
||||||
|
from locations.utils import reverse_geocode
|
||||||
|
|
||||||
|
result = reverse_geocode(self.lat, self.lon)
|
||||||
|
if result is None:
|
||||||
|
return False
|
||||||
|
for field, value in result.items():
|
||||||
|
setattr(self, field, value)
|
||||||
|
self.save(update_fields=list(result.keys()))
|
||||||
|
return True
|
||||||
|
|
||||||
def loc_diff(self, old_lat_lon: tuple) -> tuple:
|
def loc_diff(self, old_lat_lon: tuple) -> tuple:
|
||||||
return (
|
return (
|
||||||
abs(Decimal(old_lat_lon[0]) - Decimal(self.lat)),
|
abs(Decimal(old_lat_lon[0]) - Decimal(self.lat)),
|
||||||
|
|||||||
@ -201,6 +201,50 @@ def detect_movement(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
NOMINATIM_URL = "https://nominatim.openstreetmap.org/reverse"
|
||||||
|
|
||||||
|
USER_AGENT = "Vrobbler/1.0 (https://github.com/secstate/vrobbler)"
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_geocode(lat: float, lon: float) -> Optional[dict]:
|
||||||
|
"""Reverse geocode lat/lon to an address using Nominatim.
|
||||||
|
|
||||||
|
Returns a dict with address fields, or None on failure.
|
||||||
|
Nominatim usage policy: max 1 request per second.
|
||||||
|
"""
|
||||||
|
params = {
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"format": "json",
|
||||||
|
}
|
||||||
|
headers = {"User-Agent": USER_AGENT}
|
||||||
|
try:
|
||||||
|
resp = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.warning("Failed to reverse geocode %s,%s: %s", lat, lon, e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
if "error" in data:
|
||||||
|
logger.warning("Nominatim error for %s,%s: %s", lat, lon, data["error"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
address = data.get("address", {})
|
||||||
|
return {
|
||||||
|
"street": address.get("road")
|
||||||
|
or address.get("pedestrian")
|
||||||
|
or address.get("footway"),
|
||||||
|
"city": address.get("city")
|
||||||
|
or address.get("town")
|
||||||
|
or address.get("village")
|
||||||
|
or address.get("hamlet"),
|
||||||
|
"state_province": address.get("state"),
|
||||||
|
"postal_code": address.get("postcode"),
|
||||||
|
"country": address.get("country"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
NWS_URL = "https://forecast.weather.gov/MapClick.php"
|
NWS_URL = "https://forecast.weather.gov/MapClick.php"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,7 @@ def import_from_webdav_for_all_users(
|
|||||||
bgstats_count,
|
bgstats_count,
|
||||||
ebird_count,
|
ebird_count,
|
||||||
scale_count,
|
scale_count,
|
||||||
|
udisc_count,
|
||||||
extra={
|
extra={
|
||||||
"koreader": ko_count,
|
"koreader": ko_count,
|
||||||
"trail_gpx": gpx_count,
|
"trail_gpx": gpx_count,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from scrobbles.tasks import (
|
|||||||
add_favorite_to_mopidy_playlist,
|
add_favorite_to_mopidy_playlist,
|
||||||
CHARTABLE_MEDIA_TYPES,
|
CHARTABLE_MEDIA_TYPES,
|
||||||
remove_favorite_from_mopidy_playlist,
|
remove_favorite_from_mopidy_playlist,
|
||||||
|
reverse_geocode_geolocation,
|
||||||
SCROBBLES_WITHOUT_CHARTS,
|
SCROBBLES_WITHOUT_CHARTS,
|
||||||
update_charts_for_timestamp,
|
update_charts_for_timestamp,
|
||||||
)
|
)
|
||||||
@ -86,6 +87,31 @@ def add_tags_from_task_title(sender, instance, **kwargs):
|
|||||||
instance.tags.add(tag)
|
instance.tags.add(tag)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Scrobble)
|
||||||
|
def reverse_geocode_on_scrobble_creation(sender, instance, created, **kwargs):
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
if not instance.geo_location_id:
|
||||||
|
logger.info(
|
||||||
|
"Skipping reverse geocode: scrobble %s has no geo_location",
|
||||||
|
instance.id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if instance.geo_location.postal_code:
|
||||||
|
logger.info(
|
||||||
|
"Skipping reverse geocode: geo_location %s already has postal_code %s",
|
||||||
|
instance.geo_location_id,
|
||||||
|
instance.geo_location.postal_code,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Enqueuing reverse geocode for geo_location %s",
|
||||||
|
instance.geo_location_id,
|
||||||
|
)
|
||||||
|
reverse_geocode_geolocation.delay(instance.geo_location_id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=FavoriteMedia)
|
@receiver(post_save, sender=FavoriteMedia)
|
||||||
def add_to_mopidy_playlist_on_favorite(sender, instance, created, **kwargs):
|
def add_to_mopidy_playlist_on_favorite(sender, instance, created, **kwargs):
|
||||||
if not created:
|
if not created:
|
||||||
|
|||||||
@ -726,3 +726,41 @@ def add_scrobble_to_mopidy_monthly_playlist(scrobble_id):
|
|||||||
break
|
break
|
||||||
|
|
||||||
add_track_to_mopidy_monthly_playlist(scrobble)
|
add_track_to_mopidy_monthly_playlist(scrobble)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def reverse_geocode_geolocation(geo_location_id):
|
||||||
|
from locations.models import GeoLocation
|
||||||
|
|
||||||
|
location = GeoLocation.objects.filter(id=geo_location_id).first()
|
||||||
|
if not location:
|
||||||
|
logger.info(
|
||||||
|
"Skipping reverse geocode: geo_location %s not found",
|
||||||
|
geo_location_id,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if location.postal_code:
|
||||||
|
logger.info(
|
||||||
|
"Skipping reverse geocode: geo_location %s already has postal_code %s",
|
||||||
|
geo_location_id,
|
||||||
|
location.postal_code,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Reverse geocoding geo_location %s (%s, %s)",
|
||||||
|
geo_location_id,
|
||||||
|
location.lat,
|
||||||
|
location.lon,
|
||||||
|
)
|
||||||
|
if location.reverse_geocode():
|
||||||
|
logger.info(
|
||||||
|
"Reverse geocode succeeded for geo_location %s: %s",
|
||||||
|
geo_location_id,
|
||||||
|
location.display_address,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Reverse geocode failed for geo_location %s",
|
||||||
|
geo_location_id,
|
||||||
|
)
|
||||||
|
|||||||
@ -3,6 +3,15 @@
|
|||||||
|
|
||||||
{% block title %}{{object.title}}{% endblock %}
|
{% block title %}{{object.title}}{% endblock %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<style>
|
||||||
|
#map {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block lists %}
|
{% block lists %}
|
||||||
{% if object.description %}
|
{% if object.description %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -11,6 +20,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if trail_gpx_url %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md mb-3">
|
||||||
|
<div id="map"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
{% if object.trail.trailhead_location.display_address %}
|
||||||
|
<p>{{ object.trail.trailhead_location.display_address }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.pdga_link or object.udisc_link %}
|
||||||
|
<p>
|
||||||
|
{% if object.pdga_link %}
|
||||||
|
<a href="{{ object.pdga_link }}" target="_blank">PDGA</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.udisc_link %}
|
||||||
|
<a href="{{ object.udisc_link }}" target="_blank">uDisc</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if charts %}
|
{% if charts %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
@ -42,3 +75,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if trail_gpx_url %}
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/leaflet-gpx@2.2.0/gpx.min.js"></script>
|
||||||
|
<script>
|
||||||
|
var map = L.map('map');
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||||
|
referrerPolicy: 'origin'
|
||||||
|
}).addTo(map);
|
||||||
|
var gpx = new L.GPX("{{ trail_gpx_url|escapejs }}", {
|
||||||
|
async: true,
|
||||||
|
polyline_options: { color: '#e74c3c' }
|
||||||
|
});
|
||||||
|
gpx.on('loaded', function(e) {
|
||||||
|
map.fitBounds(e.target.getBounds());
|
||||||
|
});
|
||||||
|
gpx.addTo(map);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user