From 21f663a090fe59305b5d93f64688d2d83fc42e87 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 25 May 2024 22:14:55 +0200 Subject: [PATCH] Add a heatmap layer to the map to show the density of points --- .app_version | 2 +- CHANGELOG.md | 7 +++++- README.md | 6 ++++- app/javascript/controllers/maps_controller.js | 7 +++++- config/importmap.rb | 25 +++++++++++-------- vendor/javascript/leaflet.heat.js | 2 ++ 6 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 vendor/javascript/leaflet.heat.js diff --git a/.app_version b/.app_version index 1d0ba9ea..267577d4 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 786c6760..6a475e76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.1] — 2024-05-25 + +### Added + +- Heatmap layer on the map to show the density of points + ## [0.4.0] — 2024-05-25 **BREAKING CHANGES**: @@ -34,7 +40,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). --- - ## [0.3.2] — 2024-05-23 ### Added diff --git a/README.md b/README.md index a2919f7a..cecd7d3f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,11 @@ You can track your location using the Owntracks or Overland app. ### Location history -You can view your location history on a map. +You can view your location history on a map. On the map you can enable/disable the following layers: + +- Heatmap +- Points +- Lines between points ### Statistics diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 9c50f076..d584b354 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -1,5 +1,6 @@ import { Controller } from "@hotwired/stimulus" import L, { circleMarker } from "leaflet" +import "leaflet.heat" // Connects to data-controller="maps" export default class extends Controller { @@ -17,17 +18,21 @@ export default class extends Controller { var markersArray = this.markersArray(markers) var markersLayer = L.layerGroup(markersArray) + var hearmapMarkers = markers.map(element => [element[0], element[1], 1]); var polylineCoordinates = markers.map(element => element.slice(0, 2)); var polylineLayer = L.polyline(polylineCoordinates, { color: 'blue', opacity: 0.6, weight: 3 }) + var heatmapLayer = L.heatLayer(hearmapMarkers, {radius: 25}).addTo(map); var controlsLayer = { "Points": markersLayer, - "Polyline": polylineLayer + "Polyline": polylineLayer, + "Heatmap": heatmapLayer } var layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(map); + this.addTileLayer(map); // markersLayer.addTo(map); polylineLayer.addTo(map); diff --git a/config/importmap.rb b/config/importmap.rb index 893287ac..d2e3184c 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -1,15 +1,18 @@ +# frozen_string_literal: true + # Pin npm packages by running ./bin/importmap -pin_all_from "app/javascript/channels", under: "channels" +pin_all_from 'app/javascript/channels', under: 'channels' -pin "application", preload: true -pin "@rails/actioncable", to: "actioncable.esm.js" -pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true -pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true -pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true -pin_all_from "app/javascript/controllers", under: "controllers" +pin 'application', preload: true +pin '@rails/actioncable', to: 'actioncable.esm.js' +pin '@hotwired/turbo-rails', to: 'turbo.min.js', preload: true +pin '@hotwired/stimulus', to: 'stimulus.min.js', preload: true +pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js', preload: true +pin_all_from 'app/javascript/controllers', under: 'controllers' -pin "leaflet" # @1.9.4 -pin "leaflet-providers" # @2.0.0 -pin "chartkick", to: "chartkick.js" -pin "Chart.bundle", to: "Chart.bundle.js" +pin 'leaflet' # @1.9.4 +pin 'leaflet-providers' # @2.0.0 +pin 'chartkick', to: 'chartkick.js' +pin 'Chart.bundle', to: 'Chart.bundle.js' +pin 'leaflet.heat' # @0.2.0 diff --git a/vendor/javascript/leaflet.heat.js b/vendor/javascript/leaflet.heat.js new file mode 100644 index 00000000..32843f4c --- /dev/null +++ b/vendor/javascript/leaflet.heat.js @@ -0,0 +1,2 @@ +var i="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:global;!function(){function t(a){return(this||i)instanceof t?((this||i)._canvas=a="string"==typeof a?document.getElementById(a):a,(this||i)._ctx=a.getContext("2d"),(this||i)._width=a.width,(this||i)._height=a.height,(this||i)._max=1,void this.clear()):new t(a)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(a,e){return(this||i)._data=a,this||i},max:function(a){return(this||i)._max=a,this||i},add:function(a){return(this||i)._data.push(a),this||i},clear:function(){return(this||i)._data=[],this||i},radius:function(a,e){e=e||15;var s=(this||i)._circle=document.createElement("canvas"),n=s.getContext("2d"),h=(this||i)._r=a+e;return s.width=s.height=2*h,n.shadowOffsetX=n.shadowOffsetY=200,n.shadowBlur=e,n.shadowColor="black",n.beginPath(),n.arc(h-200,h-200,a,0,2*Math.PI,!0),n.closePath(),n.fill(),this||i},gradient:function(a){var e=document.createElement("canvas"),s=e.getContext("2d"),n=s.createLinearGradient(0,0,0,256);e.width=1,e.height=256;for(var h in a)n.addColorStop(h,a[h]);return s.fillStyle=n,s.fillRect(0,0,1,256),(this||i)._grad=s.getImageData(0,0,1,256).data,this||i},draw:function(a){(this||i)._circle||this.radius((this||i).defaultRadius),(this||i)._grad||this.gradient((this||i).defaultGradient);var e=(this||i)._ctx;e.clearRect(0,0,(this||i)._width,(this||i)._height);for(var s,n=0,h=(this||i)._data.length;h>n;n++)s=(this||i)._data[n],e.globalAlpha=Math.max(s[2]/(this||i)._max,a||.05),e.drawImage((this||i)._circle,s[0]-(this||i)._r,s[1]-(this||i)._r);var o=e.getImageData(0,0,(this||i)._width,(this||i)._height);return this._colorize(o.data,(this||i)._grad),e.putImageData(o,0,0),this||i},_colorize:function(i,a){for(var e,s=3,n=i.length;n>s;s+=4)e=4*i[s],e&&(i[s-3]=a[e],i[s-2]=a[e+1],i[s-1]=a[e+2])}},window.simpleheat=t}(),L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(a,e){(this||i)._latlngs=a,L.setOptions(this||i,e)},setLatLngs:function(a){return(this||i)._latlngs=a,this.redraw()},addLatLng:function(a){return(this||i)._latlngs.push(a),this.redraw()},setOptions:function(a){return L.setOptions(this||i,a),(this||i)._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!(this||i)._heat||(this||i)._frame||(this||i)._map._animating||((this||i)._frame=L.Util.requestAnimFrame((this||i)._redraw,this||i)),this||i},onAdd:function(a){(this||i)._map=a,(this||i)._canvas||this._initCanvas(),a._panes.overlayPane.appendChild((this||i)._canvas),a.on("moveend",(this||i)._reset,this||i),a.options.zoomAnimation&&L.Browser.any3d&&a.on("zoomanim",(this||i)._animateZoom,this||i),this._reset()},onRemove:function(a){a.getPanes().overlayPane.removeChild((this||i)._canvas),a.off("moveend",(this||i)._reset,this||i),a.options.zoomAnimation&&a.off("zoomanim",(this||i)._animateZoom,this||i)},addTo:function(a){return a.addLayer(this||i),this||i},_initCanvas:function(){var a=(this||i)._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),e=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);a.style[e]="50% 50%";var s=(this||i)._map.getSize();a.width=s.x,a.height=s.y;var n=(this||i)._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(a,"leaflet-zoom-"+(n?"animated":"hide")),(this||i)._heat=simpleheat(a),this._updateOptions()},_updateOptions:function(){(this||i)._heat.radius((this||i).options.radius||(this||i)._heat.defaultRadius,(this||i).options.blur),(this||i).options.gradient&&(this||i)._heat.gradient((this||i).options.gradient),(this||i).options.max&&(this||i)._heat.max((this||i).options.max)},_reset:function(){var a=(this||i)._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition((this||i)._canvas,a);var e=(this||i)._map.getSize();(this||i)._heat._width!==e.x&&((this||i)._canvas.width=(this||i)._heat._width=e.x),(this||i)._heat._height!==e.y&&((this||i)._canvas.height=(this||i)._heat._height=e.y),this._redraw()},_redraw:function(){var a,e,s,n,h,o,r,l,d,_=[],m=(this||i)._heat._r,u=(this||i)._map.getSize(),f=new L.Bounds(L.point([-m,-m]),u.add([m,m])),c=void 0===(this||i).options.max?1:(this||i).options.max,g=void 0===(this||i).options.maxZoom?(this||i)._map.getMaxZoom():(this||i).options.maxZoom,p=1/Math.pow(2,Math.max(0,Math.min(g-(this||i)._map.getZoom(),12))),v=m/2,w=[],y=(this||i)._map._getMapPanePos(),x=y.x%v,P=y.y%v;for(a=0,e=(this||i)._latlngs.length;e>a;a++)if(s=(this||i)._map.latLngToContainerPoint((this||i)._latlngs[a]),f.contains(s)){h=Math.floor((s.x-x)/v)+2,o=Math.floor((s.y-P)/v)+2;var M=void 0!==(this||i)._latlngs[a].alt?(this||i)._latlngs[a].alt:void 0!==(this||i)._latlngs[a][2]?+(this||i)._latlngs[a][2]:1;d=M*p,w[o]=w[o]||[],n=w[o][h],n?(n[0]=(n[0]*n[2]+s.x*d)/(n[2]+d),n[1]=(n[1]*n[2]+s.y*d)/(n[2]+d),n[2]+=d):w[o][h]=[s.x,s.y,d]}for(a=0,e=w.length;e>a;a++)if(w[a])for(r=0,l=w[a].length;l>r;r++)n=w[a][r],n&&_.push([Math.round(n[0]),Math.round(n[1]),Math.min(n[2],c)]);(this||i)._heat.data(_).draw((this||i).options.minOpacity),(this||i)._frame=null},_animateZoom:function(a){var e=(this||i)._map.getZoomScale(a.zoom),s=(this||i)._map._getCenterOffset(a.center)._multiplyBy(-e).subtract((this||i)._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform((this||i)._canvas,s,e):(this||i)._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(s)+" scale("+e+")"}}),L.heatLayer=function(i,a){return new L.HeatLayer(i,a)};var a={};export default a; +