Support for paginated results.
This commit is contained in:
parent
62051f06a7
commit
78fbf45bd6
8 changed files with 200 additions and 37 deletions
frontend/src
src/models
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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" /> 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>
|
||||
|
|
|
@ -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())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
53
frontend/src/mixins/Paginate.vue
Normal file
53
frontend/src/mixins/Paginate.vue
Normal 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>
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue