sotlas-frontend/src/components/MapFilterControl.vue

353 wiersze
13 KiB
Vue

<template>
<div :class="{ 'mapboxgl-ctrl-group': true, 'mapboxgl-ctrl': true, 'mapbox-gl-filter-container': true }">
<button :class="{ 'mapboxgl-ctrl-icon': true, 'mapbox-gl-filter': true, active: active }" type="button" title="Toggle filter" @click="toggleFilter" />
<div v-if="open" class="filter-container">
<div class="filter-criterion">
<b-field>
<b-checkbox v-model="activationsEnabled" size="is-small">Activations</b-checkbox>
</b-field>
<b-field grouped>
<b-input v-model="activationsFrom" class="count" placeholder="min." size="is-small" :disabled="!activationsEnabled" />
<div class="tlabel">to</div>
<b-input v-model="activationsTo" class="count" placeholder="max." size="is-small" :disabled="!activationsEnabled" />
</b-field>
<b-field grouped>
<b-button class="control" size="is-small" @click="setActivations(0, 0)">Never</b-button>
<b-button class="control" size="is-small" @click="setActivations(0, 3)">Rarely</b-button>
</b-field>
</div>
<div class="filter-criterion">
<b-field>
<b-checkbox v-model="pointsEnabled" size="is-small">Points</b-checkbox>
</b-field>
<b-field grouped>
<b-select v-model="pointsFrom" placeholder="min." size="is-small" :disabled="!pointsEnabled">
<option v-for="point in points" :key="point" :value="point">{{ point }}</option>
</b-select>
<div class="tlabel">to</div>
<b-select v-model="pointsTo" placeholder="max." size="is-small" :disabled="!pointsEnabled">
<option v-for="point in points" :key="point" :value="point">{{ point }}</option>
</b-select>
</b-field>
</div>
<div class="filter-criterion">
<b-field>
<b-checkbox v-model="altitudeEnabled" size="is-small">Altitude</b-checkbox>
</b-field>
<b-field grouped>
<b-input v-model="altitudeFrom" class="altitude" placeholder="min." size="is-small" :disabled="!altitudeEnabled" />
<div class="tlabel">to</div>
<b-input v-model="altitudeTo" class="altitude" placeholder="max." size="is-small" :disabled="!altitudeEnabled" />
<div class="tlabel">{{ $store.state.altitudeUnits }}</div>
</b-field>
</div>
<div class="filter-criterion">
<b-field>
<b-checkbox v-model="activatedByEnabled" size="is-small">Activated by</b-checkbox>
</b-field>
<b-field grouped>
<b-input v-model="activatedBy" class="callsign" placeholder="Callsign" size="is-small" :disabled="!activatedByEnabled" />
<b-button v-if="myCallsign" class="control" size="is-small" @click="activatedBy = myCallsign" :disabled="!activatedByEnabled">Me</b-button>
<b-checkbox v-model="activatedByThisYear" size="is-small">This year</b-checkbox>
</b-field>
</div>
<div class="filter-criterion">
<b-field>
<b-checkbox v-model="notActivatedByEnabled" size="is-small">Not activated by</b-checkbox>
</b-field>
<b-field grouped>
<b-input v-model="notActivatedBy" class="callsign" placeholder="Callsign" size="is-small" :disabled="!notActivatedByEnabled" />
<b-button v-if="myCallsign" class="control" size="is-small" @click="notActivatedBy = myCallsign" :disabled="!notActivatedByEnabled">Me</b-button>
<b-checkbox v-model="notActivatedByThisYear" size="is-small">This year</b-checkbox>
</b-field>
</div>
<div class="filter-criterion">
<b-field>
<b-tooltip label="Only available if logged in" type="is-info" :active="!authenticated"><b-checkbox v-model="completeCandidateEnabled" size="is-small" :disabled="!authenticated">Complete candidate for me</b-checkbox></b-tooltip>
</b-field>
</div>
<div class="action-buttons">
<b-button type="is-link" size="is-small" :loading="filterLoadingCount > 0" @click="updateFilter">Update</b-button>
<b-button type="is-danger" size="is-small" @click="clearFilter">Clear</b-button>
</div>
</div>
</div>
</template>
<script>
import moment from 'moment'
import prefs from '../mixins/prefs.js'
import utils from '../mixins/utils.js'
import api from '../mixins/api.js'
import sotadb from '../mixins/sotadb.js'
export default {
name: 'MapFilterControl',
inject: ['map'],
mixins: [prefs, utils, api, sotadb],
prefs: {
key: 'mapFilter',
props: ['activationsEnabled', 'activationsFrom', 'activationsTo', 'pointsEnabled', 'pointsFrom', 'pointsTo',
'altitudeEnabled', 'altitudeFrom', 'altitudeTo', 'activatedByEnabled', 'activatedBy', 'activatedByThisYear',
'notActivatedByEnabled', 'notActivatedBy', 'notActivatedByThisYear', 'completeCandidateEnabled']
},
data () {
return {
open: false,
active: false,
activationsEnabled: false,
activationsFrom: null,
activationsTo: null,
pointsEnabled: false,
pointsFrom: null,
pointsTo: null,
altitudeEnabled: false,
altitudeFrom: null,
altitudeTo: null,
activatedByEnabled: false,
activatedBy: null,
activatedByThisYear: false,
notActivatedByEnabled: false,
notActivatedBy: null,
notActivatedByThisYear: false,
completeCandidateEnabled: false,
points: [1, 2, 4, 6, 8, 10],
filterLoadingCount: 0
}
},
mounted () {
this.updateFilter()
},
watch: {
filterLoadingCount (newFilterLoadingCount) {
if (newFilterLoadingCount > 0) {
this.$emit('startFiltering')
} else {
this.$emit('stopFiltering')
}
}
},
methods: {
close () {
this.open = false
},
toggleFilter () {
this.open = !this.open
},
updateFilter () {
let filterPromises = []
if (this.activationsEnabled) {
if (this.activationsFrom && this.activationsTo && this.activationsTo < this.activationsFrom) {
let tmp = this.activationsFrom
this.activationsFrom = this.activationsTo
this.activationsTo = tmp
}
if (this.activationsFrom !== null && this.activationsFrom !== '') {
filterPromises.push(['>=', 'act', parseInt(this.activationsFrom)])
}
if (this.activationsTo !== null && this.activationsTo !== '') {
filterPromises.push(['<=', 'act', parseInt(this.activationsTo)])
}
}
if (this.pointsEnabled) {
if (this.pointsFrom && this.pointsTo && this.pointsTo < this.pointsFrom) {
let tmp = this.pointsFrom
this.pointsFrom = this.pointsTo
this.pointsTo = tmp
}
if (this.pointsFrom) {
filterPromises.push(['>=', 'points', parseInt(this.pointsFrom)])
}
if (this.pointsTo) {
filterPromises.push(['<=', 'points', parseInt(this.pointsTo)])
}
}
if (this.altitudeEnabled) {
if (this.altitudeFrom) {
this.altitudeFrom = parseInt(this.altitudeFrom)
}
if (this.altitudeTo) {
this.altitudeTo = parseInt(this.altitudeTo)
}
if (this.altitudeFrom && this.altitudeTo && this.altitudeTo < this.altitudeFrom) {
let tmp = this.altitudeFrom
this.altitudeFrom = this.altitudeTo
this.altitudeTo = tmp
}
let mul = 1
if (this.$store.state.altitudeUnits === 'ft') {
mul = 1 / 3.28084
}
if (this.altitudeFrom) {
filterPromises.push(['>=', 'alt', Math.round(parseInt(this.altitudeFrom) * mul)])
}
if (this.altitudeTo) {
filterPromises.push(['<=', 'alt', Math.round(parseInt(this.altitudeTo) * mul)])
}
}
filterPromises.push(this.makeActivationsFilter('activatedBy', ['in', 'code']))
filterPromises.push(this.makeActivationsFilter('notActivatedBy', ['!in', 'code']))
if (this.completeCandidateEnabled) {
filterPromises.push(this.makeCompleteCandidateFilter())
}
Promise.all(filterPromises).then(filters => {
let filtersNoNull = filters.filter(el => {
return el !== null
})
if (filtersNoNull.length > 0) {
this.setSummitFilter(['all'].concat(filtersNoNull))
this.active = true
} else {
this.setSummitFilter(null)
this.active = false
}
})
},
setActivations (from, to) {
this.activationsEnabled = true
this.activationsFrom = from
this.activationsTo = to
this.updateFilter()
},
clearFilter () {
this.$options.prefs.props.forEach(key => {
this[key] = null
})
this.updateFilter()
},
setSummitFilter (filter) {
this.map.setFilter('summits_circles', filter)
this.map.setFilter('summits_names', filter)
this.map.setFilter('summits_activations', filter)
this.map.setFilter('summits_inactive_circles', filter)
this.map.setFilter('summits_inactive_names', filter)
},
makeActivationsFilter (paramField, filterTemplate) {
if (!this[paramField + 'Enabled'] || !this[paramField]) {
return null
}
this.filterLoadingCount++
return this.loadActivations(this[paramField].toUpperCase().trim())
.then(activations => {
this.filterLoadingCount--
let filter = filterTemplate
if (this[paramField + 'ThisYear']) {
let now = moment.utc()
activations = activations.filter(activation => {
return moment.utc(activation.date).isSame(now, 'year')
})
}
activations.forEach(activation => {
filter.push(activation.summit.code)
})
return filter
})
.catch(() => {
this.filterLoadingCount--
})
},
makeCompleteCandidateFilter () {
if (!this.authenticated) {
return null
}
this.filterLoadingCount++
return this.loadMyChaserUniques()
.then(chaserUniques => {
return this.loadMyActivatorUniques()
.then(activatorUniques => {
// Find all summits that have been chased, but not activated
let completeCandidates = new Set()
chaserUniques.forEach(ent => {
completeCandidates.add(ent)
})
activatorUniques.forEach(ent => {
completeCandidates.delete(ent)
})
this.filterLoadingCount--
return ['in', 'code', ...completeCandidates]
})
})
},
isActive () {
return this.active
}
}
}
</script>
<style scoped>
.mapbox-gl-filter {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M16.93 3.62C16.86 3.47 16.71 3.37 16.54 3.37H3.15c-0.17 0-0.32 0.1-0.39 0.25-0.07 0.15-0.05 0.33 0.06 0.46l5.15 6.24v5.76c0 0.15 0.08 0.29 0.2 0.37 0.07 0.04 0.15 0.06 0.23 0.06 0.07 0 0.13-0.01 0.19-0.04l2.89-1.43c0.15-0.07 0.24-0.22 0.24-0.39l0.01-4.32 5.15-6.24c0.11-0.13 0.13-0.31 0.06-0.46zm-5.97 6.27c-0.06 0.08-0.1 0.17-0.1 0.27l-0.01 4.21-2.03 1.01v-5.21c0-0.1-0.03-0.2-0.1-0.27L4.07 4.23H15.62Z' stroke-width='0.06'/%3E%3C/svg%3E%0A");
}
.mapbox-gl-filter.active {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M16.93 3.62C16.86 3.47 16.71 3.37 16.54 3.37H3.15c-0.17 0-0.32 0.1-0.39 0.25-0.07 0.15-0.05 0.33 0.06 0.46l5.15 6.24v5.76c0 0.15 0.08 0.29 0.2 0.37 0.07 0.04 0.15 0.06 0.23 0.06 0.07 0 0.13-0.01 0.19-0.04l2.89-1.43c0.15-0.07 0.24-0.22 0.24-0.39l0.01-4.32 5.15-6.24c0.11-0.13 0.13-0.31 0.06-0.46zm-5.97 6.27c-0.06 0.08-0.1 0.17-0.1 0.27l-0.01 4.21-2.03 1.01v-5.21c0-0.1-0.03-0.2-0.1-0.27L4.07 4.23H15.62Z' stroke-width='0.06' fill='%2333b5e5'/%3E%3C/svg%3E%0A");
}
.filter-criterion {
margin: 0.3em 0;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 0.2em;
background-color: #eee;
font-size: 0.8rem;
}
.filter-criterion div.tlabel {
margin-right: 0.75rem;
display: inline-block;
}
.filter-criterion .field {
margin: 0.2rem 0.2rem 0.5rem 0.2rem;
line-height: 1;
align-items: center;
}
.filter-criterion .field:last-child {
margin-bottom: 0.2rem;
}
.filter-criterion .callsign >>> input {
text-transform: uppercase;
}
.filter-criterion .count >>> input {
width: 4em;
}
.filter-criterion .altitude >>> input {
width: 5em;
}
.filter-criterion .field input, .filter-criterion .field select {
vertical-align: baseline;
}
/* Fix overrides from mapbox-gl.css */
.filter-container button {
width: auto;
height: auto;
padding-bottom: calc(.375em - 1px);
padding-left: .75em;
padding-right: .75em;
padding-top: calc(.375em - 1px);
}
.filter-criterion button {
border: 1px solid #dbdbdb;
background-color: #fff;
}
.action-buttons button {
float: right;
margin-left: 0.5em;
margin-top: 0.2em;
}
.mapbox-gl-filter-container .filter-container {
display: none;
padding: 0 0.5em 0.5em 0;
display: inline-block;
}
.mapbox-gl-filter-container button {
display: inline-block;
vertical-align: top;
}
</style>