add geolocalization

This commit is contained in:
sedum
2022-09-02 08:32:13 +02:00
parent 098e0202c9
commit dcdefec6de
12 changed files with 150 additions and 26 deletions

View File

@@ -30,11 +30,30 @@ v-row
:label="$t('common.address')" :label="$t('common.address')"
@change="changeAddress" @change="changeAddress"
:value="value.address") :value="value.address")
v-combobox.mr-4(ref='detailsView' v-if='settings.allow_geolocalization'
:prepend-icon='mdiMapSearch'
:disabled='disableDetails'
@input.native='searchCoordinates'
:label="$t('common.coordinates')"
:value='value.detailsView'
persistent-hint hide-no-data clearable no-filter
:loading='loading'
@change='selectDetails'
@focus='searchCoordinates'
:items="detailsList"
:hint="$t('event.coordinates_description')")
template(v-slot:item="{ item, attrs, on }")
v-list-item(v-bind='attrs' v-on='on')
v-list-item-content(two-line v-if='item')
v-list-item-title(v-text='item.display_name')
v-list-item-subtitle(v-text='`${item.lat}`+`,`+`${item.lon}`')
v-text-field(ref='details' v-show='false' v-if='settings.allow_geolocalization')
</template> </template>
<script> <script>
import { mdiMap, mdiMapMarker, mdiPlus } from '@mdi/js' import { mdiMap, mdiMapMarker, mdiPlus, mdiMapSearch } from '@mdi/js'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import { mapState } from 'vuex'
export default { export default {
name: 'WhereInput', name: 'WhereInput',
@@ -43,14 +62,20 @@ export default {
}, },
data () { data () {
return { return {
mdiMap, mdiMapMarker, mdiPlus, mdiMap, mdiMapMarker, mdiPlus, mdiMapSearch,
place: { }, place: { },
placeName: '', placeName: '',
places: [], places: [],
disableAddress: true disableAddress: true,
details: { },
detailsView: '',
detailsList: [],
disableDetails: true,
loading: false
} }
}, },
computed: { computed: {
...mapState(['settings']),
filteredPlaces () { filteredPlaces () {
if (!this.placeName) { return this.places } if (!this.placeName) { return this.places }
const placeName = this.placeName.trim().toLowerCase() const placeName = this.placeName.trim().toLowerCase()
@@ -85,6 +110,9 @@ export default {
if (typeof p === 'object' && !p.create) { if (typeof p === 'object' && !p.create) {
this.place.name = p.name.trim() this.place.name = p.name.trim()
this.place.address = p.address this.place.address = p.address
if (this.settings.allow_geolocalization) {
this.place.details = p.details
}
this.place.id = p.id this.place.id = p.id
this.disableAddress = true this.disableAddress = true
} else { // this is a new place } else { // this is a new place
@@ -100,6 +128,9 @@ export default {
} else { } else {
delete this.place.id delete this.place.id
this.place.address = '' this.place.address = ''
if (this.settings.allow_geolocalization) {
this.place.details = p.details
}
this.disableAddress = false this.disableAddress = false
this.$refs.place.blur() this.$refs.place.blur()
this.$refs.address.focus() this.$refs.address.focus()
@@ -110,7 +141,30 @@ export default {
changeAddress (v) { changeAddress (v) {
this.place.address = v this.place.address = v
this.$emit('input', { ...this.place }) this.$emit('input', { ...this.place })
} this.disableDetails = false
},
selectDetails (v) {
if (!v) { return }
if (typeof v === 'object') {
this.place.detailsView = v.lat+', '+v.lon
let c = {}
c.lat = v.lat
c.lon = v.lon
if (typeof c === 'object') {
this.place.details = JSON.stringify(c)
}
}
this.$emit('input', { ...this.place })
},
searchCoordinates: debounce(async function(ev) {
this.loading = true
const searchCoordinates = ev.target.value.trim().toLowerCase()
// this.detailsList = await this.$axios.$get(`placeNominatim?search=${searchCoordinates}`)
this.detailsList = await this.$axios.$get(`https://nominatim.openstreetmap.org/search?limit=3&format=json&namedetails=1&q=${searchCoordinates}` )
if (this.detailsList) {
this.loading = false;
}
}, 300),
} }
} }
</script> </script>

View File

@@ -25,6 +25,13 @@ v-container
v-model='place.address' v-model='place.address'
:placeholder='$t("common.address")') :placeholder='$t("common.address")')
v-textarea(v-if="settings.allow_geolocalization"
row-height="15"
:disabled="true"
:label="$t('common.details')"
v-model='place.details'
:placeholder='$t("common.details")')
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(@click='dialog=false' color='warning') {{$t('common.cancel')}} v-btn(@click='dialog=false' color='warning') {{$t('common.cancel')}}
@@ -47,6 +54,7 @@ v-container
</template> </template>
<script> <script>
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye } from '@mdi/js' import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye } from '@mdi/js'
import { mapState } from 'vuex'
export default { export default {
data () { data () {
@@ -68,10 +76,16 @@ export default {
async fetch () { async fetch () {
this.places = await this.$axios.$get('/place/all') this.places = await this.$axios.$get('/place/all')
}, },
computed: {
...mapState(['settings']),
},
methods: { methods: {
editPlace (item) { editPlace (item) {
this.place.name = item.name this.place.name = item.name
this.place.address = item.address this.place.address = item.address
if (this.settings.allow_geolocalization) {
this.place.details = JSON.parse(item.details)
}
this.place.id = item.id this.place.id = item.id
this.dialog = true this.dialog = true
}, },

View File

@@ -48,6 +48,10 @@ v-container
inset inset
:label="$t('admin.recurrent_event_visible')") :label="$t('admin.recurrent_event_visible')")
v-switch.mt-1(v-model='allow_geolocalization'
inset
:label="$t('admin.allow_geolocalization')")
v-dialog(v-model='showSMTP' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly') v-dialog(v-model='showSMTP' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
SMTP(@close='showSMTP = false') SMTP(@close='showSMTP = false')
@@ -107,6 +111,10 @@ export default {
get () { return this.settings.recurrent_event_visible }, get () { return this.settings.recurrent_event_visible },
set (value) { this.setSetting({ key: 'recurrent_event_visible', value }) } set (value) { this.setSetting({ key: 'recurrent_event_visible', value }) }
}, },
allow_geolocalization: {
get () { return this.settings.allow_geolocalization },
set (value) { this.setSetting({ key: 'allow_geolocalization', value }) }
},
filteredTimezones () { filteredTimezones () {
const current_timezone = moment.tz.guess() const current_timezone = moment.tz.guess()
tzNames.unshift(current_timezone) tzNames.unshift(current_timezone)

View File

@@ -6,6 +6,8 @@
"send": "Send", "send": "Send",
"where": "Where", "where": "Where",
"address": "Address", "address": "Address",
"coordinates": "Coordinates",
"details": "Details",
"when": "When", "when": "When",
"what": "What", "what": "What",
"media": "Media", "media": "Media",
@@ -129,6 +131,7 @@
"added_anon": "Event added, but has yet to be confirmed.", "added_anon": "Event added, but has yet to be confirmed.",
"updated": "Event updated", "updated": "Event updated",
"where_description": "Where's the event? If not present you can create it.", "where_description": "Where's the event? If not present you can create it.",
"coordinates_description": "You can search the place by name, or paste the coordinates separated by comma: lat,lon",
"confirmed": "Event confirmed", "confirmed": "Event confirmed",
"not_found": "Could not find event", "not_found": "Could not find event",
"remove_confirmation": "Are you sure you want to remove this event?", "remove_confirmation": "Are you sure you want to remove this event?",
@@ -182,6 +185,7 @@
"allow_registration_description": "Allow open registrations?", "allow_registration_description": "Allow open registrations?",
"allow_anon_event": "Allow anonymous events (has to be confirmed)?", "allow_anon_event": "Allow anonymous events (has to be confirmed)?",
"allow_recurrent_event": "Allow recurring events", "allow_recurrent_event": "Allow recurring events",
"allow_geolocalization": "Allow geolocalization of events",
"recurrent_event_visible": "Show recurring events by default", "recurrent_event_visible": "Show recurring events by default",
"federation": "Federation / ActivityPub", "federation": "Federation / ActivityPub",
"enable_federation": "Turn on federation", "enable_federation": "Turn on federation",

View File

@@ -135,7 +135,7 @@ export default {
valid: false, valid: false,
openImportDialog: false, openImportDialog: false,
event: { event: {
place: { name: '', address: '' }, place: { name: '', address: '', details: {} },
title: '', title: '',
description: '', description: '',
tags: [], tags: [],
@@ -214,6 +214,7 @@ export default {
} }
formData.append('place_name', this.event.place.name) formData.append('place_name', this.event.place.name)
formData.append('place_address', this.event.place.address) formData.append('place_address', this.event.place.address)
formData.append('place_details', this.event.place.details)
formData.append('description', this.event.description) formData.append('description', this.event.description)
formData.append('multidate', !!this.date.multidate) formData.append('multidate', !!this.date.multidate)
formData.append('start_datetime', dayjs(this.date.from).unix()) formData.append('start_datetime', dayjs(this.date.from).unix())

View File

@@ -44,7 +44,7 @@ const eventController = {
order: [[Sequelize.col('w'), 'DESC']], order: [[Sequelize.col('w'), 'DESC']],
where: { where: {
tag: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('tag')), 'LIKE', '%' + search + '%'), tag: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('tag')), 'LIKE', '%' + search + '%'),
}, },
attributes: [['tag','label'], [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'INTEGER'), 'w']], attributes: [['tag','label'], [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'INTEGER'), 'w']],
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'], group: ['tag.tag'],
@@ -110,7 +110,7 @@ const eventController = {
attributes: ['tag'], attributes: ['tag'],
through: { attributes: [] } through: { attributes: [] }
}, },
{ model: Place, required: true, attributes: ['id', 'name', 'address'] } { model: Place, required: true, attributes: ['id', 'name', 'address', 'details'] }
], ],
replacements, replacements,
limit: 30, limit: 30,
@@ -192,7 +192,7 @@ const eventController = {
}, },
include: [ include: [
{ model: Tag, required: false, attributes: ['tag'], through: { attributes: [] } }, { model: Tag, required: false, attributes: ['tag'], through: { attributes: [] } },
{ model: Place, attributes: ['name', 'address', 'id'] }, { model: Place, attributes: ['name', 'address', 'details', 'id'] },
{ {
model: Resource, model: Resource,
where: !is_admin && { hidden: false }, where: !is_admin && { hidden: false },
@@ -222,7 +222,7 @@ const eventController = {
recurrent: null, recurrent: null,
[Op.or]: [ [Op.or]: [
{ start_datetime: { [Op.gt]: event.start_datetime } }, { start_datetime: { [Op.gt]: event.start_datetime } },
{ {
start_datetime: event.start_datetime, start_datetime: event.start_datetime,
id: { [Op.gt]: event.id } id: { [Op.gt]: event.id }
} }
@@ -239,7 +239,7 @@ const eventController = {
recurrent: null, recurrent: null,
[Op.or]: [ [Op.or]: [
{ start_datetime: { [Op.lt]: event.start_datetime } }, { start_datetime: { [Op.lt]: event.start_datetime } },
{ {
start_datetime: event.start_datetime, start_datetime: event.start_datetime,
id: { [Op.lt]: event.id } id: { [Op.lt]: event.id }
} }
@@ -405,7 +405,8 @@ const eventController = {
} }
place = await Place.create({ place = await Place.create({
name: body.place_name, name: body.place_name,
address: body.place_address address: body.place_address,
details: body.place_details
}) })
} }
} }
@@ -559,13 +560,14 @@ const eventController = {
} }
place = await Place.create({ place = await Place.create({
name: body.place_name, name: body.place_name,
address: body.place_address address: body.place_address,
details: body.place_details
}) })
} }
} }
await event.setPlace(place) await event.setPlace(place)
// create/assign tags // create/assign tags
let tags = [] let tags = []
if (body.tags) { if (body.tags) {
@@ -624,7 +626,7 @@ const eventController = {
/** /**
* Method to search for events with pagination and filtering * Method to search for events with pagination and filtering
* @returns * @returns
*/ */
async _select ({ async _select ({
start = dayjs().unix(), start = dayjs().unix(),
@@ -665,7 +667,7 @@ const eventController = {
const replacements = [] const replacements = []
if (tags && places) { if (tags && places) {
where[Op.and] = [ where[Op.and] = [
{ placeId: places ? places.split(',') : []}, { placeId: places ? places.split(',') : []},
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) in (?)`)) Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) in (?)`))
] ]
@@ -679,10 +681,10 @@ const eventController = {
let pagination = {} let pagination = {}
if (limit) { if (limit) {
pagination = { pagination = {
limit, limit,
offset: limit * page, offset: limit * page,
} }
} }
const events = await Event.findAll({ const events = await Event.findAll({
@@ -698,7 +700,7 @@ const eventController = {
attributes: ['tag'], attributes: ['tag'],
through: { attributes: [] } through: { attributes: [] }
}, },
{ model: Place, required: true, attributes: ['id', 'name', 'address'] } { model: Place, required: true, attributes: ['id', 'name', 'address', 'details'] }
], ],
...pagination, ...pagination,
replacements replacements

View File

@@ -5,6 +5,7 @@ const exportController = require('./export')
const log = require('../../log') const log = require('../../log')
const { Op, where, col, fn, cast } = require('sequelize') const { Op, where, col, fn, cast } = require('sequelize')
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search?limit=3&format=geocodejson&accept-language=it&q='
module.exports = { module.exports = {
@@ -60,7 +61,7 @@ module.exports = {
{ address: where(fn('LOWER', col('address')), 'LIKE', '%' + search + '%')}, { address: where(fn('LOWER', col('address')), 'LIKE', '%' + search + '%')},
] ]
}, },
attributes: ['name', 'address', 'id'], attributes: ['name', 'address', 'details', 'id'],
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }], include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
group: ['place.id'], group: ['place.id'],
raw: true, raw: true,
@@ -70,6 +71,16 @@ module.exports = {
// TOFIX: don't know why limit does not work // TOFIX: don't know why limit does not work
return res.json(places.slice(0, 10)) return res.json(places.slice(0, 10))
} },
} // async _nominatim (req, res) {
// const details = req.params.place_details
// const ret = await axios.get(`${NOMINATIM_URL}${details}`, { headers: { 'User-Agent': 'gancio 0.20' } })
// debug(`${NOMINATIM_URL}${details}`)
// debug(ret.status)
// debug(ret.statusText)
// debug(ret.data)
// return ret
// },
}

View File

@@ -29,6 +29,7 @@ const defaultSettings = {
allow_anon_event: true, allow_anon_event: true,
allow_recurrent_event: false, allow_recurrent_event: false,
recurrent_event_visible: false, recurrent_event_visible: false,
allow_geolocalization: false,
enable_federation: true, enable_federation: true,
enable_resources: false, enable_resources: false,
hide_boosts: true, hide_boosts: true,

View File

@@ -89,16 +89,16 @@ if (config.status !== 'READY') {
* @param {integer} [end] - end timestamp (optional) * @param {integer} [end] - end timestamp (optional)
* @param {array} [tags] - List of tags * @param {array} [tags] - List of tags
* @param {array} [places] - List of places id * @param {array} [places] - List of places id
* @param {integer} [max] - Limit events * @param {integer} [max] - Limit events
* @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings) * @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings)
* @param {integer} [page] - Pagination * @param {integer} [page] - Pagination
* @param {boolean} [older] - select <= start instead of >= * @param {boolean} [older] - select <= start instead of >=
* @example ***Example*** * @example ***Example***
* [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events) * [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
* [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42) * [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
*/ */
api.get('/events', cors, eventController.select) api.get('/events', cors, eventController.select)
/** /**
* Add a new event * Add a new event
@@ -110,6 +110,7 @@ if (config.status !== 'READY') {
* @param {string} description - event's description (html accepted and sanitized) * @param {string} description - event's description (html accepted and sanitized)
* @param {string} place_name - the name of the place * @param {string} place_name - the name of the place
* @param {string} [place_address] - the address of the place * @param {string} [place_address] - the address of the place
* @param {array} [place_details] - the details of the place
* @param {integer} start_datetime - start timestamp * @param {integer} start_datetime - start timestamp
* @param {integer} multidate - is a multidate event? * @param {integer} multidate - is a multidate event?
* @param {array} tags - List of tags * @param {array} tags - List of tags
@@ -160,6 +161,7 @@ if (config.status !== 'READY') {
api.get('/place/all', isAdmin, placeController.getAll) api.get('/place/all', isAdmin, placeController.getAll)
api.get('/place/:placeName', cors, placeController.getEvents) api.get('/place/:placeName', cors, placeController.getEvents)
api.get('/place', cors, placeController.search) api.get('/place', cors, placeController.search)
// api.get('/placeNominatim/:place_details', cors, placeController._nominatim)
api.put('/place', isAdmin, placeController.updatePlace) api.put('/place', isAdmin, placeController.updatePlace)
api.get('/tag', cors, tagController.search) api.get('/tag', cors, tagController.search)

View File

@@ -10,7 +10,8 @@ Place.init({
index: true, index: true,
allowNull: false allowNull: false
}, },
address: DataTypes.STRING address: DataTypes.STRING,
details: DataTypes.JSON
}, { sequelize, modelName: 'place' }) }, { sequelize, modelName: 'place' })
module.exports = Place module.exports = Place

View File

@@ -0,0 +1,25 @@
'use strict';
module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
return queryInterface.addColumn('places', 'details', { type: Sequelize.JSON })
},
async down (queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
return queryInterface.removeColumn('places', 'details')
}
};

View File

@@ -8,6 +8,7 @@ export const state = () => ({
allow_anon_event: true, allow_anon_event: true,
allow_recurrent_event: true, allow_recurrent_event: true,
recurrent_event_visible: false, recurrent_event_visible: false,
allow_geolocalization: false,
enable_federation: false, enable_federation: false,
enable_resources: false, enable_resources: false,
hide_boosts: true, hide_boosts: true,