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="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>
<FilterButton @input="showControls = !showControls"
:value="showControls" />
@ -35,6 +42,7 @@ import FilterForm from './filter/Form.vue';
import GPSPoint from '../models/GPSPoint';
import LocationQuery from '../models/LocationQuery';
import MapView from '../mixins/MapView.vue';
import Paginate from '../mixins/Paginate.vue';
import Points from '../mixins/Points.vue';
import Routes from '../mixins/Routes.vue';
import URLQueryHandler from '../mixins/URLQueryHandler.vue';
@ -45,6 +53,7 @@ export default {
mixins: [
Api,
MapView,
Paginate,
Points,
Routes,
URLQueryHandler,
@ -58,13 +67,14 @@ export default {
data() {
return {
gpsPoints: [] as GPSPoint[],
loading: false,
locationQuery: new LocationQuery({}),
map: null as Nullable<Map>,
mappedPoints: [] as Point[],
mapView: null as Nullable<View>,
pointsLayer: null as Nullable<VectorLayer>,
popup: null as Nullable<Overlay>,
queryInitialized: false,
routesLayer: null as Nullable<VectorLayer>,
selectedPoint: null as Nullable<GPSPoint>,
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 {
const points = gpsPoints.map((gps: GPSPoint) => new Point([gps.longitude, gps.latitude]))
this.pointsLayer = this.createPointsLayer(points)
this.routesLayer = this.createRoutesLayer(points)
this.mapView = this.createMapView(gpsPoints)
this.mappedPoints = this.toMappedPoints(gpsPoints)
this.pointsLayer = this.createPointsLayer(this.mappedPoints)
this.routesLayer = this.createRoutesLayer(this.mappedPoints)
this.mapView = this.mapView || this.createMapView(gpsPoints)
const map = new Map({
target: 'map',
layers: [
@ -142,13 +170,56 @@ export default {
watch: {
locationQuery: {
async handler() {
this.setQuery(this.locationQuery)
this.gpsPoints = this.groupPoints(await this.fetch())
const points = this.gpsPoints.map((gps: GPSPoint) => new Point([gps.longitude, gps.latitude]))
this.refreshMapView(this.mapView, this.gpsPoints)
this.refreshPointsLayer(this.pointsLayer, points)
this.refreshRoutesLayer(this.routesLayer, points)
async handler(newQuery, oldQuery) {
const isFirstQuery = !this.queryInitialized
// If startDate/endDate have changed, reset minId/maxId
if (!isFirstQuery &&
(newQuery.startDate !== oldQuery.startDate || newQuery.endDate !== oldQuery.endDate)
) {
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,
},
@ -156,7 +227,7 @@ export default {
async mounted() {
this.initQuery()
this.gpsPoints = this.groupPoints(await this.fetch())
this.gpsPoints = await this.fetch()
this.map = this.createMap(this.gpsPoints)
},
}

View file

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

View file

@ -26,8 +26,13 @@ export default {
return (await response.json())
.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()
},
toMappedPoints(gpsPoints: GPSPoint[]): Point[] {
return this.groupPoints(gpsPoints).map(
(gps: GPSPoint) => new Point([gps.longitude, gps.latitude])
)
},
getCenterAndZoom(points: GPSPoint[]) {
if (!points?.length) {
return {

View file

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

View file

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

View file

@ -25,7 +25,8 @@ class LocationRequest {
this.country = req.country;
this.locality = req.locality;
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 {
@ -93,7 +94,7 @@ class LocationRequest {
}
queryMap.where = where;
queryMap.order = [[colMapping[this.orderBy], this.order]];
queryMap.order = [[colMapping[this.orderBy], this.order.toUpperCase()]];
return queryMap;
}
}