Skip to content

Commit

Permalink
Merge pull request #970 from nextcloud/devices-heatmap
Browse files Browse the repository at this point in the history
Devices heatmap
  • Loading branch information
tacruc authored Feb 13, 2023
2 parents 24a2729 + 901bc2e commit e5075ae
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 20 deletions.
4 changes: 2 additions & 2 deletions lib/Controller/DevicesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ public function getDevices(): DataResponse {
* @param int $pruneBefore
* @return DataResponse
*/
public function getDevicePoints($id, int $pruneBefore=0): DataResponse {
$points = $this->devicesService->getDevicePointsFromDB($this->userId, $id, $pruneBefore);
public function getDevicePoints($id, ?int $pruneBefore=0, ?int $limit=10000, ?int $offset=0): DataResponse {
$points = $this->devicesService->getDevicePointsFromDB($this->userId, $id, $pruneBefore, $limit, $offset);
return new DataResponse($points);
}

Expand Down
21 changes: 18 additions & 3 deletions lib/Service/DevicesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,16 @@ public function getDevicesFromDB($userId) {
return $devices;
}

public function getDevicePointsFromDB($userId, $deviceId, $pruneBefore=0) {
/**
* @param $userId
* @param $deviceId
* @param int|null $pruneBefore
* @param int|null $limit
* @param int|null $offset
* @return array
* @throws \OCP\DB\Exception
*/
public function getDevicePointsFromDB($userId, $deviceId, ?int $pruneBefore=0, ?int $limit=null, ?int $offset=null) {
$qb = $this->qb;
// get coordinates
$qb->select('p.id', 'lat', 'lng', 'timestamp', 'altitude', 'accuracy', 'battery')
Expand All @@ -92,7 +101,13 @@ public function getDevicePointsFromDB($userId, $deviceId, $pruneBefore=0) {
$qb->expr()->gt('timestamp', $qb->createNamedParameter(intval($pruneBefore), IQueryBuilder::PARAM_INT))
);
}
$qb->orderBy('timestamp', 'ASC');
if (!is_null($offset)) {
$qb->setFirstResult($offset);
}
if (!is_null($limit)) {
$qb->setMaxResults($limit);
}
$qb->orderBy('timestamp', 'DESC');
$req = $qb->execute();

$points = [];
Expand All @@ -110,7 +125,7 @@ public function getDevicePointsFromDB($userId, $deviceId, $pruneBefore=0) {
$req->closeCursor();
$qb = $qb->resetQueryParts();

return $points;
return array_reverse($points);
}

public function getOrCreateDeviceFromDB($userId, $userAgent) {
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"leaflet-mouse-position": "^1.0.4",
"leaflet-routing-machine": "^3.2.12",
"leaflet.featuregroup.subgroup": "^1.0.2",
"leaflet.heat": "^0.2.0",
"leaflet.locatecontrol": "^0.79.0",
"leaflet.markercluster": "^1.5.3",
"lrm-graphhopper": "^1.3.0",
Expand Down
6 changes: 3 additions & 3 deletions src/components/map/DeviceLayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
@mouseover="deviceLastPointMouseover" />
<LFeatureGroup
@mouseover="deviceLineMouseover">
<LPolyline v-if="device.historyEnabled"
<LPolyline v-if="device.historyEnabled && points.length <= 2500"
color="black"
:opacity="1"
:weight="4 * 1.6"
:lat-lngs="points" />
<LPolyline v-if="device.historyEnabled"
<LPolyline v-if="device.historyEnabled && points.length <= 2500"
:color="color"
:opacity="1"
:weight="4"
Expand All @@ -64,7 +64,7 @@ import { LMarker, LTooltip, LPopup, LFeatureGroup, LPolyline } from 'vue2-leafle
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton'
import { isComputer } from '../../utils'
import {binSearch, isPublic} from '../../utils/common'
import { binSearch, isPublic } from '../../utils/common'
import optionsController from '../../optionsController'
import moment from '@nextcloud/moment'
Expand Down
60 changes: 55 additions & 5 deletions src/components/map/DevicesLayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<DeviceHoverMarker
v-if="hoverPoint"
:point="hoverPoint" />
<LHeatMap v-if="points.length >= 2500"
ref="devicesHeatMap"
:initial-points="points"
:options="optionsHeatMap" />
</LFeatureGroup>
</template>

Expand All @@ -25,11 +29,14 @@ import DeviceHoverMarker from './DeviceHoverMarker'
import optionsController from '../../optionsController'
import moment from '@nextcloud/moment'
import { binSearch } from '../../utils/common'
import LHeatMap from './LHeatMap'
export default {
name: 'DevicesLayer',
components: {
LFeatureGroup,
LHeatMap,
DeviceLayer,
DeviceHoverMarker,
},
Expand Down Expand Up @@ -57,6 +64,13 @@ export default {
data() {
return {
optionsHeatMap: {
// minOpacity: null,
// maxZoom: null,
radius: 15,
blur: 10,
gradient: { 0.4: 'blue', 0.65: 'lime', 1: 'red' },
},
optionValues: optionsController.optionValues,
hoverPoint: null,
}
Expand All @@ -66,14 +80,50 @@ export default {
displayedDevices() {
return this.devices.filter(d => d.enabled)
},
enabledDevices() {
return this.devices.map(d => d.enabled)
},
displayedDevicesHistories() {
return this.devices.map(d => d.enabled && d.historyEnabled)
},
points() {
return this.devices.reduce((points, device) => {
if (device.enabled && device.historyEnabled) {
const lastNullIndex = binSearch(device.points, (p) => !p.timestamp)
const firstShownIndex = binSearch(device.points, (p) => (p.timestamp || 0) < this.start) + 1
const lastShownIndex = binSearch(device.points, (p) => (p.timestamp || 0) < this.end)
if (lastNullIndex + 1 + lastShownIndex - firstShownIndex + 1 > 2500) {
const filteredDevicePoints = [
...device.points.slice(0, lastNullIndex + 1),
...device.points.slice(firstShownIndex, lastShownIndex + 1),
]
const deviceLatLngs = filteredDevicePoints.map((p) => [p.lat, p.lng])
points = points.concat(deviceLatLngs)
}
}
return points
}, [])
},
},
watch: {
devices: {
handler() {
this.hoverPoint = null
},
deep: true,
enabledDevices() {
this.hoverPoint = null
},
displayedDevicesHistories() {
if (this.$refs.devicesHeatMap) {
this.$refs.devicesHeatMap.setLatLngs(this.points)
}
},
start() {
if (this.$refs.devicesHeatMap) {
this.$refs.devicesHeatMap.setLatLngs(this.points)
}
},
end() {
if (this.$refs.devicesHeatMap) {
this.$refs.devicesHeatMap.setLatLngs(this.points)
}
},
},
Expand Down
84 changes: 84 additions & 0 deletions src/components/map/LHeatMap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@

<template>
<div style="display: none;">
<slot v-if="ready" />
</div>
</template>

<script>
import 'leaflet.heat/dist/leaflet-heat.js'
import { findRealParent, propsBinder } from 'vue2-leaflet'
import { DomEvent } from 'leaflet'
const props = {
initialPoints: {
type: Array,
required: false,
default() { return [] },
},
options: {
type: Object,
default() { return {} },
},
}
export default {
props,
data() {
return {
points: null,
ready: false,
}
},
watch: {
options: {
handler(newOptions) {
this.mapObject.setOptions(newOptions)
},
deep: true,
},
points: {
handler(newPoints) {
this.mapObject.setLatLngs(newPoints)
},
deep: true,
},
},
mounted() {
this.points = this.initialPoints
this.mapObject = L.heatLayer(this.points, this.options)
DomEvent.on(this.mapObject, this.$listeners)
propsBinder(this, this.mapObject, props)
this.ready = true
this.parentContainer = findRealParent(this.$parent)
this.parentContainer.addLayer(this)
this.$nextTick(() => {
this.$emit('ready', this.mapObject)
})
},
beforeDestroy() {
this.parentContainer.removeLayer(this)
},
methods: {
addLayer(layer, alreadyAdded) {
if (!alreadyAdded) {
this.mapObject.addLayer(layer.mapObject)
}
},
removeLayer(layer, alreadyRemoved) {
if (!alreadyRemoved) {
this.mapObject.removeLayer(layer.mapObject)
}
},
addLatLng(latlng) {
this.mapObject.addLatLng(latlng)
},
setLatLngs(latlngs) {
this.mapObject.setLatLngs(latlngs)
},
redraw() {
this.mapObject.redraw()
},
},
}
</script>
4 changes: 3 additions & 1 deletion src/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,12 @@ export function getDevices(myMapId = null) {
return axios.get(url, conf)
}

export function getDevice(id, myMapId = null) {
export async function getDevice(id, myMapId = null, limit= null, offset = null) {
const conf = {
params: {
myMapId,
limit,
offset,
},
}
const url = generateUrl('/apps/maps/devices/' + id)
Expand Down
32 changes: 26 additions & 6 deletions src/views/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ import AppNavigationDevicesItem from '../components/AppNavigationDevicesItem'
import AppNavigationMyMapsItem from '../components/AppNavigationMyMapsItem'
import optionsController from '../optionsController'
import { getLetterColor, hslToRgb, Timer, getDeviceInfoFromUserAgent2, isComputer, isPhone } from '../utils'
import {binSearch, getToken, isPublic} from '../utils/common'
import { binSearch, getToken, isPublic } from '../utils/common'
import { poiSearchData } from '../utils/poiData'
import { processGpx } from '../tracksUtils'

Expand Down Expand Up @@ -342,6 +342,7 @@ export default {
this.minPhotoTimestamp,
this.minFavoriteTimestamp,
this.minTrackTimestamp,
this.minDevicesTimestamp,
) || 0
},
maxDataTimestamp() {
Expand Down Expand Up @@ -409,6 +410,15 @@ export default {
? this.trackDates[0] + 100
: moment().unix() + 100
},
minDevicesTimestamp() {
return this.devices.reduce((min, device) => {
if (device.enabled && device.historyEnabled) {
const lastNullIndex = binSearch(device.points, (p) => !p.timestamp)
min = Math.min(min, device.points[lastNullIndex + 1].timestamp)
}
return min
}, moment().unix() + 100)
},
// displayed data
displayedTracks() {
return this.sliderEnabled
Expand Down Expand Up @@ -1965,7 +1975,7 @@ export default {
...device,
loading: false,
enabled: false,
historyEnabled: optionsController.enabledDeviceLines.includes(device.id),
historyEnabled: false, // optionsController.enabledDeviceLines.includes(device.id),
}
})
this.devices.forEach((device) => {
Expand All @@ -1983,7 +1993,7 @@ export default {
if (device.enabled) {
this.disableDevice(device)
} else {
this.enableDevice(device, true)
this.enableDevice(device, false)
}
},
onSearchEnableDevice(device) {
Expand All @@ -2002,12 +2012,22 @@ export default {
},
disableDevice(device) {
device.enabled = false
device.historyEnabled = false
this.saveEnabledDevices()
},
getDevice(device, enable = false, save = true, zoom = false) {
device.loading = true
network.getDevice(device.id, this.myMapId).then((response) => {
this.$set(device, 'points', response.data.sort((p1, p2) => (p1.timestamp || 0) - (p2.timestamp || 0)))
network.getDevice(device.id, this.myMapId, 100000, device.points?.length || 0).then((response) => {
//There are too many points making it responsiv crashes most browsers
// this.$set(device, 'points', response.data /* .sort((p1, p2) => (p1.timestamp || 0) - (p2.timestamp || 0)) */)
if (device.points) {
device.points = response.data.concat(device.points)
} else {
device.points = response.data
}
if (response.data.length >= 100000) {
this.getDevice(device, false, false, false)
}
if (enable) {
device.enabled = true
}
Expand Down Expand Up @@ -2115,7 +2135,7 @@ export default {
// Fixme
showInfo('Adding device to map not supported yet')
},
onToggleDeviceHistory(device) {
async onToggleDeviceHistory(device) {
device.historyEnabled = !device.historyEnabled
this.saveEnabledDeviceLines()
},
Expand Down

0 comments on commit e5075ae

Please sign in to comment.