kortforsyningen maps (#484)
* add maps app, add proxy view for kortforsyningen services, add static_src/js/kfmap.js to create leaflet maps with tiles from kortforsyningen, adjust facility views to use the new map, add marker field to FacilityType model to specify marker colour, add js and marker pngs for coloured markers * remove debug print Co-authored-by: Thomas Steen Rasmussen <tykling@bornhack.org>
|
@ -95,4 +95,4 @@ ECONOMYTEAM_EMAIL = "{{ django_economyteam_email }}"
|
|||
ECONOMYTEAM_NAME = "Economy"
|
||||
|
||||
BORNHACK_2019_OLD_TOKEN_TOKEN = "{{ bornhack_2019_old_token_token }}"
|
||||
|
||||
KORTFORSYNINGEN_TOKEN = "{{ kortforsyningen_token }}"
|
||||
|
|
|
@ -54,6 +54,7 @@ INSTALLED_APPS = [
|
|||
"wishlist",
|
||||
"facilities",
|
||||
"phonebook",
|
||||
"maps",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth_2fa",
|
||||
|
|
|
@ -53,6 +53,7 @@ urlpatterns = [
|
|||
path("api/", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
||||
path("camps/", CampListView.as_view(), name="camp_list"),
|
||||
path("token/", include("tokens.urls", namespace="tokens")),
|
||||
path("maps/", include("maps.urls", namespace="maps")),
|
||||
# camp redirect views here
|
||||
path(
|
||||
"",
|
||||
|
|
|
@ -12,8 +12,8 @@ class FacilityQuickFeedbackAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(FacilityType)
|
||||
class FacilityTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "description", "responsible_team", "camp"]
|
||||
list_filter = ["responsible_team__camp", "responsible_team"]
|
||||
list_display = ["name", "description", "icon", "marker", "responsible_team", "camp"]
|
||||
list_filter = ["responsible_team__camp", "icon", "marker", "responsible_team"]
|
||||
|
||||
|
||||
@admin.register(Facility)
|
||||
|
|
33
src/facilities/migrations/0004_facilitytype_marker.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 3.0.3 on 2020-03-10 11:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("facilities", "0003_location_not_null"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="facilitytype",
|
||||
name="marker",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("blueIcon", "Blue (#2A81CB)"),
|
||||
("goldIcon", "Gold (#FFD326)"),
|
||||
("redIcon", "Red (#CB2B3E)"),
|
||||
("greenIcon", "Green (#2AAD27)"),
|
||||
("orangeIcon", "Orange (#CB8427)"),
|
||||
("yellowIcon", "Yellow (#CAC428)"),
|
||||
("violetIcon", "Violet (#9C2BCB)"),
|
||||
("greyIcon", "Grey (#7B7B7B)"),
|
||||
("blackIcon", "Black (#3D3D3D)"),
|
||||
],
|
||||
default="blueIcon",
|
||||
help_text="The name/colour of the Leaflet marker to use for this facility type.",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError
|
|||
from django.db import models
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.text import slugify
|
||||
from maps.utils import LeafletMarkerChoices
|
||||
from utils.models import CampRelatedModel, UUIDModel
|
||||
|
||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||
|
@ -59,6 +60,13 @@ class FacilityType(CampRelatedModel):
|
|||
help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part.",
|
||||
)
|
||||
|
||||
marker = models.CharField(
|
||||
max_length=10,
|
||||
choices=LeafletMarkerChoices.choices,
|
||||
default=LeafletMarkerChoices.BLUE,
|
||||
help_text="The name/colour of the Leaflet marker to use for this facility type.",
|
||||
)
|
||||
|
||||
responsible_team = models.ForeignKey(
|
||||
"teams.Team",
|
||||
on_delete=models.PROTECT,
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
{% extends 'program_base.html' %}
|
||||
{% load leaflet_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'js/leaflet-1.6.0.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/proj4.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/proj4leaflet.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/leaflet-color-markers.js' %}" type="text/javascript"></script>
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -16,26 +20,29 @@
|
|||
<h3 class="panel-title">{{ facility.facility_type.name }}: {{ facility.name }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">{{ facility.description }}</p>
|
||||
<p class="lead">
|
||||
<i class="{{ facility.facility_type.icon }} fa-2x fa-pull-left fa-fw"></i> {{ facility.name }} {{ facility.description }}</p>
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url "facilities:facility_feedback" camp_slug=camp.slug facility_type_slug=facilitytype.slug facility_uuid=facility.uuid %}" class="btn btn-primary"><i class="fas fa-comment-dots"></i> Submit Feedback</a>
|
||||
{% endif %}
|
||||
<a href="{% url "facilities:facility_list" camp_slug=camp.slug facility_type_slug=facilitytype.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Back to {{ facilitytype.name }} list</a>
|
||||
<p>{% leaflet_map "facility_detail" %}</p>
|
||||
<p>
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
// add a listener to add the marker for the facility on the leaflet map after it inits
|
||||
window.addEventListener("map:init", function (e) {
|
||||
var detail = e.detail;
|
||||
function MapReadyCallback() {
|
||||
// add a marker for this facility
|
||||
{% url "facilities:facility_feedback" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as feedback %}
|
||||
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}]);
|
||||
marker.addTo(detail.map);
|
||||
marker.bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}");
|
||||
detail.map.setView([{{ facility_list.0.location.y }}, {{ facility_list.0.location.x }}], 15);
|
||||
}, false);
|
||||
var marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}])
|
||||
marker.bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}").addTo(this);
|
||||
// max zoom since we have only one marker
|
||||
this.setView(marker.getLatLng(), 13);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="{% static 'js/kfmap.js' %}" type="text/javascript"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
{% extends 'program_base.html' %}
|
||||
{% load leaflet_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% leaflet_js %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'js/leaflet-1.6.0.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/proj4.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/proj4leaflet.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/leaflet-color-markers.js' %}" type="text/javascript"></script>
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block title %}
|
||||
|
@ -25,34 +29,43 @@ Facilities of type {{ facilitytype }} | {{ block.super }}
|
|||
{% endif %}
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="{{ facility.facility_type.icon }} fa-2x fa-pull-left fa-fw"></i> {{ facility.name }}
|
||||
<img class="pull-right" src="{% static 'img/leaflet/marker-icon-'|add:facility.facility_type.marker|slice:"-4"|add:'.png' %}">
|
||||
</h4>
|
||||
<p class="list-group-item-text">{{ facility.description }}</p>
|
||||
<p class="list-group-item-text"><i>Location: Lat {{ facility.location.y }} Long {{ facility.location.x }}</i></p>
|
||||
{% if request.user.is_authenticated %}
|
||||
</a>
|
||||
{% else %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<p>
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
<p>{% leaflet_map "facility_list" %}</p>
|
||||
<a href="{% url "facilities:facility_type_list" camp_slug=camp.slug %}" class="btn btn-primary"><i class="fas fa-list"></i> Back to facility type list</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
// add a listener to add the marker for each facility on the leaflet map after it inits
|
||||
window.addEventListener("map:init", function (e) {
|
||||
var detail = e.detail;
|
||||
function MapReadyCallback() {
|
||||
// loop over facilities and add a marker for each,
|
||||
// include a popup in the marker
|
||||
var markers = new Array();
|
||||
{% for facility in facility_list %}
|
||||
{% url "facilities:facility_detail" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as detail %}
|
||||
{% url "facilities:facility_feedback" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as feedback %}
|
||||
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}]);
|
||||
marker.addTo(detail.map);
|
||||
marker.bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ detail }}' class='btn btn-primary' style='color: white;'><i class='fas fa-search'></i> Details</a><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}");
|
||||
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}], {icon: {{ facility.facility_type.marker }}}).bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ detail }}' class='btn btn-primary' style='color: white;'><i class='fas fa-search'></i> Details</a><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}").addTo(this);
|
||||
markers.push(marker.getLatLng());
|
||||
{% endfor %}
|
||||
detail.map.setView([{{ facility_list.0.location.y }}, {{ facility_list.0.location.x }}], 15);
|
||||
}, false);
|
||||
var markerBounds = L.latLngBounds(markers);
|
||||
// fitBounds appears to not respect maxZoom from the tilelayer,
|
||||
// leading to the "Error: Attempted to load an infinite number of tiles" mess,
|
||||
// so hardcode maxZoom to 13 here
|
||||
this.fitBounds(markerBounds, {maxZoom: 13});
|
||||
};
|
||||
</script>
|
||||
|
||||
<script src="{% static 'js/kfmap.js' %}" type="text/javascript"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,26 +1,63 @@
|
|||
{% extends 'program_base.html' %}
|
||||
{% load leaflet_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
Facility Types | {{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% leaflet_css %}
|
||||
<script src="{% static 'js/leaflet-1.6.0.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/proj4.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/proj4leaflet.js' %}" type="text/javascript"></script>
|
||||
<script src="{% static 'js/leaflet-color-markers.js' %}" type="text/javascript"></script>
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Facility Types</h3>
|
||||
<h3 class="panel-title">Facilities</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p class="lead">The {{ camp.title }} facilities are grouped in categories. Pick a category to limit the facilities shown.</p>
|
||||
<div class="list-group">
|
||||
{% for ft in facilitytype_list %}
|
||||
<a href="{% url 'facilities:facility_list' camp_slug=camp.slug facility_type_slug=ft.slug %}" class="list-group-item">
|
||||
<h4 class="list-group-item-heading">
|
||||
<i class="{{ ft.icon }} fa-2x fa-pull-left fa-fw"></i> {{ ft.name }}
|
||||
<img class="pull-right" src="{% static 'img/leaflet/marker-icon-'|add:ft.marker|slice:"-4"|add:'.png' %}">
|
||||
</h4>
|
||||
<p class="list-group-item-text">{{ ft.description }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p>
|
||||
<div id="map" class="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function MapReadyCallback() {
|
||||
// loop over facilities and add a marker for each,
|
||||
// include a popup in the marker
|
||||
var markers = new Array();
|
||||
{% for ft in facilitytype_list %}
|
||||
{% for facility in ft.facilities.all %}
|
||||
{% url "facilities:facility_detail" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as detail %}
|
||||
{% url "facilities:facility_feedback" camp_slug=facility.camp.slug facility_type_slug=facility.facility_type.slug facility_uuid=facility.uuid as feedback %}
|
||||
marker = L.marker([{{ facility.location.y }}, {{ facility.location.x }}], {icon: {{ facility.facility_type.marker }}}).bindPopup("<b>{{ facility.name }}</b><br><p>{{ facility.description }}</p><p>Responsible team: {{ facility.facility_type.responsible_team.name }} Team</p>{% if request.user.is_authenticated %}<p><a href='{{ detail }}' class='btn btn-primary' style='color: white;'><i class='fas fa-search'></i> Details</a><a href='{{ feedback }}' class='btn btn-primary' style='color: white;'><i class='fas fa-comment-dots'></i> Feedback</a></p>{% endif %}").addTo(this);
|
||||
markers.push(marker.getLatLng());
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
var markerBounds = L.latLngBounds(markers);
|
||||
// fitBounds appears to not respect maxZoom from the tilelayer,
|
||||
// leading to the "Error: Attempted to load an infinite number of tiles" mess,
|
||||
// so hardcode maxZoom to 13 here
|
||||
this.fitBounds(markerBounds, {maxZoom: 13});
|
||||
};
|
||||
</script>
|
||||
<script src="{% static 'js/kfmap.js' %}" type="text/javascript"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
0
src/maps/__init__.py
Normal file
5
src/maps/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MapsConfig(AppConfig):
|
||||
name = "maps"
|
0
src/maps/migrations/__init__.py
Normal file
9
src/maps/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.urls import re_path
|
||||
|
||||
from .views import MapProxyView
|
||||
|
||||
app_name = "maps"
|
||||
|
||||
urlpatterns = [
|
||||
re_path("kfproxy/(?P<path>.*)", MapProxyView.as_view(), name="proxy"),
|
||||
]
|
19
src/maps/utils.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class LeafletMarkerChoices(models.TextChoices):
|
||||
"""
|
||||
Leaflet icon color choices, a models.TextChoices class to use when we want to set
|
||||
choices for a model field to pick a marker colour for a Leaflet map.
|
||||
These map directly to the L.Icon() objects in static_src/js/leaflet-color-markers.js.
|
||||
"""
|
||||
|
||||
BLUE = "blueIcon", "Blue (#2A81CB)"
|
||||
GOLD = "goldIcon", "Gold (#FFD326)"
|
||||
RED = "redIcon", "Red (#CB2B3E)"
|
||||
GREEN = "greenIcon", "Green (#2AAD27)"
|
||||
ORANGE = "orangeIcon", "Orange (#CB8427)"
|
||||
YELLOW = "yellowIcon", "Yellow (#CAC428)"
|
||||
VIOLET = "violetIcon", "Violet (#9C2BCB)"
|
||||
GREY = "greyIcon", "Grey (#7B7B7B)"
|
||||
BLACK = "blackIcon", "Black (#3D3D3D)"
|
54
src/maps/views.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic import View
|
||||
|
||||
|
||||
class MapProxyView(View):
|
||||
"""
|
||||
Proxy for KortForsyningen map service. Created so we can show maps without
|
||||
leaking the IP of our visitors.
|
||||
"""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""
|
||||
Before we make the request we check that the path is in our whitelist.
|
||||
Before we return the response we copy headers except for a list we dont want.
|
||||
"""
|
||||
# only permit certain paths
|
||||
path = self.request.path.replace("/maps/kfproxy", "", 1)
|
||||
if path not in ["/orto_foraar", "/topo_skaermkort", "/mat", "/dhm"]:
|
||||
raise PermissionDenied("No thanks")
|
||||
|
||||
# ok, get the full path including querystring, and add our token
|
||||
fullpath = self.request.get_full_path().replace("/maps/kfproxy", "", 1)
|
||||
fullpath += f"&token={settings.KORTFORSYNINGEN_TOKEN}"
|
||||
|
||||
# make the request
|
||||
r = requests.get("https://services.kortforsyningen.dk" + fullpath)
|
||||
|
||||
# make the response
|
||||
response = HttpResponse(r.content, status=r.status_code)
|
||||
|
||||
# list of headers that cause trouble when proxying
|
||||
excluded_headers = [
|
||||
"connection",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailers",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]
|
||||
# proxy all headers from our upstream request to the response to our client,
|
||||
# if they headers are not in our list of troublemakers
|
||||
for key, value in r.headers.items():
|
||||
if key.lower() not in excluded_headers:
|
||||
response[key] = value
|
||||
|
||||
# all good, return the response
|
||||
return response
|
BIN
src/static_src/img/leaflet/marker-icon-2x-black.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-blue.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-gold.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-green.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-grey.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-orange.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-red.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-violet.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static_src/img/leaflet/marker-icon-2x-yellow.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/static_src/img/leaflet/marker-icon-black.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/static_src/img/leaflet/marker-icon-blue.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/static_src/img/leaflet/marker-icon-gold.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static_src/img/leaflet/marker-icon-green.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static_src/img/leaflet/marker-icon-grey.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/static_src/img/leaflet/marker-icon-orange.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static_src/img/leaflet/marker-icon-red.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static_src/img/leaflet/marker-icon-violet.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static_src/img/leaflet/marker-icon-yellow.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/static_src/img/leaflet/marker-shadow.png
Normal file
After Width: | Height: | Size: 608 B |
101
src/static_src/js/kfmap.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
(function() {
|
||||
|
||||
// Workaround for 1px lines appearing in some browsers due to fractional transforms
|
||||
// and resulting anti-aliasing.
|
||||
// https://github.com/Leaflet/Leaflet/issues/3575
|
||||
if (window.navigator.userAgent.indexOf('Chrome') > -1) {
|
||||
var originalInitTile = L.GridLayer.prototype._initTile;
|
||||
L.GridLayer.include({
|
||||
_initTile: function (tile) {
|
||||
originalInitTile.call(this, tile);
|
||||
var tileSize = this.getTileSize();
|
||||
tile.style.width = tileSize.x + 1 + 'px';
|
||||
tile.style.height = tileSize.y + 1 + 'px';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var myAttributionText = '© <a target="_blank" href="https://download.kortforsyningen.dk/content/vilk%C3%A5r-og-betingelser">Styrelsen for Dataforsyning og Effektivisering</a>';
|
||||
|
||||
// we need a custom crs for the map
|
||||
var crs = new L.Proj.CRS('EPSG:25832', '+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs', {
|
||||
resolutions: [1638.4,819.2,409.6,204.8,102.4,51.2,25.6,12.8,6.4,3.2,1.6,0.8,0.4,0.2],
|
||||
origin: [120000,6500000],
|
||||
bounds: L.bounds([120000, 5661139.2],[1378291.2, 6500000])
|
||||
});
|
||||
|
||||
// Make the map object using the custom projection
|
||||
var map = new L.Map('map', {
|
||||
crs: crs,
|
||||
center: [55.3, 9.9], // Set center location
|
||||
zoom: 9, // Initial zoom level
|
||||
minzoom: 0,
|
||||
maxzoom: 13,
|
||||
})
|
||||
|
||||
// Define ortophoto layer [WMTS:orto_foraar]
|
||||
var ortofotowmts = L.tileLayer('/maps/kfproxy/orto_foraar?request=GetTile&version=1.0.0&service=WMTS&Layer=orto_foraar&style=default&format=image/jpeg&TileMatrixSet=View1&TileMatrix={zoom}&TileRow={y}&TileCol={x}', {
|
||||
minZoom: 0,
|
||||
maxZoom: 13,
|
||||
attribution: myAttributionText,
|
||||
crossOrigin: true,
|
||||
zoom: function () {
|
||||
var zoomlevel = map._animateToZoom ? map._animateToZoom : map.getZoom();
|
||||
if (zoomlevel < 10)
|
||||
return 'L0' + zoomlevel;
|
||||
else
|
||||
return 'L' + zoomlevel;
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
// skaermkort layer [WMTS:topo_skaermkort]
|
||||
var toposkaermkortwmts = L.tileLayer.wms('/maps/kfproxy/topo_skaermkort', {
|
||||
layers: 'dtk_skaermkort',
|
||||
format: 'image/png',
|
||||
attribution: myAttributionText
|
||||
});
|
||||
|
||||
// hillshade tile layer [WMTS:dhm]
|
||||
var dhmwmts = L.tileLayer.wms('/maps/kfproxy/dhm', {
|
||||
layers: 'dhm_terraen_skyggekort_overdrevet',
|
||||
format: 'image/png',
|
||||
attribution: myAttributionText
|
||||
});
|
||||
|
||||
// Matrikelskel overlay [WMS:mat]
|
||||
var matrikel = L.tileLayer.wms('/maps/kfproxy/mat', {
|
||||
transparent: true,
|
||||
layers: 'MatrikelSkel,Centroide',
|
||||
format: 'image/png',
|
||||
attribution: myAttributionText,
|
||||
minZoom: 9
|
||||
}).addTo(map); // addTo means that the layer is visible by default
|
||||
|
||||
// Hillshade overlay [WMS:dhm]
|
||||
var hillshade = L.tileLayer.wms('/maps/kfproxy/dhm', {
|
||||
transparent: true,
|
||||
layers: 'dhm_terraen_skyggekort_transparent_overdrevet',
|
||||
format: 'image/png',
|
||||
attribution: myAttributionText,
|
||||
});
|
||||
|
||||
// Define layer groups for layer control
|
||||
var baseLayers = {
|
||||
"Ortophoto Map": ortofotowmts,
|
||||
"Regular Map": toposkaermkortwmts,
|
||||
"Hillshade Map": dhmwmts
|
||||
};
|
||||
var overlays = {
|
||||
"Cadastral Map Overlay": matrikel,
|
||||
"Hillshade Map Overlay": hillshade
|
||||
};
|
||||
|
||||
// Add layer control to map
|
||||
L.control.layers(baseLayers, overlays).addTo(map);
|
||||
|
||||
// Add scale line to map, disable imperial units
|
||||
L.control.scale({imperial: false}).addTo(map);
|
||||
|
||||
// fire our callback when ready
|
||||
map.whenReady(MapReadyCallback);
|
||||
})();
|
5
src/static_src/js/leaflet-1.6.0.js
Normal file
82
src/static_src/js/leaflet-color-markers.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
// borrowed from https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/js/leaflet-color-markers.js
|
||||
|
||||
var blueIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-blue.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var goldIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-gold.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var redIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-red.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var greenIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-green.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var orangeIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-orange.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var yellowIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-yellow.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var violetIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-violet.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var greyIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-grey.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
|
||||
var blackIcon = new L.Icon({
|
||||
iconUrl: '/static/img/leaflet/marker-icon-2x-black.png',
|
||||
shadowUrl: '/static/img/leaflet/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
1
src/static_src/js/proj4.js
Normal file
272
src/static_src/js/proj4leaflet.js
Normal file
|
@ -0,0 +1,272 @@
|
|||
(function (factory) {
|
||||
var L, proj4;
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(['leaflet', 'proj4'], factory);
|
||||
} else if (typeof module === 'object' && typeof module.exports === "object") {
|
||||
// Node/CommonJS
|
||||
L = require('leaflet');
|
||||
proj4 = require('proj4');
|
||||
module.exports = factory(L, proj4);
|
||||
} else {
|
||||
// Browser globals
|
||||
if (typeof window.L === 'undefined' || typeof window.proj4 === 'undefined')
|
||||
throw 'Leaflet and proj4 must be loaded first';
|
||||
factory(window.L, window.proj4);
|
||||
}
|
||||
}(function (L, proj4) {
|
||||
if (proj4.__esModule && proj4.default) {
|
||||
// If proj4 was bundled as an ES6 module, unwrap it to get
|
||||
// to the actual main proj4 object.
|
||||
// See discussion in https://github.com/kartena/Proj4Leaflet/pull/147
|
||||
proj4 = proj4.default;
|
||||
}
|
||||
|
||||
L.Proj = {};
|
||||
|
||||
L.Proj._isProj4Obj = function(a) {
|
||||
return (typeof a.inverse !== 'undefined' &&
|
||||
typeof a.forward !== 'undefined');
|
||||
};
|
||||
|
||||
L.Proj.Projection = L.Class.extend({
|
||||
initialize: function(code, def, bounds) {
|
||||
var isP4 = L.Proj._isProj4Obj(code);
|
||||
this._proj = isP4 ? code : this._projFromCodeDef(code, def);
|
||||
this.bounds = isP4 ? def : bounds;
|
||||
},
|
||||
|
||||
project: function (latlng) {
|
||||
var point = this._proj.forward([latlng.lng, latlng.lat]);
|
||||
return new L.Point(point[0], point[1]);
|
||||
},
|
||||
|
||||
unproject: function (point, unbounded) {
|
||||
var point2 = this._proj.inverse([point.x, point.y]);
|
||||
return new L.LatLng(point2[1], point2[0], unbounded);
|
||||
},
|
||||
|
||||
_projFromCodeDef: function(code, def) {
|
||||
if (def) {
|
||||
proj4.defs(code, def);
|
||||
} else if (proj4.defs[code] === undefined) {
|
||||
var urn = code.split(':');
|
||||
if (urn.length > 3) {
|
||||
code = urn[urn.length - 3] + ':' + urn[urn.length - 1];
|
||||
}
|
||||
if (proj4.defs[code] === undefined) {
|
||||
throw 'No projection definition for code ' + code;
|
||||
}
|
||||
}
|
||||
|
||||
return proj4(code);
|
||||
}
|
||||
});
|
||||
|
||||
L.Proj.CRS = L.Class.extend({
|
||||
includes: L.CRS,
|
||||
|
||||
options: {
|
||||
transformation: new L.Transformation(1, 0, -1, 0)
|
||||
},
|
||||
|
||||
initialize: function(a, b, c) {
|
||||
var code,
|
||||
proj,
|
||||
def,
|
||||
options;
|
||||
|
||||
if (L.Proj._isProj4Obj(a)) {
|
||||
proj = a;
|
||||
code = proj.srsCode;
|
||||
options = b || {};
|
||||
|
||||
this.projection = new L.Proj.Projection(proj, options.bounds);
|
||||
} else {
|
||||
code = a;
|
||||
def = b;
|
||||
options = c || {};
|
||||
this.projection = new L.Proj.Projection(code, def, options.bounds);
|
||||
}
|
||||
|
||||
L.Util.setOptions(this, options);
|
||||
this.code = code;
|
||||
this.transformation = this.options.transformation;
|
||||
|
||||
if (this.options.origin) {
|
||||
this.transformation =
|
||||
new L.Transformation(1, -this.options.origin[0],
|
||||
-1, this.options.origin[1]);
|
||||
}
|
||||
|
||||
if (this.options.scales) {
|
||||
this._scales = this.options.scales;
|
||||
} else if (this.options.resolutions) {
|
||||
this._scales = [];
|
||||
for (var i = this.options.resolutions.length - 1; i >= 0; i--) {
|
||||
if (this.options.resolutions[i]) {
|
||||
this._scales[i] = 1 / this.options.resolutions[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.infinite = !this.options.bounds;
|
||||
|
||||
},
|
||||
|
||||
scale: function(zoom) {
|
||||
var iZoom = Math.floor(zoom),
|
||||
baseScale,
|
||||
nextScale,
|
||||
scaleDiff,
|
||||
zDiff;
|
||||
if (zoom === iZoom) {
|
||||
return this._scales[zoom];
|
||||
} else {
|
||||
// Non-integer zoom, interpolate
|
||||
baseScale = this._scales[iZoom];
|
||||
nextScale = this._scales[iZoom + 1];
|
||||
scaleDiff = nextScale - baseScale;
|
||||
zDiff = (zoom - iZoom);
|
||||
return baseScale + scaleDiff * zDiff;
|
||||
}
|
||||
},
|
||||
|
||||
zoom: function(scale) {
|
||||
// Find closest number in this._scales, down
|
||||
var downScale = this._closestElement(this._scales, scale),
|
||||
downZoom = this._scales.indexOf(downScale),
|
||||
nextScale,
|
||||
nextZoom,
|
||||
scaleDiff;
|
||||
// Check if scale is downScale => return array index
|
||||
if (scale === downScale) {
|
||||
return downZoom;
|
||||
}
|
||||
if (downScale === undefined) {
|
||||
return -Infinity;
|
||||
}
|
||||
// Interpolate
|
||||
nextZoom = downZoom + 1;
|
||||
nextScale = this._scales[nextZoom];
|
||||
if (nextScale === undefined) {
|
||||
return Infinity;
|
||||
}
|
||||
scaleDiff = nextScale - downScale;
|
||||
return (scale - downScale) / scaleDiff + downZoom;
|
||||
},
|
||||
|
||||
distance: L.CRS.Earth.distance,
|
||||
|
||||
R: L.CRS.Earth.R,
|
||||
|
||||
/* Get the closest lowest element in an array */
|
||||
_closestElement: function(array, element) {
|
||||
var low;
|
||||
for (var i = array.length; i--;) {
|
||||
if (array[i] <= element && (low === undefined || low < array[i])) {
|
||||
low = array[i];
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
});
|
||||
|
||||
L.Proj.GeoJSON = L.GeoJSON.extend({
|
||||
initialize: function(geojson, options) {
|
||||
this._callLevel = 0;
|
||||
L.GeoJSON.prototype.initialize.call(this, geojson, options);
|
||||
},
|
||||
|
||||
addData: function(geojson) {
|
||||
var crs;
|
||||
|
||||
if (geojson) {
|
||||
if (geojson.crs && geojson.crs.type === 'name') {
|
||||
crs = new L.Proj.CRS(geojson.crs.properties.name);
|
||||
} else if (geojson.crs && geojson.crs.type) {
|
||||
crs = new L.Proj.CRS(geojson.crs.type + ':' + geojson.crs.properties.code);
|
||||
}
|
||||
|
||||
if (crs !== undefined) {
|
||||
this.options.coordsToLatLng = function(coords) {
|
||||
var point = L.point(coords[0], coords[1]);
|
||||
return crs.projection.unproject(point);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Base class' addData might call us recursively, but
|
||||
// CRS shouldn't be cleared in that case, since CRS applies
|
||||
// to the whole GeoJSON, inluding sub-features.
|
||||
this._callLevel++;
|
||||
try {
|
||||
L.GeoJSON.prototype.addData.call(this, geojson);
|
||||
} finally {
|
||||
this._callLevel--;
|
||||
if (this._callLevel === 0) {
|
||||
delete this.options.coordsToLatLng;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
L.Proj.geoJson = function(geojson, options) {
|
||||
return new L.Proj.GeoJSON(geojson, options);
|
||||
};
|
||||
|
||||
L.Proj.ImageOverlay = L.ImageOverlay.extend({
|
||||
initialize: function (url, bounds, options) {
|
||||
L.ImageOverlay.prototype.initialize.call(this, url, null, options);
|
||||
this._projectedBounds = bounds;
|
||||
},
|
||||
|
||||
// Danger ahead: Overriding internal methods in Leaflet.
|
||||
// Decided to do this rather than making a copy of L.ImageOverlay
|
||||
// and doing very tiny modifications to it.
|
||||
// Future will tell if this was wise or not.
|
||||
_animateZoom: function (event) {
|
||||
var scale = this._map.getZoomScale(event.zoom);
|
||||
var northWest = L.point(this._projectedBounds.min.x, this._projectedBounds.max.y);
|
||||
var offset = this._projectedToNewLayerPoint(northWest, event.zoom, event.center);
|
||||
|
||||
L.DomUtil.setTransform(this._image, offset, scale);
|
||||
},
|
||||
|
||||
_reset: function () {
|
||||
var zoom = this._map.getZoom();
|
||||
var pixelOrigin = this._map.getPixelOrigin();
|
||||
var bounds = L.bounds(
|
||||
this._transform(this._projectedBounds.min, zoom)._subtract(pixelOrigin),
|
||||
this._transform(this._projectedBounds.max, zoom)._subtract(pixelOrigin)
|
||||
);
|
||||
var size = bounds.getSize();
|
||||
|
||||
L.DomUtil.setPosition(this._image, bounds.min);
|
||||
this._image.style.width = size.x + 'px';
|
||||
this._image.style.height = size.y + 'px';
|
||||
},
|
||||
|
||||
_projectedToNewLayerPoint: function (point, zoom, center) {
|
||||
var viewHalf = this._map.getSize()._divideBy(2);
|
||||
var newTopLeft = this._map.project(center, zoom)._subtract(viewHalf)._round();
|
||||
var topLeft = newTopLeft.add(this._map._getMapPanePos());
|
||||
|
||||
return this._transform(point, zoom)._subtract(topLeft);
|
||||
},
|
||||
|
||||
_transform: function (point, zoom) {
|
||||
var crs = this._map.options.crs;
|
||||
var transformation = crs.transformation;
|
||||
var scale = crs.scale(zoom);
|
||||
|
||||
return transformation.transform(point, scale);
|
||||
}
|
||||
});
|
||||
|
||||
L.Proj.imageOverlay = function (url, bounds, options) {
|
||||
return new L.Proj.ImageOverlay(url, bounds, options);
|
||||
};
|
||||
|
||||
return L.Proj;
|
||||
}));
|
|
@ -203,6 +203,7 @@ class Command(BaseCommand):
|
|||
name="Toilets",
|
||||
description="All the toilets",
|
||||
icon="fas fa-toilet",
|
||||
marker="greyIcon",
|
||||
responsible_team=teams["shit"],
|
||||
)
|
||||
types["toilet"].quickfeedback_options.add(options["na"])
|
||||
|
@ -214,6 +215,7 @@ class Command(BaseCommand):
|
|||
name="Power Infrastructure",
|
||||
description="Power related infrastructure, distribution points, distribution cables, and so on.",
|
||||
icon="fas fa-plug",
|
||||
marker="goldIcon",
|
||||
responsible_team=teams["power"],
|
||||
)
|
||||
types["power"].quickfeedback_options.add(options["attention"])
|
||||
|
@ -225,39 +227,39 @@ class Command(BaseCommand):
|
|||
self.output("Creating facilities...")
|
||||
facilities["toilet1"] = Facility.objects.create(
|
||||
facility_type=facility_types["toilet"],
|
||||
name="Toilet A1",
|
||||
description="Toilet on the left side in the NOC building",
|
||||
location=Point(1, 2),
|
||||
name="Toilet NOC East",
|
||||
description="Toilet on the east side of the NOC building",
|
||||
location=Point(9.939783, 55.387217),
|
||||
)
|
||||
facilities["toilet2"] = Facility.objects.create(
|
||||
facility_type=facility_types["toilet"],
|
||||
name="Toilet A2",
|
||||
description="Toilet on the right side in the NOC building",
|
||||
location=Point(3, 4),
|
||||
name="Toilet NOC West",
|
||||
description="Toilet on the west side of the NOC building",
|
||||
location=Point(9.93967, 55.387197),
|
||||
)
|
||||
facilities["pdp1"] = Facility.objects.create(
|
||||
facility_type=facility_types["power"],
|
||||
name="PDP1",
|
||||
description="In orga area",
|
||||
location=Point(5, 6),
|
||||
location=Point(9.94079, 55.388022),
|
||||
)
|
||||
facilities["pdp2"] = Facility.objects.create(
|
||||
facility_type=facility_types["power"],
|
||||
name="PDP2",
|
||||
description="In bar area",
|
||||
location=Point(7, 8),
|
||||
location=Point(9.942036, 55.387891),
|
||||
)
|
||||
facilities["pdp3"] = Facility.objects.create(
|
||||
facility_type=facility_types["power"],
|
||||
name="PDP3",
|
||||
description="In speaker tent",
|
||||
location=Point(9, 10),
|
||||
location=Point(9.938416, 55.387109),
|
||||
)
|
||||
facilities["pdp4"] = Facility.objects.create(
|
||||
facility_type=facility_types["power"],
|
||||
name="PDP4",
|
||||
description="In food area",
|
||||
location=Point(11, 12),
|
||||
location=Point(9.940146, 55.386983),
|
||||
)
|
||||
return facilities
|
||||
|
||||
|
|