diff --git a/src/fellchensammlung/static/fellchensammlung/img/pin.png b/src/fellchensammlung/static/fellchensammlung/img/pin.png new file mode 100644 index 0000000..8b5b31d Binary files /dev/null and b/src/fellchensammlung/static/fellchensammlung/img/pin.png differ diff --git a/src/fellchensammlung/templates/fellchensammlung/partials/partial-map.html b/src/fellchensammlung/templates/fellchensammlung/partials/partial-map.html index b29ed9f..186fe75 100644 --- a/src/fellchensammlung/templates/fellchensammlung/partials/partial-map.html +++ b/src/fellchensammlung/templates/fellchensammlung/partials/partial-map.html @@ -71,6 +71,136 @@ {% endif %} {% endfor %} + map.on('load', async () => { + image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}'); + map.addImage('pin', image.data); + {% for map_pin in map_pins %} + map.addSource('point', { + 'type': 'geojson', + 'data': { + 'type': 'FeatureCollection', + 'features': [ + { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [parseFloat({{ map_pin.location.longitude }}), + parseFloat({{ map_pin.location.latitude }})] + } + } + ] + } + }); + {% endfor %} + map.addLayer({ + 'id': 'pints', + 'type': 'symbol', + 'source': 'point', + 'layout': { + 'icon-image': 'pin', + 'icon-size': 0.1 + } + }); + }); + + const size = 200; + + // implementation of StyleImageInterface to draw a pulsing dot icon on the map + // Search for StyleImageInterface in https://maplibre.org/maplibre-gl-js/docs/API/ + const pulsingDot = { + width: size, + height: size, + data: new Uint8Array(size * size * 4), + + // get rendering context for the map canvas when layer is added to the map + onAdd() { + const canvas = document.createElement('canvas'); + canvas.width = this.width; + canvas.height = this.height; + this.context = canvas.getContext('2d'); + }, + + // called once before every frame where the icon will be used + render() { + const duration = 1000; + const t = (performance.now() % duration) / duration; + + const radius = (size / 2) * 0.3; + const outerRadius = (size / 2) * 0.7 * t + radius; + const context = this.context; + + // draw outer circle + context.clearRect(0, 0, this.width, this.height); + context.beginPath(); + context.arc( + this.width / 2, + this.height / 2, + outerRadius, + 0, + Math.PI * 2 + ); + context.fillStyle = `rgba(255, 200, 200,${1 - t})`; + context.fill(); + + // draw inner circle + context.beginPath(); + context.arc( + this.width / 2, + this.height / 2, + radius, + 0, + Math.PI * 2 + ); + context.fillStyle = 'rgba(255, 100, 100, 1)'; + context.strokeStyle = 'white'; + context.lineWidth = 2 + 4 * (1 - t); + context.fill(); + context.stroke(); + + // update this image's data with data from the canvas + this.data = context.getImageData( + 0, + 0, + this.width, + this.height + ).data; + + // continuously repaint the map, resulting in the smooth animation of the dot + map.triggerRepaint(); + + // return `true` to let the map know that the image was updated + return true; + } + }; + + map.on('load', () => { + map.addImage('pulsing-dot', pulsingDot, {pixelRatio: 2}); + + map.addSource('points', { + 'type': 'geojson', + 'data': { + 'type': 'FeatureCollection', + 'features': [ + { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [0, 0] + } + } + ] + } + }); + map.addLayer({ + 'id': 'points', + 'type': 'symbol', + 'source': 'points', + 'layout': { + 'icon-image': 'pulsing-dot' + } + }); + }); + {% if search_radius %} map.on('load', () => { const radius = {{ search_radius }}; // kilometer diff --git a/src/fellchensammlung/views.py b/src/fellchensammlung/views.py index 1b18c2f..64e99fd 100644 --- a/src/fellchensammlung/views.py +++ b/src/fellchensammlung/views.py @@ -204,6 +204,7 @@ def search(request): "searched": searched, "adoption_notices_map": AdoptionNotice.get_active_ANs(), "map_center": search.position, + "map_pins": [search], "location": search.location, "search_radius": search.max_distance, "zoom_level": zoom_level_for_radius(search.max_distance)}