Support for paginated results.

This commit is contained in:
Fabio Manganiello 2025-02-24 00:28:59 +01:00
parent 62051f06a7
commit 78fbf45bd6
8 changed files with 200 additions and 37 deletions

View file

@ -9,7 +9,14 @@
<div class="controls"> <div class="controls">
<div class="form-container" v-if="showControls"> <div class="form-container" v-if="showControls">
<FilterForm :value="locationQuery" @refresh="locationQuery = $event" /> <FilterForm :value="locationQuery"
:disabled="loading"
:has-next-page="hasNextPage"
:has-prev-page="hasPrevPage"
@refresh="locationQuery = $event"
@reset-page="locationQuery.minId = locationQuery.maxId = undefined"
@next-page="fetchNextPage"
@prev-page="fetchPrevPage" />
</div> </div>
<FilterButton @input="showControls = !showControls" <FilterButton @input="showControls = !showControls"
:value="showControls" /> :value="showControls" />
@ -35,6 +42,7 @@ import FilterForm from './filter/Form.vue';
import GPSPoint from '../models/GPSPoint'; import GPSPoint from '../models/GPSPoint';
import LocationQuery from '../models/LocationQuery'; import LocationQuery from '../models/LocationQuery';
import MapView from '../mixins/MapView.vue'; import MapView from '../mixins/MapView.vue';
import Paginate from '../mixins/Paginate.vue';
import Points from '../mixins/Points.vue'; import Points from '../mixins/Points.vue';
import Routes from '../mixins/Routes.vue'; import Routes from '../mixins/Routes.vue';
import URLQueryHandler from '../mixins/URLQueryHandler.vue'; import URLQueryHandler from '../mixins/URLQueryHandler.vue';
@ -45,6 +53,7 @@ export default {
mixins: [ mixins: [
Api, Api,
MapView, MapView,
Paginate,
Points, Points,
Routes, Routes,
URLQueryHandler, URLQueryHandler,
@ -58,13 +67,14 @@ export default {
data() { data() {
return { return {
gpsPoints: [] as GPSPoint[],
loading: false, loading: false,
locationQuery: new LocationQuery({}), locationQuery: new LocationQuery({}),
map: null as Nullable<Map>, map: null as Nullable<Map>,
mappedPoints: [] as Point[],
mapView: null as Nullable<View>, mapView: null as Nullable<View>,
pointsLayer: null as Nullable<VectorLayer>, pointsLayer: null as Nullable<VectorLayer>,
popup: null as Nullable<Overlay>, popup: null as Nullable<Overlay>,
queryInitialized: false,
routesLayer: null as Nullable<VectorLayer>, routesLayer: null as Nullable<VectorLayer>,
selectedPoint: null as Nullable<GPSPoint>, selectedPoint: null as Nullable<GPSPoint>,
showControls: false, showControls: false,
@ -84,11 +94,29 @@ export default {
} }
}, },
fetchNextPage() {
const nextPageQuery = this.nextPageQuery()
if (!nextPageQuery) {
return
}
this.locationQuery = nextPageQuery
},
fetchPrevPage() {
const prevPageQuery = this.prevPageQuery()
if (!prevPageQuery) {
return
}
this.locationQuery = prevPageQuery
},
createMap(gpsPoints: GPSPoint[]): Map { createMap(gpsPoints: GPSPoint[]): Map {
const points = gpsPoints.map((gps: GPSPoint) => new Point([gps.longitude, gps.latitude])) this.mappedPoints = this.toMappedPoints(gpsPoints)
this.pointsLayer = this.createPointsLayer(points) this.pointsLayer = this.createPointsLayer(this.mappedPoints)
this.routesLayer = this.createRoutesLayer(points) this.routesLayer = this.createRoutesLayer(this.mappedPoints)
this.mapView = this.createMapView(gpsPoints) this.mapView = this.mapView || this.createMapView(gpsPoints)
const map = new Map({ const map = new Map({
target: 'map', target: 'map',
layers: [ layers: [
@ -142,13 +170,56 @@ export default {
watch: { watch: {
locationQuery: { locationQuery: {
async handler() { async handler(newQuery, oldQuery) {
this.setQuery(this.locationQuery) const isFirstQuery = !this.queryInitialized
this.gpsPoints = this.groupPoints(await this.fetch())
const points = this.gpsPoints.map((gps: GPSPoint) => new Point([gps.longitude, gps.latitude])) // If startDate/endDate have changed, reset minId/maxId
this.refreshMapView(this.mapView, this.gpsPoints) if (!isFirstQuery &&
this.refreshPointsLayer(this.pointsLayer, points) (newQuery.startDate !== oldQuery.startDate || newQuery.endDate !== oldQuery.endDate)
this.refreshRoutesLayer(this.routesLayer, points) ) {
newQuery.minId = undefined
newQuery.maxId = undefined
this.hasNextPage = true
this.hasPrevPage = true
}
// Results with maxId should be retrieved in descending order,
// otherwise all results should be retrieved in ascending order
newQuery.order = newQuery.maxId ? 'desc' : 'asc'
this.setQuery(newQuery)
this.queryInitialized = true
if (!isFirstQuery) {
const gpsPoints = await this.fetch()
// If there are no points, and minId/maxId are set, reset them
// and don't update the map (it means that we have reached the
// start/end of the current window)
if (gpsPoints.length < 2 && (newQuery.minId || newQuery.maxId)) {
if (newQuery.minId) {
this.hasNextPage = false
}
if (newQuery.maxId) {
this.hasPrevPage = false
}
newQuery.minId = oldQuery.minId
newQuery.maxId = oldQuery.maxId
return
}
this.gpsPoints = gpsPoints
this.hasNextPage = gpsPoints.length > 1
this.hasPrevPage = gpsPoints.length > 1
}
this.mappedPoints = this.toMappedPoints(this.gpsPoints)
if (this.mapView) {
this.refreshMapView(this.mapView, this.gpsPoints)
this.refreshPointsLayer(this.pointsLayer, this.mappedPoints)
this.refreshRoutesLayer(this.routesLayer, this.mappedPoints)
}
}, },
deep: true, deep: true,
}, },
@ -156,7 +227,7 @@ export default {
async mounted() { async mounted() {
this.initQuery() this.initQuery()
this.gpsPoints = this.groupPoints(await this.fetch()) this.gpsPoints = await this.fetch()
this.map = this.createMap(this.gpsPoints) this.map = this.createMap(this.gpsPoints)
}, },
} }

View file

@ -11,30 +11,31 @@
@input="newFilter.startDate = startPlusHours($event.target.value, 0)" @input="newFilter.startDate = startPlusHours($event.target.value, 0)"
@change="newFilter.startDate = startPlusHours($event.target.value, 0)" @change="newFilter.startDate = startPlusHours($event.target.value, 0)"
:value="toLocalString(newFilter.startDate)" :value="toLocalString(newFilter.startDate)"
:disabled="disabled"
:max="maxDate" /> :max="maxDate" />
<div class="footer"> <div class="footer">
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusDays(newFilter.startDate, -7)" @click="newFilter.startDate = startPlusDays(newFilter.startDate, -7)"
:disabled="!newFilter.startDate">-1w</button> :disabled="disabled || !newFilter.startDate">-1w</button>
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusDays(newFilter.startDate, -1)" @click="newFilter.startDate = startPlusDays(newFilter.startDate, -1)"
:disabled="!newFilter.startDate">-1d</button> :disabled="disabled || !newFilter.startDate">-1d</button>
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusHours(newFilter.startDate, -1)" @click="newFilter.startDate = startPlusHours(newFilter.startDate, -1)"
:disabled="!newFilter.startDate">-1h</button> :disabled="disabled || !newFilter.startDate">-1h</button>
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusDays(new Date(), 0)" @click="newFilter.startDate = startPlusDays(new Date(), 0)"
:disabled="!newFilter.startDate">Now</button> :disabled="disabled || !newFilter.startDate">Now</button>
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusHours(newFilter.startDate, 1)" @click="newFilter.startDate = startPlusHours(newFilter.startDate, 1)"
:disabled="!newFilter.startDate">+1h</button> :disabled="disabled || !newFilter.startDate">+1h</button>
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusDays(newFilter.startDate, 1)" @click="newFilter.startDate = startPlusDays(newFilter.startDate, 1)"
:disabled="!newFilter.startDate">+1d</button> :disabled="disabled || !newFilter.startDate">+1d</button>
<button type="button" <button type="button"
@click="newFilter.startDate = startPlusDays(newFilter.startDate, 7)" @click="newFilter.startDate = startPlusDays(newFilter.startDate, 7)"
:disabled="!newFilter.startDate">+1w</button> :disabled="disabled || !newFilter.startDate">+1w</button>
</div> </div>
</div> </div>
@ -46,40 +47,50 @@
@input="newFilter.endDate = endPlusHours($event.target.value, 0)" @input="newFilter.endDate = endPlusHours($event.target.value, 0)"
@change="newFilter.endDate = endPlusHours($event.target.value, 0)" @change="newFilter.endDate = endPlusHours($event.target.value, 0)"
:value="toLocalString(newFilter.endDate)" :value="toLocalString(newFilter.endDate)"
:disabled="disabled"
:max="maxDate" /> :max="maxDate" />
<div class="footer"> <div class="footer">
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusDays(newFilter.endDate, -7)" @click="newFilter.endDate = endPlusDays(newFilter.endDate, -7)"
:disabled="!newFilter.endDate">-1w</button> :disabled="disabled || !newFilter.endDate">-1w</button>
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusDays(newFilter.endDate, -1)" @click="newFilter.endDate = endPlusDays(newFilter.endDate, -1)"
:disabled="!newFilter.endDate">-1d</button> :disabled="disabled || !newFilter.endDate">-1d</button>
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusHours(newFilter.endDate, -1)" @click="newFilter.endDate = endPlusHours(newFilter.endDate, -1)"
:disabled="!newFilter.endDate">-1h</button> :disabled="disabled || !newFilter.endDate">-1h</button>
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusDays(new Date(), 0)" @click="newFilter.endDate = endPlusDays(new Date(), 0)"
:disabled="!newFilter.endDate">Now</button> :disabled="disabled || !newFilter.endDate">Now</button>
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusHours(newFilter.endDate, 1)" @click="newFilter.endDate = endPlusHours(newFilter.endDate, 1)"
:disabled="!newFilter.endDate">+1h</button> :disabled="disabled || !newFilter.endDate">+1h</button>
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusDays(newFilter.endDate, 1)" @click="newFilter.endDate = endPlusDays(newFilter.endDate, 1)"
:disabled="!newFilter.endDate">+1d</button> :disabled="disabled || !newFilter.endDate">+1d</button>
<button type="button" <button type="button"
@click="newFilter.endDate = endPlusDays(newFilter.endDate, 7)" @click="newFilter.endDate = endPlusDays(newFilter.endDate, 7)"
:disabled="!newFilter.endDate">+1w</button> :disabled="disabled || !newFilter.endDate">+1w</button>
</div> </div>
</div> </div>
</div> </div>
<div class="pagination-container"> <div class="pagination-container">
<div class="page-button-container">
<button type="button"
:disabled="disabled"
v-if="value.minId || value.maxId"
@click.stop="$emit('reset-page')">
<font-awesome-icon icon="fas fa-undo" />
</button>
</div>
<div class="page-button-container"> <div class="page-button-container">
<button type="button" <button type="button"
@click="$emit('prev-page')" @click="$emit('prev-page')"
title="Previous Results" title="Previous Results"
:disabled="!hasPrev"> :disabled="disabled || !hasPrevPage">
<font-awesome-icon icon="fas fa-chevron-left" /> <font-awesome-icon icon="fas fa-chevron-left" />
</button> </button>
</div> </div>
@ -91,6 +102,7 @@
@input="newFilter.limit = Number($event.target.value)" @input="newFilter.limit = Number($event.target.value)"
@change="newFilter.limit = Number($event.target.value)" @change="newFilter.limit = Number($event.target.value)"
:value="newFilter.limit" :value="newFilter.limit"
:disabled="disabled"
min="1" /> min="1" />
</div> </div>
@ -98,14 +110,16 @@
<button type="button" <button type="button"
@click="$emit('next-page')" @click="$emit('next-page')"
title="Next Results" title="Next Results"
:disabled="!hasNext"> :disabled="disabled || !hasNextPage">
<font-awesome-icon icon="fas fa-chevron-right" /> <font-awesome-icon icon="fas fa-chevron-right" />
</button> </button>
</div> </div>
</div> </div>
<div class="footer"> <div class="footer">
<button type="submit" :disabled="!changed">Apply</button> <button type="submit" :disabled="disabled || !changed">
<font-awesome-icon icon="fas fa-check" />&nbsp;Apply
</button>
</div> </div>
</form> </form>
</template> </template>
@ -117,15 +131,20 @@ export default {
emit: [ emit: [
'next-page', 'next-page',
'prev-page', 'prev-page',
'reset-page',
'refresh', 'refresh',
], ],
props: { props: {
value: Object, value: Object,
hasPrev: { disabled: {
type: Boolean,
default: false,
},
hasPrevPage: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
hasNext: { hasNextPage: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
@ -333,5 +352,9 @@ export default {
width: 100%; width: 100%;
} }
} }
button[type=submit] {
min-width: 10em;
}
} }
</style> </style>

View file

@ -26,8 +26,13 @@ export default {
return (await response.json()) return (await response.json())
.map((gps: any) => { .map((gps: any) => {
return new GPSPoint(gps) return new GPSPoint({
...gps,
// Normalize timestamp to Date object
timestamp: new Date(gps.timestamp),
})
}) })
.sort((a: GPSPoint, b: GPSPoint) => a.timestamp.getTime() - b.timestamp.getTime())
}, },
}, },
} }

View file

@ -0,0 +1,53 @@
<script lang="ts">
import GPSPoint from '../models/GPSPoint';
import LocationQuery from '../models/LocationQuery';
export default {
data() {
return {
gpsPoints: [] as GPSPoint[],
hasNextPage: true,
hasPrevPage: true,
}
},
computed: {
newestPoint(): GPSPoint | undefined {
return this.gpsPoints[this.gpsPoints.length - 1] || undefined
},
oldestPoint(): GPSPoint | undefined {
return this.gpsPoints[0] || undefined
},
},
methods: {
prevPageQuery(): LocationQuery | null {
if (!this.oldestPoint) {
return null
}
return new LocationQuery({
...this.locationQuery,
minId: undefined,
maxId: this.oldestPoint.id,
// Previous page results should be retrieved in descending order
order: 'desc',
})
},
nextPageQuery(): LocationQuery | null {
if (!this.newestPoint) {
return null
}
return new LocationQuery({
...this.locationQuery,
minId: this.newestPoint.id,
maxId: undefined,
order: 'asc',
})
},
},
}
</script>

View file

@ -75,6 +75,12 @@ export default {
source.changed() source.changed()
}, },
toMappedPoints(gpsPoints: GPSPoint[]): Point[] {
return this.groupPoints(gpsPoints).map(
(gps: GPSPoint) => new Point([gps.longitude, gps.latitude])
)
},
getCenterAndZoom(points: GPSPoint[]) { getCenterAndZoom(points: GPSPoint[]) {
if (!points?.length) { if (!points?.length) {
return { return {

View file

@ -1,4 +1,5 @@
class GPSPoint { class GPSPoint {
public id: number;
public latitude: number; public latitude: number;
public longitude: number; public longitude: number;
public altitude: number; public altitude: number;
@ -9,6 +10,7 @@ class GPSPoint {
public timestamp: Date; public timestamp: Date;
constructor(public data: any) { constructor(public data: any) {
this.id = data.id;
this.latitude = data.latitude; this.latitude = data.latitude;
this.longitude = data.longitude; this.longitude = data.longitude;
this.altitude = data.altitude; this.altitude = data.altitude;

View file

@ -1,6 +1,6 @@
class LocationQuery { class LocationQuery {
public limit: number = 250; public limit: number = 250;
public offset: number = 0; public offset: number | null = null;
public startDate: Date | null = null; public startDate: Date | null = null;
public endDate: Date | null = null; public endDate: Date | null = null;
public minId: number | null = null; public minId: number | null = null;
@ -8,6 +8,7 @@ class LocationQuery {
public country: string | null = null; public country: string | null = null;
public locality: string | null = null; public locality: string | null = null;
public postalCode: string | null = null; public postalCode: string | null = null;
public order: string = 'asc';
constructor(public data: any) { constructor(public data: any) {
this.limit = data.limit || this.limit; this.limit = data.limit || this.limit;
@ -19,6 +20,7 @@ class LocationQuery {
this.country = data.country || this.country; this.country = data.country || this.country;
this.locality = data.locality || this.locality; this.locality = data.locality || this.locality;
this.postalCode = data.postalCode || this.postalCode; this.postalCode = data.postalCode || this.postalCode;
this.order = data.order || this.order;
if (!(this.startDate && this.endDate)) { if (!(this.startDate && this.endDate)) {
// Default to the past 24 hours // Default to the past 24 hours

View file

@ -25,7 +25,8 @@ class LocationRequest {
this.country = req.country; this.country = req.country;
this.locality = req.locality; this.locality = req.locality;
this.postalCode = req.postalCode; this.postalCode = req.postalCode;
this.orderBy = req.orderBy || 'timestamp'; this.orderBy = req.orderBy || this.orderBy;
this.order = req.order || this.order;
} }
private initNumber(key: string, req: any): void { private initNumber(key: string, req: any): void {
@ -93,7 +94,7 @@ class LocationRequest {
} }
queryMap.where = where; queryMap.where = where;
queryMap.order = [[colMapping[this.orderBy], this.order]]; queryMap.order = [[colMapping[this.orderBy], this.order.toUpperCase()]];
return queryMap; return queryMap;
} }
} }