forked from potsda.mn/mobilizon
Introduce Mimirsbrunn geocoder and improve addresses & maps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
0e7cf89492
commit
c599a47d58
|
@ -137,6 +137,9 @@ config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps,
|
||||||
config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
|
config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
|
||||||
api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil
|
api_key: System.get_env("GEOSPATIAL_MAP_QUEST_API_KEY") || nil
|
||||||
|
|
||||||
|
config :mobilizon, Mobilizon.Service.Geospatial.Mimirsbrunn,
|
||||||
|
endpoint: System.get_env("GEOSPATIAL_MIMIRSBRUNN_ENDPOINT") || nil
|
||||||
|
|
||||||
config :mobilizon, Oban,
|
config :mobilizon, Oban,
|
||||||
repo: Mobilizon.Storage.Repo,
|
repo: Mobilizon.Storage.Repo,
|
||||||
prune: {:maxlen, 10_000},
|
prune: {:maxlen, 10_000},
|
||||||
|
|
|
@ -52,7 +52,7 @@ config :mobilizon, MobilizonWeb.Endpoint,
|
||||||
# Do not include metadata nor timestamps in development logs
|
# Do not include metadata nor timestamps in development logs
|
||||||
config :logger, :console, format: "[$level] $message\n", level: :debug
|
config :logger, :console, format: "[$level] $message\n", level: :debug
|
||||||
|
|
||||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.GoogleMaps
|
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||||
|
|
||||||
# Set a higher stacktrace during development. Avoid configuring such
|
# Set a higher stacktrace during development. Avoid configuring such
|
||||||
# in production as building large stacktraces may be expensive.
|
# in production as building large stacktraces may be expensive.
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
"graphql-tag": "^2.10.1",
|
"graphql-tag": "^2.10.1",
|
||||||
"intersection-observer": "^0.7.0",
|
"intersection-observer": "^0.7.0",
|
||||||
"leaflet": "^1.4.0",
|
"leaflet": "^1.4.0",
|
||||||
|
"leaflet.locatecontrol": "^0.68.0",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
"register-service-worker": "^1.6.2",
|
"register-service-worker": "^1.6.2",
|
||||||
|
@ -44,6 +45,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.3",
|
"@types/chai": "^4.2.3",
|
||||||
"@types/leaflet": "^1.5.2",
|
"@types/leaflet": "^1.5.2",
|
||||||
|
"@types/leaflet.locatecontrol": "^0.60.7",
|
||||||
"@types/lodash": "^4.14.141",
|
"@types/lodash": "^4.14.141",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^5.2.6",
|
||||||
"@vue/cli-plugin-babel": "^4.0.3",
|
"@vue/cli-plugin-babel": "^4.0.3",
|
||||||
|
|
|
@ -1,125 +1,242 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<b-field :label="$t('Find an address')">
|
<b-field expanded>
|
||||||
|
<template slot="label">
|
||||||
|
{{ $t('Find an address') }}
|
||||||
|
<b-button v-if="!gettingLocation" size="is-small" icon-right="map-marker" @click="locateMe" />
|
||||||
|
<span v-else>{{ $t('Getting location') }}</span>
|
||||||
|
</template>
|
||||||
<b-autocomplete
|
<b-autocomplete
|
||||||
:data="data"
|
:data="data"
|
||||||
v-model="queryText"
|
v-model="queryText"
|
||||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||||
field="description"
|
field="fullName"
|
||||||
:loading="isFetching"
|
:loading="isFetching"
|
||||||
@typing="getAsyncData"
|
@typing="getAsyncData"
|
||||||
icon="map-marker"
|
icon="map-marker"
|
||||||
@select="option => selected = option">
|
expanded
|
||||||
|
@select="updateSelected">
|
||||||
|
|
||||||
<template slot-scope="{option}">
|
<template slot-scope="{option}">
|
||||||
<b>{{ option.description }}</b><br />
|
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||||
<i v-if="option.url != null">Local</i>
|
<b>{{ option.poiInfos.name }}</b><br />
|
||||||
<p>
|
<small>{{ option.poiInfos.alternativeName }}</small>
|
||||||
<small>{{ option.street }},  {{ option.postalCode }} {{ option.locality }}</small>
|
|
||||||
</p>
|
|
||||||
</template>
|
</template>
|
||||||
<template slot="empty">
|
<template slot="empty">
|
||||||
<span v-if="queryText.length < 5">{{ $t('Please type at least 5 characters') }}</span>
|
<span v-if="isFetching">{{ $t('Searching…') }}</span>
|
||||||
<span v-else-if="isFetching">{{ $t('Searching…') }}</span>
|
|
||||||
<div v-else class="is-enabled">
|
<div v-else class="is-enabled">
|
||||||
<span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
|
<span>{{ $t('No results for "{queryText}". You can try another search term or drag and drop the marker on the map', { queryText }) }}</span>
|
||||||
<p class="control" @click="addressModalActive = true">
|
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||||
<button type="button" class="button is-primary">{{ $t('Add') }}</button>
|
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||||
</p>
|
<!-- </p>-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</b-autocomplete>
|
</b-autocomplete>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-modal :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">
|
<div class="map" v-if="selected && selected.geom">
|
||||||
<div class="modal-card" style="width: auto">
|
<map-leaflet
|
||||||
<header class="modal-card-head">
|
:coords="selected.geom"
|
||||||
<p class="modal-card-title">{{ $t('Add an address') }}</p>
|
:marker="{ text: [selected.poiInfos.name, selected.poiInfos.alternativeName], icon: selected.poiInfos.poiIcon.icon}"
|
||||||
</header>
|
:updateDraggableMarkerCallback="reverseGeoCode"
|
||||||
<section class="modal-card-body">
|
:options="{ zoom: mapDefaultZoom }"
|
||||||
<form>
|
:readOnly="false"
|
||||||
<b-field :label="$t('Name')">
|
/>
|
||||||
<b-input aria-required="true" required v-model="selected.description" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field :label="$t('Street')">
|
|
||||||
<b-input v-model="selected.street" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field :label="$t('Postal Code')">
|
|
||||||
<b-input v-model="selected.postalCode" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field :label="$t('Locality')">
|
|
||||||
<b-input v-model="selected.locality" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field :label="$t('Region')">
|
|
||||||
<b-input v-model="selected.region" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field :label="$t('Country')">
|
|
||||||
<b-input v-model="selected.country" />
|
|
||||||
</b-field>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
<!-- <b-modal v-if="selected" :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">-->
|
||||||
|
<!-- <div class="modal-card" style="width: auto">-->
|
||||||
|
<!-- <header class="modal-card-head">-->
|
||||||
|
<!-- <p class="modal-card-title">{{ $t('Add an address') }}</p>-->
|
||||||
|
<!-- </header>-->
|
||||||
|
<!-- <section class="modal-card-body">-->
|
||||||
|
<!-- <form>-->
|
||||||
|
<!-- <b-field :label="$t('Name')">-->
|
||||||
|
<!-- <b-input aria-required="true" required v-model="selected.description" />-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
|
||||||
|
<!-- <b-field :label="$t('Street')">-->
|
||||||
|
<!-- <b-input v-model="selected.street" />-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
|
||||||
|
<!-- <b-field grouped>-->
|
||||||
|
<!-- <b-field :label="$t('Postal Code')">-->
|
||||||
|
<!-- <b-input v-model="selected.postalCode" />-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
|
||||||
|
<!-- <b-field :label="$t('Locality')">-->
|
||||||
|
<!-- <b-input v-model="selected.locality" />-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
|
||||||
|
<!-- <b-field grouped>-->
|
||||||
|
<!-- <b-field :label="$t('Region')">-->
|
||||||
|
<!-- <b-input v-model="selected.region" />-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
|
||||||
|
<!-- <b-field :label="$t('Country')">-->
|
||||||
|
<!-- <b-input v-model="selected.country" />-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
<!-- </b-field>-->
|
||||||
|
<!-- </form>-->
|
||||||
|
<!-- </section>-->
|
||||||
|
<!-- <footer class="modal-card-foot">-->
|
||||||
|
<!-- <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>-->
|
||||||
|
<!-- </footer>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </b-modal>-->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||||
import { Address, IAddress } from '@/types/address.model';
|
import { Address, IAddress } from '@/types/address.model';
|
||||||
import { ADDRESS } from '@/graphql/address';
|
import { ADDRESS, REVERSE_GEOCODE } from '@/graphql/address';
|
||||||
import { Modal } from 'buefy/dist/components/dialog';
|
import { Modal } from 'buefy/dist/components/dialog';
|
||||||
|
import { LatLng } from 'leaflet';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
|
||||||
Modal,
|
Modal,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class AddressAutoComplete extends Vue {
|
export default class AddressAutoComplete extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, default: () => [] }) initialData!: IAddress[];
|
@Prop({ required: true }) value!: IAddress;
|
||||||
@Prop({ required: false }) value!: IAddress;
|
|
||||||
|
|
||||||
data: IAddress[] = this.initialData;
|
data: IAddress[] = [];
|
||||||
selected: IAddress|null = new Address();
|
selected!: IAddress;
|
||||||
isFetching: boolean = false;
|
isFetching: boolean = false;
|
||||||
queryText: string = this.value && this.value.description || '';
|
queryText: string = this.value && (new Address(this.value)).fullName || '';
|
||||||
addressModalActive: boolean = false;
|
addressModalActive: boolean = false;
|
||||||
|
private gettingLocation: boolean = false;
|
||||||
|
private location!: Position;
|
||||||
|
private gettingLocationError: any;
|
||||||
|
private mapDefaultZoom: number = 15;
|
||||||
|
|
||||||
|
@Watch('value')
|
||||||
|
updateEditing() {
|
||||||
|
this.selected = this.value;
|
||||||
|
const address = new Address(this.selected);
|
||||||
|
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
async getAsyncData(query) {
|
async getAsyncData(query) {
|
||||||
if (query.length < 5) {
|
if (!query.length) {
|
||||||
|
this.data = [];
|
||||||
|
this.selected = new Address();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length < 3) {
|
||||||
this.data = [];
|
this.data = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
const result = await this.$apollo.query({
|
const result = await this.$apollo.query({
|
||||||
query: ADDRESS,
|
query: ADDRESS,
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'network-only',
|
||||||
variables: { query },
|
variables: {
|
||||||
|
query,
|
||||||
|
locale: this.$i18n.locale,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.data = result.data.searchAddress as IAddress[];
|
this.data = result.data.searchAddress.map(address => new Address(address));
|
||||||
this.isFetching = false;
|
this.isFetching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch deep because of subproperties
|
updateSelected(option) {
|
||||||
@Watch('selected', { deep: true })
|
if (option == null) return;
|
||||||
updateSelected() {
|
this.selected = option;
|
||||||
|
console.log('update selected', this.selected);
|
||||||
this.$emit('input', this.selected);
|
this.$emit('input', this.selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPopup() {
|
resetPopup() {
|
||||||
this.selected = new Address();
|
this.selected = new Address();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openNewAddressModal() {
|
||||||
|
this.resetPopup();
|
||||||
|
this.addressModalActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reverseGeoCode(e: LatLng, zoom: Number) {
|
||||||
|
// If the position has been updated through autocomplete selection, no need to geocode it !
|
||||||
|
if (this.checkCurrentPosition(e)) return;
|
||||||
|
const result = await this.$apollo.query({
|
||||||
|
query: REVERSE_GEOCODE,
|
||||||
|
variables: {
|
||||||
|
latitude: e.lat,
|
||||||
|
longitude: e.lng,
|
||||||
|
zoom,
|
||||||
|
locale: this.$i18n.locale,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.data = result.data.reverseGeocode.map(address => new Address(address));
|
||||||
|
const defaultAddress = new Address(this.data[0]);
|
||||||
|
this.selected = defaultAddress;
|
||||||
|
this.$emit('input', this.selected);
|
||||||
|
this.queryText = `${defaultAddress.poiInfos.name} ${defaultAddress.poiInfos.alternativeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCurrentPosition(e: LatLng) {
|
||||||
|
if (!this.selected || !this.selected.geom) return false;
|
||||||
|
const lat = parseFloat(this.selected.geom.split(';')[1]);
|
||||||
|
const lon = parseFloat(this.selected.geom.split(';')[0]);
|
||||||
|
|
||||||
|
return e.lat === lat && e.lng === lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
async locateMe(): Promise<void> {
|
||||||
|
|
||||||
|
this.gettingLocation = true;
|
||||||
|
try {
|
||||||
|
this.gettingLocation = false;
|
||||||
|
this.location = await this.getLocation();
|
||||||
|
this.mapDefaultZoom = 12;
|
||||||
|
this.reverseGeoCode(new LatLng(this.location.coords.latitude, this.location.coords.longitude), 12);
|
||||||
|
} catch (e) {
|
||||||
|
this.gettingLocation = false;
|
||||||
|
this.gettingLocationError = e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLocation(): Promise<Position> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
if (!('geolocation' in navigator)) {
|
||||||
|
reject(new Error('Geolocation is not available.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(pos => {
|
||||||
|
resolve(pos);
|
||||||
|
}, err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.autocomplete .dropdown-item.is-disabled .is-enabled {
|
.autocomplete {
|
||||||
|
.dropdown-menu {
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.is-disabled {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-only {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,40 +5,54 @@
|
||||||
:style="`height: ${mergedOptions.height}; width: ${mergedOptions.width}`"
|
:style="`height: ${mergedOptions.height}; width: ${mergedOptions.width}`"
|
||||||
class="leaflet-map"
|
class="leaflet-map"
|
||||||
:center="[lat, lon]"
|
:center="[lat, lon]"
|
||||||
|
@click="clickMap"
|
||||||
|
@update:zoom="updateZoom"
|
||||||
>
|
>
|
||||||
<l-tile-layer
|
<l-tile-layer
|
||||||
url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png"
|
||||||
attribution="© OpenStreetMap contributors"
|
:attribution="$t('© The OpenStreetMap Contributors')"
|
||||||
>
|
>
|
||||||
|
|
||||||
</l-tile-layer>
|
</l-tile-layer>
|
||||||
<l-marker :lat-lng="[lat, lon]" >
|
<v-locatecontrol :options="{icon: 'mdi mdi-map-marker'}"/>
|
||||||
<l-popup v-if="popup">{{ popup }}</l-popup>
|
<l-marker :lat-lng="[lat, lon]" @add="openPopup" @update:latLng="updateDraggableMarkerPosition" :draggable="!readOnly">
|
||||||
|
<l-popup v-if="popupMultiLine">
|
||||||
|
<span v-for="line in popupMultiLine" :key="line">{{ line }}<br /></span>
|
||||||
|
</l-popup>
|
||||||
</l-marker>
|
</l-marker>
|
||||||
</l-map>
|
</l-map>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Icon } from 'leaflet';
|
import { Icon, LatLng, LeafletMouseEvent } from 'leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
import { LMap, LTileLayer, LMarker, LPopup } from 'vue2-leaflet';
|
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from 'vue2-leaflet';
|
||||||
|
import Vue2LeafletLocateControl from '@/components/Map/Vue2LeafletLocateControl.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { LTileLayer, LMap, LMarker, LPopup },
|
components: { LTileLayer, LMap, LMarker, LPopup, LIcon, 'v-locatecontrol': Vue2LeafletLocateControl },
|
||||||
})
|
})
|
||||||
export default class Map extends Vue {
|
export default class Map extends Vue {
|
||||||
|
@Prop({ type: Boolean, required: false, default: true }) readOnly!: boolean;
|
||||||
@Prop({ type: String, required: true }) coords!: string;
|
@Prop({ type: String, required: true }) coords!: string;
|
||||||
@Prop({ type: String, required: false }) popup!: string;
|
@Prop({ type: Object, required: false }) marker!: { text: String|String[], icon: String };
|
||||||
@Prop({ type: Object, required: false }) options!: object;
|
@Prop({ type: Object, required: false }) options!: object;
|
||||||
|
@Prop({ type: Function, required: false, default: () => {} }) updateDraggableMarkerCallback!: Function;
|
||||||
|
|
||||||
defaultOptions: object = {
|
defaultOptions: {
|
||||||
|
zoom: Number;
|
||||||
|
height: String;
|
||||||
|
width: String;
|
||||||
|
} = {
|
||||||
zoom: 15,
|
zoom: 15,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
zoom = this.defaultOptions.zoom;
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// this part resolve an issue where the markers would not appear
|
// this part resolve an issue where the markers would not appear
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -51,12 +65,38 @@ export default class Map extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openPopup(event) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
event.target.openPopup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get mergedOptions(): object {
|
get mergedOptions(): object {
|
||||||
return { ...this.defaultOptions, ...this.options };
|
return { ...this.defaultOptions, ...this.options };
|
||||||
}
|
}
|
||||||
|
|
||||||
get lat() { return this.$props.coords.split(';')[1]; }
|
get lat() { return this.$props.coords.split(';')[1]; }
|
||||||
get lon() { return this.$props.coords.split(';')[0]; }
|
get lon() { return this.$props.coords.split(';')[0]; }
|
||||||
|
|
||||||
|
get popupMultiLine() {
|
||||||
|
if (Array.isArray(this.marker.text)) {
|
||||||
|
return this.marker.text;
|
||||||
|
}
|
||||||
|
return [this.marker.text];
|
||||||
|
}
|
||||||
|
|
||||||
|
clickMap(event: LeafletMouseEvent) {
|
||||||
|
this.updateDraggableMarkerPosition(event.latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDraggableMarkerPosition(e: LatLng) {
|
||||||
|
console.log('updateDraggableMarkerPosition', e);
|
||||||
|
this.updateDraggableMarkerCallback(e, this.zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateZoom(zoom: Number) {
|
||||||
|
this.zoom = zoom;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
47
js/src/components/Map/Vue2LeafletLocateControl.vue
Normal file
47
js/src/components/Map/Vue2LeafletLocateControl.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div style="display: none;">
|
||||||
|
<slot v-if="ready"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Fork of https://github.com/domoritz/leaflet-locatecontrol to try to trigger location manually (not done ATM)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import L, { DomEvent } from 'leaflet';
|
||||||
|
import { findRealParent, propsBinder } from 'vue2-leaflet';
|
||||||
|
import 'leaflet.locatecontrol';
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
beforeDestroy() {
|
||||||
|
// @ts-ignore
|
||||||
|
this.parentContainer.removeLayer(this);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class Vue2LeafletLocateControl extends Vue {
|
||||||
|
@Prop({ type: Object, default: () => { return {}; } }) options;
|
||||||
|
@Prop({ type: Boolean, default: true }) visible = true;
|
||||||
|
ready: boolean = false;
|
||||||
|
mapObject!: any;
|
||||||
|
parentContainer: any;
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.mapObject = L.control.locate(this.options);
|
||||||
|
DomEvent.on(this.mapObject, this.$listeners as any);
|
||||||
|
propsBinder(this, this.mapObject, this.$props);
|
||||||
|
this.ready = true;
|
||||||
|
this.parentContainer = findRealParent(this.$parent);
|
||||||
|
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public locate() {
|
||||||
|
this.mapObject.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "~leaflet.locatecontrol/dist/L.Control.Locate.css";
|
||||||
|
</style>
|
|
@ -1,10 +1,6 @@
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
|
||||||
export const ADDRESS = gql`
|
const $addressFragment = `
|
||||||
query($query:String!) {
|
|
||||||
searchAddress(
|
|
||||||
query: $query
|
|
||||||
) {
|
|
||||||
id,
|
id,
|
||||||
description,
|
description,
|
||||||
geom,
|
geom,
|
||||||
|
@ -13,8 +9,26 @@ export const ADDRESS = gql`
|
||||||
postalCode,
|
postalCode,
|
||||||
region,
|
region,
|
||||||
country,
|
country,
|
||||||
|
type,
|
||||||
url,
|
url,
|
||||||
originId
|
originId
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ADDRESS = gql`
|
||||||
|
query($query:String!, $locale: String) {
|
||||||
|
searchAddress(
|
||||||
|
query: $query,
|
||||||
|
locale: $locale
|
||||||
|
) {
|
||||||
|
${$addressFragment}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const REVERSE_GEOCODE = gql`
|
||||||
|
query($latitude: Float!, $longitude: Float!, $zoom: Int, $locale: String) {
|
||||||
|
reverseGeocode(latitude: $latitude, longitude: $longitude, zoom: $zoom, locale: $locale) {
|
||||||
|
${$addressFragment}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -5,7 +5,13 @@ query {
|
||||||
config {
|
config {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
registrationsOpen
|
registrationsOpen,
|
||||||
|
countryCode,
|
||||||
|
location {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
accuracyRadius
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -24,7 +24,9 @@ const physicalAddressQuery = `
|
||||||
region,
|
region,
|
||||||
country,
|
country,
|
||||||
geom,
|
geom,
|
||||||
id
|
type,
|
||||||
|
id,
|
||||||
|
originId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const tagsQuery = `
|
const tagsQuery = `
|
||||||
|
|
|
@ -110,6 +110,7 @@
|
||||||
"From the {startDate} to the {endDate}": "From the {startDate} to the {endDate}",
|
"From the {startDate} to the {endDate}": "From the {startDate} to the {endDate}",
|
||||||
"Gather ⋅ Organize ⋅ Mobilize": "Gather ⋅ Organize ⋅ Mobilize",
|
"Gather ⋅ Organize ⋅ Mobilize": "Gather ⋅ Organize ⋅ Mobilize",
|
||||||
"General information": "General information",
|
"General information": "General information",
|
||||||
|
"Getting location": "Getting location",
|
||||||
"Going as {name}": "Going as {name}",
|
"Going as {name}": "Going as {name}",
|
||||||
"Group List": "Group List",
|
"Group List": "Group List",
|
||||||
"Group full name": "Group full name",
|
"Group full name": "Group full name",
|
||||||
|
@ -160,7 +161,7 @@
|
||||||
"No events found": "No events found",
|
"No events found": "No events found",
|
||||||
"No group found": "No group found",
|
"No group found": "No group found",
|
||||||
"No groups found": "No groups found",
|
"No groups found": "No groups found",
|
||||||
"No results for \"{queryText}\"": "No results for \"{queryText}\"",
|
"No results for \"{queryText}\". You can try another search term or drag and drop the marker on the map": "No results for \"{queryText}\". You can try another search term or drag and drop the marker on the map",
|
||||||
"No user account with this email was found. Maybe you made a typo?": "No user account with this email was found. Maybe you made a typo?",
|
"No user account with this email was found. Maybe you made a typo?": "No user account with this email was found. Maybe you made a typo?",
|
||||||
"Number of places": "Number of places",
|
"Number of places": "Number of places",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
|
@ -195,7 +196,6 @@
|
||||||
"Please make sure the address is correct and that the page hasn't been moved.": "Please make sure the address is correct and that the page hasn't been moved.",
|
"Please make sure the address is correct and that the page hasn't been moved.": "Please make sure the address is correct and that the page hasn't been moved.",
|
||||||
"Please read the full rules": "Please read the full rules",
|
"Please read the full rules": "Please read the full rules",
|
||||||
"Please refresh the page and retry.": "Please refresh the page and retry.",
|
"Please refresh the page and retry.": "Please refresh the page and retry.",
|
||||||
"Please type at least 5 characters": "Please type at least 5 characters",
|
|
||||||
"Postal Code": "Postal Code",
|
"Postal Code": "Postal Code",
|
||||||
"Private event": "Private event",
|
"Private event": "Private event",
|
||||||
"Private feeds": "Private feeds",
|
"Private feeds": "Private feeds",
|
||||||
|
@ -327,5 +327,6 @@
|
||||||
"{count} participants": "No participants yet | One participant | {count} participants",
|
"{count} participants": "No participants yet | One participant | {count} participants",
|
||||||
"{count} requests waiting": "{count} requests waiting",
|
"{count} requests waiting": "{count} requests waiting",
|
||||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
|
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
|
||||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
|
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks",
|
||||||
|
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
|
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
|
||||||
"A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
|
"A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
|
||||||
"Abandon edition": "Abandonner l'édition",
|
"Abandon edition": "Abandonner l'édition",
|
||||||
"About": "À propos",
|
|
||||||
"About Mobilizon": "À propos de Mobilizon",
|
"About Mobilizon": "À propos de Mobilizon",
|
||||||
"About this event": "À propos de cet événement",
|
"About this event": "À propos de cet événement",
|
||||||
"About this instance": "À propos de cette instance",
|
"About this instance": "À propos de cette instance",
|
||||||
"Add": "Ajouter",
|
"About": "À propos",
|
||||||
"Add an address": "Ajouter une adresse",
|
"Add an address": "Ajouter une adresse",
|
||||||
"Add some tags": "Ajouter des tags",
|
"Add some tags": "Ajouter des tags",
|
||||||
"Add to my calendar": "Ajouter à mon agenda",
|
"Add to my calendar": "Ajouter à mon agenda",
|
||||||
|
"Add": "Ajouter",
|
||||||
"Additional comments": "Commentaires additionnels",
|
"Additional comments": "Commentaires additionnels",
|
||||||
"Administration": "Administration",
|
"Administration": "Administration",
|
||||||
"All data will be deleted every 48 hours, so please don't use this for anything real.": "Toutes les données seront effacées toutes les 48 heures, donc n'utilisez pas ce site à des fins autres que de démonstration.",
|
"All data will be deleted every 48 hours, so please don't use this for anything real.": "Toutes les données seront effacées toutes les 48 heures, donc n'utilisez pas ce site à des fins autres que de démonstration.",
|
||||||
|
@ -25,28 +25,27 @@
|
||||||
"Avatar": "Avatar",
|
"Avatar": "Avatar",
|
||||||
"Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
|
"Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
|
||||||
"By {name}": "Par {name}",
|
"By {name}": "Par {name}",
|
||||||
"Cancel": "Annuler",
|
|
||||||
"Cancel creation": "Annuler la création",
|
"Cancel creation": "Annuler la création",
|
||||||
"Cancel edition": "Annuler l'édition",
|
"Cancel edition": "Annuler l'édition",
|
||||||
"Cancel my participation request…": "Annuler ma demande de participation…",
|
"Cancel my participation request…": "Annuler ma demande de participation…",
|
||||||
"Cancel my participation…": "Annuler ma participation…",
|
"Cancel my participation…": "Annuler ma participation…",
|
||||||
"Cancelled: Won't happen": "Annulé : N'aura pas lieu",
|
"Cancel": "Annuler",
|
||||||
|
"Cancelled: Won't happen": "Annulé : N'aura pas lieu",
|
||||||
"Category": "Catégorie",
|
"Category": "Catégorie",
|
||||||
"Change": "Modifier",
|
|
||||||
"Change my identity…": "Changer mon identité…",
|
"Change my identity…": "Changer mon identité…",
|
||||||
"Change my password": "Modifier mon mot de passe",
|
"Change my password": "Modifier mon mot de passe",
|
||||||
"Change password": "Modifier mot de passe",
|
"Change password": "Modifier mot de passe",
|
||||||
|
"Change": "Modifier",
|
||||||
"Clear": "Effacer",
|
"Clear": "Effacer",
|
||||||
"Click to select": "Cliquez pour sélectionner",
|
"Click to select": "Cliquez pour sélectionner",
|
||||||
"Click to upload": "Cliquez pour uploader",
|
"Click to upload": "Cliquez pour uploader",
|
||||||
"Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)",
|
"Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)",
|
||||||
"Comments": "Commentaires",
|
|
||||||
"Comments on the event page": "Commentaires sur la page de l'événement",
|
"Comments on the event page": "Commentaires sur la page de l'événement",
|
||||||
|
"Comments": "Commentaires",
|
||||||
"Confirm my particpation": "Confirmer ma participation",
|
"Confirm my particpation": "Confirmer ma participation",
|
||||||
"Confirmed: Will happen": "Confirmé : aura lieu",
|
"Confirmed: Will happen": "Confirmé : aura lieu",
|
||||||
"Continue editing": "Continuer l'édition",
|
"Continue editing": "Continuer l'édition",
|
||||||
"Country": "Pays",
|
"Country": "Pays",
|
||||||
"Create": "Créer",
|
|
||||||
"Create a new event": "Créer un nouvel événement",
|
"Create a new event": "Créer un nouvel événement",
|
||||||
"Create a new group": "Créer un nouveau groupe",
|
"Create a new group": "Créer un nouveau groupe",
|
||||||
"Create a new identity": "Créer une nouvelle identité",
|
"Create a new identity": "Créer une nouvelle identité",
|
||||||
|
@ -57,16 +56,17 @@
|
||||||
"Create my profile": "Créer mon profil",
|
"Create my profile": "Créer mon profil",
|
||||||
"Create token": "Créer un jeton",
|
"Create token": "Créer un jeton",
|
||||||
"Create, edit or delete events": "Créer, modifier ou supprimer des événements",
|
"Create, edit or delete events": "Créer, modifier ou supprimer des événements",
|
||||||
|
"Create": "Créer",
|
||||||
"Creator": "Créateur",
|
"Creator": "Créateur",
|
||||||
"Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet événement.",
|
"Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet événement.",
|
||||||
"Date and time settings": "Paramètres de date et d'heure",
|
"Date and time settings": "Paramètres de date et d'heure",
|
||||||
"Date parameters": "Paramètres de date",
|
"Date parameters": "Paramètres de date",
|
||||||
"Delete": "Supprimer",
|
|
||||||
"Delete event": "Supprimer un événement",
|
"Delete event": "Supprimer un événement",
|
||||||
"Delete this identity": "Supprimer cette identité",
|
"Delete this identity": "Supprimer cette identité",
|
||||||
"Delete your identity": "Supprimer votre identité",
|
"Delete your identity": "Supprimer votre identité",
|
||||||
"Delete {eventTitle}": "Supprimer {eventTitle}",
|
"Delete {eventTitle}": "Supprimer {eventTitle}",
|
||||||
"Delete {preferredUsername}": "Supprimer {preferredUsername}",
|
"Delete {preferredUsername}": "Supprimer {preferredUsername}",
|
||||||
|
"Delete": "Supprimer",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Didn't receive the instructions ?": "Vous n'avez pas reçu les instructions ?",
|
"Didn't receive the instructions ?": "Vous n'avez pas reçu les instructions ?",
|
||||||
"Display name": "Nom affiché",
|
"Display name": "Nom affiché",
|
||||||
|
@ -84,7 +84,6 @@
|
||||||
"Error while communicating with the server.": "Erreur de communication avec le serveur.",
|
"Error while communicating with the server.": "Erreur de communication avec le serveur.",
|
||||||
"Error while saving report.": "Erreur lors de l'enregistrement du signalement.",
|
"Error while saving report.": "Erreur lors de l'enregistrement du signalement.",
|
||||||
"Error while validating account": "Erreur lors de la validation du compte",
|
"Error while validating account": "Erreur lors de la validation du compte",
|
||||||
"Event": "Événement",
|
|
||||||
"Event already passed": "Événement déjà passé",
|
"Event already passed": "Événement déjà passé",
|
||||||
"Event cancelled": "Événement annulé",
|
"Event cancelled": "Événement annulé",
|
||||||
"Event creation": "Création d'événement",
|
"Event creation": "Création d'événement",
|
||||||
|
@ -95,6 +94,7 @@
|
||||||
"Event to be confirmed": "Événement à confirmer",
|
"Event to be confirmed": "Événement à confirmer",
|
||||||
"Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
|
"Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
|
||||||
"Event {eventTitle} reported": "Événement {eventTitle} signalé",
|
"Event {eventTitle} reported": "Événement {eventTitle} signalé",
|
||||||
|
"Event": "Événement",
|
||||||
"Events": "Événements",
|
"Events": "Événements",
|
||||||
"Exclude": "Exclure",
|
"Exclude": "Exclure",
|
||||||
"Explore": "Explorer",
|
"Explore": "Explorer",
|
||||||
|
@ -102,14 +102,15 @@
|
||||||
"Features": "Fonctionnalités",
|
"Features": "Fonctionnalités",
|
||||||
"Find an address": "Trouver une adresse",
|
"Find an address": "Trouver une adresse",
|
||||||
"Find an instance": "Trouver une instance",
|
"Find an instance": "Trouver une instance",
|
||||||
"For instance: London, Taekwondo, Architecture…": "Par exemple : Lyon, Taekwondo, Architecture…",
|
"For instance: London, Taekwondo, Architecture…": "Par exemple : Lyon, Taekwondo, Architecture…",
|
||||||
"Forgot your password ?": "Mot de passe oublié ?",
|
"Forgot your password ?": "Mot de passe oublié ?",
|
||||||
"From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants’ platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "De l’anniversaire entre ami·e·s à une marche pour le climat, aujourd’hui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment s’organiser, comment cliquer sur « je participe » sans <b>livrer des données intimes</b> à Facebook ou<b> s’enfermer</b> dans MeetUp ?",
|
"From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants’ platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "De l’anniversaire entre ami·e·s à une marche pour le climat, aujourd’hui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment s’organiser, comment cliquer sur « je participe » sans <b>livrer des données intimes</b> à Facebook ou<b> s’enfermer</b> dans MeetUp ?",
|
||||||
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
|
|
||||||
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
|
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
|
||||||
|
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
|
||||||
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
|
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
|
||||||
"Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser",
|
"Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser",
|
||||||
"General information": "Informations générales",
|
"General information": "Informations générales",
|
||||||
|
"Getting location": "Récupération de la position",
|
||||||
"Going as {name}": "En tant que {name}",
|
"Going as {name}": "En tant que {name}",
|
||||||
"Group List": "Liste de groupes",
|
"Group List": "Liste de groupes",
|
||||||
"Group full name": "Nom complet du groupe",
|
"Group full name": "Nom complet du groupe",
|
||||||
|
@ -131,8 +132,8 @@
|
||||||
"Join {instance}, a Mobilizon instance": "Rejoignez {instance}, une instance Mobilizon",
|
"Join {instance}, a Mobilizon instance": "Rejoignez {instance}, une instance Mobilizon",
|
||||||
"Last published event": "Dernier événement publié",
|
"Last published event": "Dernier événement publié",
|
||||||
"Last week": "La semaine dernière",
|
"Last week": "La semaine dernière",
|
||||||
"Learn more": "En apprendre plus",
|
|
||||||
"Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon",
|
"Learn more about Mobilizon": "En apprendre plus à propos de Mobilizon",
|
||||||
|
"Learn more": "En apprendre plus",
|
||||||
"Leave event": "Annuler ma participation à l'événement",
|
"Leave event": "Annuler ma participation à l'événement",
|
||||||
"Leaving event \"{title}\"": "Annuler ma participation à l'événement",
|
"Leaving event \"{title}\"": "Annuler ma participation à l'événement",
|
||||||
"Let's create a new common": "Créons un nouveau Common",
|
"Let's create a new common": "Créons un nouveau Common",
|
||||||
|
@ -142,8 +143,8 @@
|
||||||
"Locality": "Commune",
|
"Locality": "Commune",
|
||||||
"Log in": "Se connecter",
|
"Log in": "Se connecter",
|
||||||
"Log out": "Se déconnecter",
|
"Log out": "Se déconnecter",
|
||||||
"Login": "Se connecter",
|
|
||||||
"Login on Mobilizon!": "Se connecter sur Mobilizon !",
|
"Login on Mobilizon!": "Se connecter sur Mobilizon !",
|
||||||
|
"Login": "Se connecter",
|
||||||
"Manage participations": "Gérer les participations",
|
"Manage participations": "Gérer les participations",
|
||||||
"Members": "Membres",
|
"Members": "Membres",
|
||||||
"Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.": "Mobilizon est un logiciel libre qui permettra à des communautés de <b>créer leurs propres espaces</b> de publication d’événements, afin de mieux s’émanciper des géants du web.",
|
"Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.": "Mobilizon est un logiciel libre qui permettra à des communautés de <b>créer leurs propres espaces</b> de publication d’événements, afin de mieux s’émanciper des géants du web.",
|
||||||
|
@ -161,19 +162,20 @@
|
||||||
"No group found": "Aucun groupe trouvé",
|
"No group found": "Aucun groupe trouvé",
|
||||||
"No groups found": "Aucun groupe trouvé",
|
"No groups found": "Aucun groupe trouvé",
|
||||||
"No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
|
"No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
|
||||||
|
"No results for \"{queryText}\". You can try another search term or drag and drop the marker on the map": "Pas de résultats pour « {queryText} ». Vous pouvez essayer avec d'autres termes de recherche ou bien glisser et déposer le marqueur sur la carte",
|
||||||
"No user account with this email was found. Maybe you made a typo?": "Aucun compte utilisateur trouvé pour cet email. Peut-être avez-vous fait une faute de frappe ?",
|
"No user account with this email was found. Maybe you made a typo?": "Aucun compte utilisateur trouvé pour cet email. Peut-être avez-vous fait une faute de frappe ?",
|
||||||
"Number of places": "Nombre de places",
|
"Number of places": "Nombre de places",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Old password": "Ancien mot de passe",
|
"Old password": "Ancien mot de passe",
|
||||||
"On {date}": "Le {date}",
|
|
||||||
"On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}",
|
"On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}",
|
||||||
"On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
|
"On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
|
||||||
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
|
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
|
||||||
|
"On {date}": "Le {date}",
|
||||||
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
|
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
|
||||||
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
|
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
|
||||||
"Opened reports": "Signalements ouverts",
|
"Opened reports": "Signalements ouverts",
|
||||||
"Organized": "Organisés",
|
|
||||||
"Organized by {name}": "Organisé par {name}",
|
"Organized by {name}": "Organisé par {name}",
|
||||||
|
"Organized": "Organisés",
|
||||||
"Organizer": "Organisateur",
|
"Organizer": "Organisateur",
|
||||||
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
|
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
|
||||||
"Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
|
"Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
|
||||||
|
@ -184,10 +186,10 @@
|
||||||
"Participate": "Participer",
|
"Participate": "Participer",
|
||||||
"Participation approval": "Validation des participations",
|
"Participation approval": "Validation des participations",
|
||||||
"Participation requested!": "Participation demandée !",
|
"Participation requested!": "Participation demandée !",
|
||||||
"Password": "Mot de passe",
|
|
||||||
"Password (confirmation)": "Mot de passe (confirmation)",
|
"Password (confirmation)": "Mot de passe (confirmation)",
|
||||||
"Password change": "Changement de mot de passe",
|
"Password change": "Changement de mot de passe",
|
||||||
"Password reset": "Réinitialisation du mot de passe",
|
"Password reset": "Réinitialisation du mot de passe",
|
||||||
|
"Password": "Mot de passe",
|
||||||
"Past events": "Événements passés",
|
"Past events": "Événements passés",
|
||||||
"Pick an identity": "Choisissez une identité",
|
"Pick an identity": "Choisissez une identité",
|
||||||
"Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.",
|
"Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.",
|
||||||
|
@ -209,23 +211,23 @@
|
||||||
"RSS/Atom Feed": "Flux RSS/Atom",
|
"RSS/Atom Feed": "Flux RSS/Atom",
|
||||||
"Read Framasoft’s statement of intent on the Framablog": "Lire la note d’intention de Framasoft sur le Framablog",
|
"Read Framasoft’s statement of intent on the Framablog": "Lire la note d’intention de Framasoft sur le Framablog",
|
||||||
"Region": "Région",
|
"Region": "Région",
|
||||||
"Register": "S'inscrire",
|
|
||||||
"Register an account on Mobilizon!": "S'inscrire sur Mobilizon !",
|
"Register an account on Mobilizon!": "S'inscrire sur Mobilizon !",
|
||||||
"Register for an event by choosing one of your identities": "S'inscrire à un événement en choisissant une de vos identités",
|
"Register for an event by choosing one of your identities": "S'inscrire à un événement en choisissant une de vos identités",
|
||||||
|
"Register": "S'inscrire",
|
||||||
"Registration is currently closed.": "Les inscriptions sont actuellement fermées.",
|
"Registration is currently closed.": "Les inscriptions sont actuellement fermées.",
|
||||||
"Reject": "Rejetter",
|
"Reject": "Rejetter",
|
||||||
"Rejected": "Rejetés",
|
|
||||||
"Rejected participations": "Participations rejetées",
|
"Rejected participations": "Participations rejetées",
|
||||||
"Report": "Signaler",
|
"Rejected": "Rejetés",
|
||||||
"Report this event": "Signaler cet événement",
|
"Report this event": "Signaler cet événement",
|
||||||
|
"Report": "Signaler",
|
||||||
"Requests": "Requêtes",
|
"Requests": "Requêtes",
|
||||||
"Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
|
"Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
|
||||||
"Reset my password": "Réinitialiser mon mot de passe",
|
"Reset my password": "Réinitialiser mon mot de passe",
|
||||||
"Save": "Enregistrer",
|
|
||||||
"Save draft": "Enregistrer le brouillon",
|
"Save draft": "Enregistrer le brouillon",
|
||||||
"Search": "Rechercher",
|
"Save": "Enregistrer",
|
||||||
"Search events, groups, etc.": "Rechercher des événements, des groupes, etc.",
|
"Search events, groups, etc.": "Rechercher des événements, des groupes, etc.",
|
||||||
"Search results: \"{search}\"": "Résultats de recherche : « {search} »",
|
"Search results: \"{search}\"": "Résultats de recherche : « {search} »",
|
||||||
|
"Search": "Rechercher",
|
||||||
"Searching…": "Recherche en cours…",
|
"Searching…": "Recherche en cours…",
|
||||||
"Send me an email to reset my password": "Envoyez-moi un email pour réinitialiser mon mot de passe",
|
"Send me an email to reset my password": "Envoyez-moi un email pour réinitialiser mon mot de passe",
|
||||||
"Send me the confirmation email once again": "Envoyez-moi l'email de confirmation encore une fois",
|
"Send me the confirmation email once again": "Envoyez-moi l'email de confirmation encore une fois",
|
||||||
|
@ -246,8 +248,8 @@
|
||||||
"The draft event has been updated": "L'événement brouillon a été mis à jour",
|
"The draft event has been updated": "L'événement brouillon a été mis à jour",
|
||||||
"The event has been created as a draft": "L'événement a été créé en tant que brouillon",
|
"The event has been created as a draft": "L'événement a été créé en tant que brouillon",
|
||||||
"The event has been published": "L'événement a été publié",
|
"The event has been published": "L'événement a été publié",
|
||||||
"The event has been updated": "L'événement a été mis à jour",
|
|
||||||
"The event has been updated and published": "L'événement a été mis à jour et publié",
|
"The event has been updated and published": "L'événement a été mis à jour et publié",
|
||||||
|
"The event has been updated": "L'événement a été mis à jour",
|
||||||
"The event organizer didn't add any description.": "L'organisateur de l'événement n'a pas ajouté de description.",
|
"The event organizer didn't add any description.": "L'organisateur de l'événement n'a pas ajouté de description.",
|
||||||
"The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.",
|
"The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.",
|
||||||
"The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
|
"The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
|
||||||
|
@ -327,5 +329,6 @@
|
||||||
"{count} participants": "Aucun⋅e participant⋅e | Un⋅e participant⋅e | {count} participant⋅e⋅s",
|
"{count} participants": "Aucun⋅e participant⋅e | Un⋅e participant⋅e | {count} participant⋅e⋅s",
|
||||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
|
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
|
||||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines"
|
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines",
|
||||||
|
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
import poiIcons from '@/utils/poiIcons';
|
||||||
|
|
||||||
export interface IAddress {
|
export interface IAddress {
|
||||||
id?: number;
|
id?: string;
|
||||||
description: string;
|
description: string;
|
||||||
street: string;
|
street: string;
|
||||||
locality: string;
|
locality: string;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
region: string;
|
region: string;
|
||||||
country: string;
|
country: string;
|
||||||
|
type: string;
|
||||||
geom?: string;
|
geom?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
originId?: string;
|
originId?: string;
|
||||||
|
@ -18,4 +21,86 @@ export class Address implements IAddress {
|
||||||
postalCode: string = '';
|
postalCode: string = '';
|
||||||
region: string = '';
|
region: string = '';
|
||||||
street: string = '';
|
street: string = '';
|
||||||
|
type: string = '';
|
||||||
|
id?: string = '';
|
||||||
|
originId?: string = '';
|
||||||
|
url?: string = '';
|
||||||
|
geom?: string = '';
|
||||||
|
|
||||||
|
constructor(hash?) {
|
||||||
|
if (!hash) return;
|
||||||
|
|
||||||
|
this.id = hash.id;
|
||||||
|
this.description = hash.description;
|
||||||
|
this.street = hash.street;
|
||||||
|
this.locality = hash.locality;
|
||||||
|
this.postalCode = hash.postalCode;
|
||||||
|
this.region = hash.region;
|
||||||
|
this.country = hash.country;
|
||||||
|
this.type = hash.type;
|
||||||
|
this.geom = hash.geom;
|
||||||
|
this.url = hash.url;
|
||||||
|
this.originId = hash.originId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get poiInfos() {
|
||||||
|
/* generate name corresponding to poi type */
|
||||||
|
let name = '';
|
||||||
|
let alternativeName = '';
|
||||||
|
let poiIcon = poiIcons.default;
|
||||||
|
// Google Maps doesn't have a type
|
||||||
|
if (this.type == null && this.description === this.street) this.type = 'house';
|
||||||
|
|
||||||
|
switch (this.type) {
|
||||||
|
case 'house':
|
||||||
|
name = this.description;
|
||||||
|
alternativeName = [this.postalCode, this.locality, this.country].filter(zone => zone).join(', ');
|
||||||
|
poiIcon = poiIcons.defaultAddress;
|
||||||
|
break;
|
||||||
|
case 'street':
|
||||||
|
case 'secondary':
|
||||||
|
name = this.description;
|
||||||
|
alternativeName = [this.postalCode, this.locality, this.country].filter(zone => zone).join(', ');
|
||||||
|
poiIcon = poiIcons.defaultStreet;
|
||||||
|
break;
|
||||||
|
case 'zone':
|
||||||
|
case 'city':
|
||||||
|
case 'administrative':
|
||||||
|
name = this.postalCode ? `${this.description} (${this.postalCode})` : this.description;
|
||||||
|
alternativeName = [this.region, this.country].filter(zone => zone).join(', ');
|
||||||
|
poiIcon = poiIcons.defaultAdministrative;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// POI
|
||||||
|
name = this.description;
|
||||||
|
alternativeName = '';
|
||||||
|
if (this.street && this.street.trim()) {
|
||||||
|
alternativeName = `${this.street}`;
|
||||||
|
if (this.locality) {
|
||||||
|
alternativeName += ` (${this.locality})`;
|
||||||
|
}
|
||||||
|
} else if (this.locality && this.locality.trim()) {
|
||||||
|
alternativeName = `${this.locality}, ${this.region}, ${this.country}`;
|
||||||
|
} else {
|
||||||
|
alternativeName = `${this.region}, ${this.country}`;
|
||||||
|
}
|
||||||
|
poiIcon = this.iconForPOI;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { name, alternativeName, poiIcon };
|
||||||
|
}
|
||||||
|
|
||||||
|
get fullName() {
|
||||||
|
const { name, alternativeName } = this.poiInfos;
|
||||||
|
return `${name}, ${alternativeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get iconForPOI() {
|
||||||
|
if (this.type == null) {
|
||||||
|
return poiIcons.default;
|
||||||
|
}
|
||||||
|
const type = this.type.split(':').pop() || '';
|
||||||
|
if (poiIcons[type]) return poiIcons[type];
|
||||||
|
return poiIcons.default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,10 @@ export interface IConfig {
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
registrationsOpen: boolean;
|
registrationsOpen: boolean;
|
||||||
|
countryCode: string;
|
||||||
|
location: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
accuracyRadius: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Actor, IActor, IPerson } from './actor';
|
import { Actor, IActor, IPerson } from './actor';
|
||||||
import { IAddress } from '@/types/address.model';
|
import { Address, IAddress } from '@/types/address.model';
|
||||||
import { ITag } from '@/types/tag.model';
|
import { ITag } from '@/types/tag.model';
|
||||||
import { IPicture } from '@/types/picture.model';
|
import { IPicture } from '@/types/picture.model';
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ export class EventModel implements IEvent {
|
||||||
|
|
||||||
this.onlineAddress = hash.onlineAddress;
|
this.onlineAddress = hash.onlineAddress;
|
||||||
this.phoneAddress = hash.phoneAddress;
|
this.phoneAddress = hash.phoneAddress;
|
||||||
this.physicalAddress = hash.physicalAddress;
|
this.physicalAddress = new Address(hash.physicalAddress);
|
||||||
this.participantStats = hash.participantStats;
|
this.participantStats = hash.participantStats;
|
||||||
|
|
||||||
this.tags = hash.tags;
|
this.tags = hash.tags;
|
||||||
|
|
22
js/src/utils/.editorconfig
Normal file
22
js/src/utils/.editorconfig
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
tab_width = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ex]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
|
||||||
|
[*.scss]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
61
js/src/utils/poiIcons.ts
Normal file
61
js/src/utils/poiIcons.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
export default {
|
||||||
|
default: {
|
||||||
|
icon: 'map-marker',
|
||||||
|
color: '#5C6F84',
|
||||||
|
},
|
||||||
|
defaultAdministrative: {
|
||||||
|
icon: 'city',
|
||||||
|
color: '#5c6f84',
|
||||||
|
},
|
||||||
|
defaultStreet: {
|
||||||
|
icon: 'road-variant',
|
||||||
|
color: '#5c6f84',
|
||||||
|
},
|
||||||
|
defaultAddress: {
|
||||||
|
icon: 'home',
|
||||||
|
color: '#5c6f84',
|
||||||
|
},
|
||||||
|
place_house: {
|
||||||
|
icon: 'home',
|
||||||
|
color: '#5c6f84',
|
||||||
|
},
|
||||||
|
theatre: {
|
||||||
|
icon: 'drama-masks',
|
||||||
|
},
|
||||||
|
parking: {
|
||||||
|
icon: 'parking',
|
||||||
|
},
|
||||||
|
police: {
|
||||||
|
icon: 'police-badge',
|
||||||
|
},
|
||||||
|
post_office: {
|
||||||
|
icon: 'email',
|
||||||
|
},
|
||||||
|
university: {
|
||||||
|
icon: 'school',
|
||||||
|
},
|
||||||
|
college: {
|
||||||
|
icon: 'school',
|
||||||
|
},
|
||||||
|
park: {
|
||||||
|
icon: 'pine-tree',
|
||||||
|
},
|
||||||
|
garden: {
|
||||||
|
icon: 'pine-tree',
|
||||||
|
},
|
||||||
|
bicycle_rental: {
|
||||||
|
icon: 'bicycle',
|
||||||
|
},
|
||||||
|
hospital: {
|
||||||
|
icon: 'hospital-box',
|
||||||
|
},
|
||||||
|
townhall: {
|
||||||
|
icon: 'office-building',
|
||||||
|
},
|
||||||
|
toilets: {
|
||||||
|
icon: 'human-male-female',
|
||||||
|
},
|
||||||
|
hairdresser: {
|
||||||
|
icon: 'content-cut',
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,4 +1,3 @@
|
||||||
import {ParticipantRole} from "@/types/event.model";
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||||
|
@ -15,7 +14,7 @@ import {ParticipantRole} from "@/types/event.model";
|
||||||
<div class="title-and-informations">
|
<div class="title-and-informations">
|
||||||
<h1 class="title">{{ event.title }}</h1>
|
<h1 class="title">{{ event.title }}</h1>
|
||||||
<span>
|
<span>
|
||||||
<router-link v-if="actorIsOrganizer" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
|
<router-link v-if="actorIsOrganizer && event.draft === false" :to="{ name: RouteName.PARTICIPATIONS, params: {eventId: event.uuid}}">
|
||||||
<small v-if="event.participantStats.going > 0 && !actorIsParticipant">
|
<small v-if="event.participantStats.going > 0 && !actorIsParticipant">
|
||||||
{{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
|
{{ $tc('One person is going', event.participantStats.going, {approved: event.participantStats.going}) }}
|
||||||
</small>
|
</small>
|
||||||
|
@ -111,23 +110,27 @@ import {ParticipantRole} from "@/types/event.model";
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="address-wrapper">
|
<div class="address-wrapper">
|
||||||
|
<span v-if="!physicalAddress">
|
||||||
<b-icon icon="map" />
|
<b-icon icon="map" />
|
||||||
<span v-if="!event.physicalAddress">{{ $t('No address defined') }}</span>
|
{{ $t('No address defined') }}
|
||||||
<div class="address" v-if="event.physicalAddress">
|
</span>
|
||||||
|
<div class="address" v-if="physicalAddress">
|
||||||
|
<span>
|
||||||
|
<b-icon :icon="physicalAddress.poiInfos.poiIcon.icon" />
|
||||||
<address>
|
<address>
|
||||||
<span class="addressDescription" :title="event.physicalAddress.description">{{ event.physicalAddress.description }}</span>
|
<span class="addressDescription" :title="physicalAddress.poiInfos.name">{{ physicalAddress.poiInfos.name }}</span>
|
||||||
<span>{{ event.physicalAddress.street }}</span>
|
<span>{{ physicalAddress.poiInfos.alternativeName }}</span>
|
||||||
<span>{{ event.physicalAddress.postalCode }} {{ event.physicalAddress.locality }}</span>
|
|
||||||
</address>
|
</address>
|
||||||
<span class="map-show-button" @click="showMap = !showMap" v-if="event.physicalAddress && event.physicalAddress.geom">
|
</span>
|
||||||
|
<span class="map-show-button" @click="showMap = !showMap" v-if="physicalAddress && physicalAddress.geom">
|
||||||
{{ $t('Show map') }}
|
{{ $t('Show map') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<b-modal v-if="event.physicalAddress && event.physicalAddress.geom" :active.sync="showMap" scroll="keep">
|
<b-modal v-if="physicalAddress && physicalAddress.geom" :active.sync="showMap" scroll="keep">
|
||||||
<div class="map">
|
<div class="map">
|
||||||
<map-leaflet
|
<map-leaflet
|
||||||
:coords="event.physicalAddress.geom"
|
:coords="physicalAddress.geom"
|
||||||
:popup="event.physicalAddress.description"
|
:marker="{ text: physicalAddress.fullName, icon: physicalAddress.poiInfos.poiIcon.icon }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
@ -254,7 +257,7 @@ import IdentityPicker from '@/views/Account/IdentityPicker.vue';
|
||||||
import ParticipationButton from '@/components/Event/ParticipationButton.vue';
|
import ParticipationButton from '@/components/Event/ParticipationButton.vue';
|
||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
import { RouteName } from '@/router';
|
import { RouteName } from '@/router';
|
||||||
import HTML = Mocha.reporters.HTML;
|
import { Address } from '@/types/address.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -596,11 +599,13 @@ export default class Event extends EventMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
get eventCapacityOK(): boolean {
|
get eventCapacityOK(): boolean {
|
||||||
|
if (this.event.draft) return true;
|
||||||
if (!this.event.options.maximumAttendeeCapacity) return true;
|
if (!this.event.options.maximumAttendeeCapacity) return true;
|
||||||
return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participant;
|
return this.event.options.maximumAttendeeCapacity > this.event.participantStats.participant;
|
||||||
}
|
}
|
||||||
|
|
||||||
get numberOfPlacesStillAvailable(): number {
|
get numberOfPlacesStillAvailable(): number {
|
||||||
|
if (this.event.draft) return this.event.options.maximumAttendeeCapacity;
|
||||||
return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participant;
|
return this.event.options.maximumAttendeeCapacity - this.event.participantStats.participant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -611,6 +616,11 @@ export default class Event extends EventMixin {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get physicalAddress(): Address|null {
|
||||||
|
if (!this.event.physicalAddress) return null;
|
||||||
|
return new Address(this.event.physicalAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -664,6 +674,13 @@ export default class Event extends EventMixin {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span:first-child {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
span.icon {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
address {
|
address {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -686,6 +703,7 @@ export default class Event extends EventMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div.map {
|
div.map {
|
||||||
height: 900px;
|
height: 900px;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import { ApolloLink, Observable } from 'apollo-link';
|
import { ApolloLink, Observable } from 'apollo-link';
|
||||||
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
|
import { defaultDataIdFromObject, InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
|
||||||
import { onError } from 'apollo-link-error';
|
import { onError } from 'apollo-link-error';
|
||||||
import { createLink } from 'apollo-absinthe-upload-link';
|
import { createLink } from 'apollo-absinthe-upload-link';
|
||||||
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
|
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
|
||||||
|
@ -132,6 +132,13 @@ const link = authMiddleware
|
||||||
|
|
||||||
const cache = new InMemoryCache({
|
const cache = new InMemoryCache({
|
||||||
fragmentMatcher,
|
fragmentMatcher,
|
||||||
|
dataIdFromObject: object => {
|
||||||
|
if (object.__typename === 'Address') {
|
||||||
|
// @ts-ignore
|
||||||
|
return object.origin_id;
|
||||||
|
}
|
||||||
|
return defaultDataIdFromObject(object);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
const apolloClient = new ApolloClient({
|
||||||
|
|
14
js/yarn.lock
14
js/yarn.lock
|
@ -927,7 +927,14 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
|
||||||
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
|
integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==
|
||||||
|
|
||||||
"@types/leaflet@^1.5.2":
|
"@types/leaflet.locatecontrol@^0.60.7":
|
||||||
|
version "0.60.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/leaflet.locatecontrol/-/leaflet.locatecontrol-0.60.7.tgz#96d258bf27376b53bb4b3e9276a14e38f270215b"
|
||||||
|
integrity sha512-sac/MeK4gB+3XTJ3JzCe3HqLwKNHblIpZrxUJ6FapWK8uISZ0wcy8motVO7+v/yO47tQgsnYaobwFZ//beWHBQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/leaflet" "*"
|
||||||
|
|
||||||
|
"@types/leaflet@*", "@types/leaflet@^1.5.2":
|
||||||
version "1.5.5"
|
version "1.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.5.tgz#006c0aa89c4b5e62941717fa71a09e846423536c"
|
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.5.tgz#006c0aa89c4b5e62941717fa71a09e846423536c"
|
||||||
integrity sha512-Eyh1LMmW4OFgafL6rjLyGkMqFS5IzgwWHMSgTKbrsvwLjLaWH8Ae8CV5liRe8HSM731oOVDwAMIZgg9P0SO9tg==
|
integrity sha512-Eyh1LMmW4OFgafL6rjLyGkMqFS5IzgwWHMSgTKbrsvwLjLaWH8Ae8CV5liRe8HSM731oOVDwAMIZgg9P0SO9tg==
|
||||||
|
@ -7404,6 +7411,11 @@ lcid@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
invert-kv "^2.0.0"
|
invert-kv "^2.0.0"
|
||||||
|
|
||||||
|
leaflet.locatecontrol@^0.68.0:
|
||||||
|
version "0.68.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/leaflet.locatecontrol/-/leaflet.locatecontrol-0.68.0.tgz#fc0d173ef0f6670af192641e5a448f0c58c814d3"
|
||||||
|
integrity sha512-jXJCpBvkyH6shjPEOK/DWu/tKX/WdkNeO96jyPrnGelYp9u6wSDj4V1V4aX9+CMTIrEyVB4/4XuU+T7VTRpb6w==
|
||||||
|
|
||||||
leaflet@^1.4.0:
|
leaflet@^1.4.0:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.5.1.tgz#9afb9d963d66c870066b1342e7a06f92840f46bf"
|
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.5.1.tgz#9afb9d963d66c870066b1342e7a06f92840f46bf"
|
||||||
|
|
|
@ -17,6 +17,7 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
geom: Geo.PostGIS.Geometry.t(),
|
geom: Geo.PostGIS.Geometry.t(),
|
||||||
postal_code: String.t(),
|
postal_code: String.t(),
|
||||||
street: String.t(),
|
street: String.t(),
|
||||||
|
type: String.t(),
|
||||||
url: String.t(),
|
url: String.t(),
|
||||||
origin_id: String.t(),
|
origin_id: String.t(),
|
||||||
events: [Event.t()]
|
events: [Event.t()]
|
||||||
|
@ -31,7 +32,8 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
:region,
|
:region,
|
||||||
:postal_code,
|
:postal_code,
|
||||||
:street,
|
:street,
|
||||||
:origin_id
|
:origin_id,
|
||||||
|
:type
|
||||||
]
|
]
|
||||||
@attrs @required_attrs ++ @optional_attrs
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
field(:geom, Geo.PostGIS.Geometry)
|
field(:geom, Geo.PostGIS.Geometry)
|
||||||
field(:postal_code, :string)
|
field(:postal_code, :string)
|
||||||
field(:street, :string)
|
field(:street, :string)
|
||||||
|
field(:type, :string)
|
||||||
field(:url, :string)
|
field(:url, :string)
|
||||||
field(:origin_id, :string)
|
field(:origin_id, :string)
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
alias Mobilizon.Media
|
alias Mobilizon.Media
|
||||||
alias Mobilizon.Media.Picture
|
alias Mobilizon.Media.Picture
|
||||||
alias Mobilizon.Mention
|
alias Mobilizon.Mention
|
||||||
|
alias Mobilizon.Storage.Repo
|
||||||
|
|
||||||
alias MobilizonWeb.Endpoint
|
alias MobilizonWeb.Endpoint
|
||||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||||
|
@ -105,7 +106,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
|
embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
|
||||||
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
||||||
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
||||||
belongs_to(:physical_address, Address, on_replace: :update)
|
belongs_to(:physical_address, Address, on_replace: :nilify)
|
||||||
belongs_to(:picture, Picture, on_replace: :update)
|
belongs_to(:picture, Picture, on_replace: :update)
|
||||||
has_many(:tracks, Track)
|
has_many(:tracks, Track)
|
||||||
has_many(:sessions, Session)
|
has_many(:sessions, Session)
|
||||||
|
@ -194,11 +195,23 @@ defmodule Mobilizon.Events.Event do
|
||||||
put_assoc(changeset, :physical_address, address)
|
put_assoc(changeset, :physical_address, address)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
changeset
|
cast_assoc(changeset, :physical_address)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# In case it's a new address
|
# In case it's a new address but the origin_id is an existing one
|
||||||
|
defp put_address(%Changeset{} = changeset, %{physical_address: %{origin_id: origin_id}})
|
||||||
|
when not is_nil(origin_id) do
|
||||||
|
case Repo.get_by(Address, origin_id: origin_id) do
|
||||||
|
%Address{} = address ->
|
||||||
|
put_assoc(changeset, :physical_address, address)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
cast_assoc(changeset, :physical_address)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# In case it's a new address without any origin_id (manual)
|
||||||
defp put_address(%Changeset{} = changeset, _attrs) do
|
defp put_address(%Changeset{} = changeset, _attrs) do
|
||||||
cast_assoc(changeset, :physical_address)
|
cast_assoc(changeset, :physical_address)
|
||||||
end
|
end
|
||||||
|
@ -225,7 +238,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
%Changeset{changes: %{draft: true}} = changeset,
|
%Changeset{changes: %{draft: true}} = changeset,
|
||||||
_action
|
_action
|
||||||
) do
|
) do
|
||||||
cast_embed(changeset, :participant_stats)
|
put_embed(changeset, :participant_stats, %{creator: 0})
|
||||||
end
|
end
|
||||||
|
|
||||||
# Created with any other value: publish
|
# Created with any other value: publish
|
||||||
|
|
|
@ -3,7 +3,6 @@ defmodule MobilizonWeb.Resolvers.Address do
|
||||||
Handles the comment-related GraphQL calls
|
Handles the comment-related GraphQL calls
|
||||||
"""
|
"""
|
||||||
require Logger
|
require Logger
|
||||||
alias Mobilizon.Addresses
|
|
||||||
alias Mobilizon.Addresses.Address
|
alias Mobilizon.Addresses.Address
|
||||||
alias Mobilizon.Service.Geospatial
|
alias Mobilizon.Service.Geospatial
|
||||||
|
|
||||||
|
@ -11,26 +10,18 @@ defmodule MobilizonWeb.Resolvers.Address do
|
||||||
Search an address
|
Search an address
|
||||||
"""
|
"""
|
||||||
@spec search(map(), map(), map()) :: {:ok, list(Address.t())}
|
@spec search(map(), map(), map()) :: {:ok, list(Address.t())}
|
||||||
def search(_parent, %{query: query, page: _page, limit: _limit}, %{context: %{ip: ip}}) do
|
def search(_parent, %{query: query, locale: locale, page: _page, limit: _limit}, %{
|
||||||
country = ip |> Geolix.lookup() |> Map.get(:country, nil)
|
context: %{ip: ip}
|
||||||
|
}) do
|
||||||
|
geolix = Geolix.lookup(ip)
|
||||||
|
|
||||||
local_addresses = Task.async(fn -> Addresses.search_addresses(query, country: country) end)
|
country_code =
|
||||||
|
case geolix do
|
||||||
remote_addresses = Task.async(fn -> Geospatial.service().search(query) end)
|
%{country: %{iso_code: country_code}} -> String.downcase(country_code)
|
||||||
|
_ -> nil
|
||||||
addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
|
|
||||||
|
|
||||||
# If we have results with same origin_id than those locally saved, don't return them
|
|
||||||
addresses =
|
|
||||||
Enum.reduce(addresses, %{}, fn address, addresses ->
|
|
||||||
if Map.has_key?(addresses, address.origin_id) && !is_nil(address.url) do
|
|
||||||
addresses
|
|
||||||
else
|
|
||||||
Map.put(addresses, address.origin_id, address)
|
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
addresses = Map.values(addresses)
|
addresses = Geospatial.service().search(query, lang: locale, country_code: country_code)
|
||||||
|
|
||||||
{:ok, addresses}
|
{:ok, addresses}
|
||||||
end
|
end
|
||||||
|
@ -39,15 +30,12 @@ defmodule MobilizonWeb.Resolvers.Address do
|
||||||
Reverse geocode some coordinates
|
Reverse geocode some coordinates
|
||||||
"""
|
"""
|
||||||
@spec reverse_geocode(map(), map(), map()) :: {:ok, list(Address.t())}
|
@spec reverse_geocode(map(), map(), map()) :: {:ok, list(Address.t())}
|
||||||
def reverse_geocode(_parent, %{longitude: longitude, latitude: latitude}, %{context: %{ip: ip}}) do
|
def reverse_geocode(
|
||||||
country = ip |> Geolix.lookup() |> Map.get(:country, nil)
|
_parent,
|
||||||
|
%{longitude: longitude, latitude: latitude, zoom: zoom, locale: locale},
|
||||||
local_addresses =
|
_context
|
||||||
Task.async(fn -> Addresses.reverse_geocode(longitude, latitude, country: country) end)
|
) do
|
||||||
|
addresses = Geospatial.service().geocode(longitude, latitude, lang: locale, zoom: zoom)
|
||||||
remote_addresses = Task.async(fn -> Geospatial.service().geocode(longitude, latitude) end)
|
|
||||||
|
|
||||||
addresses = Task.await(local_addresses) ++ Task.await(remote_addresses)
|
|
||||||
|
|
||||||
{:ok, addresses}
|
{:ok, addresses}
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,16 +4,35 @@ defmodule MobilizonWeb.Resolvers.Config do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Mobilizon.Config
|
alias Mobilizon.Config
|
||||||
|
alias Geolix.Adapter.MMDB2.Record.{Country, Location}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets config.
|
Gets config.
|
||||||
"""
|
"""
|
||||||
def get_config(_parent, _params, _context) do
|
def get_config(_parent, _params, %{
|
||||||
|
context: %{ip: ip}
|
||||||
|
}) do
|
||||||
|
geolix = Geolix.lookup(ip)
|
||||||
|
|
||||||
|
country_code =
|
||||||
|
case geolix.city do
|
||||||
|
%{country: %Country{iso_code: country_code}} -> String.downcase(country_code)
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
location =
|
||||||
|
case geolix.city do
|
||||||
|
%{location: %Location{} = location} -> Map.from_struct(location)
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
name: Config.instance_name(),
|
name: Config.instance_name(),
|
||||||
registrations_open: Config.instance_registrations_open?(),
|
registrations_open: Config.instance_registrations_open?(),
|
||||||
description: Config.instance_description()
|
description: Config.instance_description(),
|
||||||
|
location: location,
|
||||||
|
country_code: country_code
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,7 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||||
field(:region, :string)
|
field(:region, :string)
|
||||||
field(:country, :string)
|
field(:country, :string)
|
||||||
field(:description, :string)
|
field(:description, :string)
|
||||||
|
field(:type, :string)
|
||||||
field(:url, :string)
|
field(:url, :string)
|
||||||
field(:id, :id)
|
field(:id, :id)
|
||||||
field(:origin_id, :string)
|
field(:origin_id, :string)
|
||||||
|
@ -38,6 +39,7 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||||
field(:country, :string)
|
field(:country, :string)
|
||||||
field(:description, :string)
|
field(:description, :string)
|
||||||
field(:url, :string)
|
field(:url, :string)
|
||||||
|
field(:type, :string)
|
||||||
field(:id, :id)
|
field(:id, :id)
|
||||||
field(:origin_id, :string)
|
field(:origin_id, :string)
|
||||||
end
|
end
|
||||||
|
@ -46,6 +48,7 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||||
@desc "Search for an address"
|
@desc "Search for an address"
|
||||||
field :search_address, type: list_of(:address) do
|
field :search_address, type: list_of(:address) do
|
||||||
arg(:query, non_null(:string))
|
arg(:query, non_null(:string))
|
||||||
|
arg(:locale, :string, default_value: "en")
|
||||||
arg(:page, :integer, default_value: 1)
|
arg(:page, :integer, default_value: 1)
|
||||||
arg(:limit, :integer, default_value: 10)
|
arg(:limit, :integer, default_value: 10)
|
||||||
|
|
||||||
|
@ -56,6 +59,8 @@ defmodule MobilizonWeb.Schema.AddressType do
|
||||||
field :reverse_geocode, type: list_of(:address) do
|
field :reverse_geocode, type: list_of(:address) do
|
||||||
arg(:longitude, non_null(:float))
|
arg(:longitude, non_null(:float))
|
||||||
arg(:latitude, non_null(:float))
|
arg(:latitude, non_null(:float))
|
||||||
|
arg(:zoom, :integer, default_value: 15)
|
||||||
|
arg(:locale, :string, default_value: "en")
|
||||||
|
|
||||||
resolve(&Resolvers.Address.reverse_geocode/3)
|
resolve(&Resolvers.Address.reverse_geocode/3)
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,14 @@ defmodule MobilizonWeb.Schema.ConfigType do
|
||||||
field(:description, :string)
|
field(:description, :string)
|
||||||
|
|
||||||
field(:registrations_open, :boolean)
|
field(:registrations_open, :boolean)
|
||||||
|
field(:country_code, :string)
|
||||||
|
field(:location, :lonlat)
|
||||||
|
end
|
||||||
|
|
||||||
|
object :lonlat do
|
||||||
|
field(:longitude, :float)
|
||||||
|
field(:latitude, :float)
|
||||||
|
field(:accuracy_radius, :integer)
|
||||||
end
|
end
|
||||||
|
|
||||||
object :config_queries do
|
object :config_queries do
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Google Maps [Geocoding service](https://developers.google.com/maps/documentation/geocoding/intro).
|
Google Maps [Geocoding service](https://developers.google.com/maps/documentation/geocoding/intro). Only works with addresses.
|
||||||
|
|
||||||
Note: Endpoint is hardcoded to Google Maps API.
|
Note: Endpoint is hardcoded to Google Maps API.
|
||||||
"""
|
"""
|
||||||
|
@ -89,7 +89,11 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||||
url <> "&address=#{args.q}"
|
url <> "&address=#{args.q}"
|
||||||
|
|
||||||
:geocode ->
|
:geocode ->
|
||||||
url <> "&latlng=#{args.lat},#{args.lon}&result_type=street_address"
|
zoom = Keyword.get(options, :zoom, 15)
|
||||||
|
|
||||||
|
result_type = if zoom >= 15, do: "street_address", else: "locality"
|
||||||
|
|
||||||
|
url <> "&latlng=#{args.lat},#{args.lon}&result_type=#{result_type}"
|
||||||
|
|
||||||
:place_details ->
|
:place_details ->
|
||||||
"https://maps.googleapis.com/maps/api/place/details/json?key=#{api_key}&placeid=#{
|
"https://maps.googleapis.com/maps/api/place/details/json?key=#{api_key}&placeid=#{
|
||||||
|
|
146
lib/service/geospatial/mimirsbrunn.ex
Normal file
146
lib/service/geospatial/mimirsbrunn.ex
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
|
||||||
|
@moduledoc """
|
||||||
|
[Mimirsbrunn](https://github.com/CanalTP/mimirsbrunn) backend.
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
* Has trouble finding POIs.
|
||||||
|
* Doesn't support zoom level for reverse geocoding
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Mobilizon.Addresses.Address
|
||||||
|
alias Mobilizon.Service.Geospatial.Provider
|
||||||
|
alias Mobilizon.Config
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@behaviour Provider
|
||||||
|
|
||||||
|
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||||
|
"""
|
||||||
|
@spec geocode(number(), number(), keyword()) :: list(Address.t())
|
||||||
|
def geocode(lon, lat, options \\ []) do
|
||||||
|
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||||
|
headers = [{"User-Agent", user_agent}]
|
||||||
|
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||||
|
Logger.debug("Asking Mimirsbrunn for reverse geocoding with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url, headers),
|
||||||
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
|
process_data(features)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl Provider
|
||||||
|
@doc """
|
||||||
|
Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.search/2`.
|
||||||
|
"""
|
||||||
|
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||||
|
def search(q, options \\ []) do
|
||||||
|
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||||
|
headers = [{"User-Agent", user_agent}]
|
||||||
|
url = build_url(:search, %{q: q}, options)
|
||||||
|
Logger.debug("Asking Mimirsbrunn for addresses with #{url}")
|
||||||
|
|
||||||
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
|
HTTPoison.get(url, headers),
|
||||||
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
|
process_data(features)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec build_url(atom(), map(), list()) :: String.t()
|
||||||
|
defp build_url(method, args, options) do
|
||||||
|
limit = Keyword.get(options, :limit, 10)
|
||||||
|
lang = Keyword.get(options, :lang, "en")
|
||||||
|
coords = Keyword.get(options, :coords, nil)
|
||||||
|
endpoint = Keyword.get(options, :endpoint, @endpoint)
|
||||||
|
|
||||||
|
case method do
|
||||||
|
:search ->
|
||||||
|
url = "#{endpoint}/autocomplete?q=#{URI.encode(args.q)}&lang=#{lang}&limit=#{limit}"
|
||||||
|
if is_nil(coords), do: url, else: url <> "&lat=#{coords.lat}&lon=#{coords.lon}"
|
||||||
|
|
||||||
|
:geocode ->
|
||||||
|
"#{endpoint}/reverse?lon=#{args.lon}&lat=#{args.lat}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_data(features) do
|
||||||
|
features
|
||||||
|
|> Enum.map(fn %{
|
||||||
|
"geometry" => %{"coordinates" => coordinates},
|
||||||
|
"properties" => %{"geocoding" => geocoding}
|
||||||
|
} ->
|
||||||
|
address = process_address(geocoding)
|
||||||
|
%Address{address | geom: Provider.coordinates(coordinates)}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_address(%{"type" => "poi", "address" => address} = geocoding) do
|
||||||
|
address = process_address(address)
|
||||||
|
|
||||||
|
%Address{
|
||||||
|
address
|
||||||
|
| type: get_type(geocoding),
|
||||||
|
origin_id: Map.get(geocoding, "id"),
|
||||||
|
description: Map.get(geocoding, "name")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_address(geocoding) do
|
||||||
|
%Address{
|
||||||
|
country: get_administrative_region(geocoding, "country"),
|
||||||
|
locality: Map.get(geocoding, "city"),
|
||||||
|
region: get_administrative_region(geocoding, "region"),
|
||||||
|
description: Map.get(geocoding, "name"),
|
||||||
|
postal_code: get_postal_code(geocoding),
|
||||||
|
street: street_address(geocoding),
|
||||||
|
origin_id: "mimirsbrunn:" <> Map.get(geocoding, "id"),
|
||||||
|
type: get_type(geocoding)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp street_address(properties) do
|
||||||
|
if Map.has_key?(properties, "housenumber") do
|
||||||
|
Map.get(properties, "housenumber") <> " " <> Map.get(properties, "street")
|
||||||
|
else
|
||||||
|
Map.get(properties, "street")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_type(%{"type" => type}) when type in ["house", "street", "zone", "address"], do: type
|
||||||
|
|
||||||
|
defp get_type(%{"type" => "poi", "poi_types" => types})
|
||||||
|
when is_list(types) and length(types) > 0,
|
||||||
|
do: hd(types)["id"]
|
||||||
|
|
||||||
|
defp get_type(_), do: nil
|
||||||
|
|
||||||
|
defp get_administrative_region(
|
||||||
|
%{"administrative_regions" => administrative_regions},
|
||||||
|
administrative_level
|
||||||
|
) do
|
||||||
|
Enum.find_value(
|
||||||
|
administrative_regions,
|
||||||
|
&process_administrative_region(&1, administrative_level)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_administrative_region(_, _), do: nil
|
||||||
|
|
||||||
|
defp process_administrative_region(%{"zone_type" => "country", "name" => name}, "country"),
|
||||||
|
do: name
|
||||||
|
|
||||||
|
defp process_administrative_region(%{"zone_type" => "state", "name" => name}, "region"),
|
||||||
|
do: name
|
||||||
|
|
||||||
|
defp process_administrative_region(_, _), do: nil
|
||||||
|
|
||||||
|
defp get_postal_code(%{"postcode" => nil}), do: nil
|
||||||
|
defp get_postal_code(%{"postcode" => postcode}), do: postcode |> String.split(";") |> hd()
|
||||||
|
end
|
|
@ -27,8 +27,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
|
|
||||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
HTTPoison.get(url, headers),
|
HTTPoison.get(url, headers),
|
||||||
{:ok, body} <- Poison.decode(body) do
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
[process_data(body)]
|
features |> process_data() |> Enum.filter(& &1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
|
|
||||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||||
HTTPoison.get(url, headers),
|
HTTPoison.get(url, headers),
|
||||||
{:ok, body} <- Poison.decode(body) do
|
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||||
body |> Enum.map(fn entry -> process_data(entry) end) |> Enum.filter(& &1)
|
features |> process_data() |> Enum.filter(& &1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,39 +55,53 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
limit = Keyword.get(options, :limit, 10)
|
limit = Keyword.get(options, :limit, 10)
|
||||||
lang = Keyword.get(options, :lang, "en")
|
lang = Keyword.get(options, :lang, "en")
|
||||||
endpoint = Keyword.get(options, :endpoint, @endpoint)
|
endpoint = Keyword.get(options, :endpoint, @endpoint)
|
||||||
|
country_code = Keyword.get(options, :country_code)
|
||||||
|
zoom = Keyword.get(options, :zoom)
|
||||||
api_key = Keyword.get(options, :api_key, @api_key)
|
api_key = Keyword.get(options, :api_key, @api_key)
|
||||||
|
|
||||||
url =
|
url =
|
||||||
case method do
|
case method do
|
||||||
:search ->
|
:search ->
|
||||||
"#{endpoint}/search?format=jsonv2&q=#{URI.encode(args.q)}&limit=#{limit}&accept-language=#{
|
"#{endpoint}/search?format=geocodejson&q=#{URI.encode(args.q)}&limit=#{limit}&accept-language=#{
|
||||||
lang
|
lang
|
||||||
}&addressdetails=1"
|
}&addressdetails=1&namedetails=1"
|
||||||
|
|
||||||
:geocode ->
|
:geocode ->
|
||||||
"#{endpoint}/reverse?format=jsonv2&lat=#{args.lat}&lon=#{args.lon}&addressdetails=1"
|
url =
|
||||||
|
"#{endpoint}/reverse?format=geocodejson&lat=#{args.lat}&lon=#{args.lon}&accept-language=#{
|
||||||
|
lang
|
||||||
|
}&addressdetails=1&namedetails=1"
|
||||||
|
|
||||||
|
if is_nil(zoom), do: url, else: url <> "&zoom=#{zoom}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
url = if is_nil(country_code), do: url, else: "#{url}&countrycodes=#{country_code}"
|
||||||
if is_nil(api_key), do: url, else: url <> "&key=#{api_key}"
|
if is_nil(api_key), do: url, else: url <> "&key=#{api_key}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec process_data(map()) :: Address.t()
|
defp process_data(features) do
|
||||||
defp process_data(%{"address" => address} = body) do
|
features
|
||||||
%Address{
|
|> Enum.map(fn %{
|
||||||
country: Map.get(address, "country"),
|
"geometry" => %{"coordinates" => coordinates},
|
||||||
locality: Map.get(address, "city"),
|
"properties" => %{"geocoding" => geocoding}
|
||||||
region: Map.get(address, "state"),
|
} ->
|
||||||
description: description(body),
|
address = process_address(geocoding)
|
||||||
geom: [Map.get(body, "lon"), Map.get(body, "lat")] |> Provider.coordinates(),
|
%Address{address | geom: Provider.coordinates(coordinates)}
|
||||||
postal_code: Map.get(address, "postcode"),
|
end)
|
||||||
street: street_address(address),
|
end
|
||||||
origin_id: "osm:" <> to_string(Map.get(body, "osm_id"))
|
|
||||||
}
|
|
||||||
rescue
|
|
||||||
error in ArgumentError ->
|
|
||||||
Logger.warn(inspect(error))
|
|
||||||
|
|
||||||
nil
|
defp process_address(geocoding) do
|
||||||
|
%Address{
|
||||||
|
country: Map.get(geocoding, "country"),
|
||||||
|
locality:
|
||||||
|
Map.get(geocoding, "city") || Map.get(geocoding, "town") || Map.get(geocoding, "county"),
|
||||||
|
region: Map.get(geocoding, "state"),
|
||||||
|
description: description(geocoding),
|
||||||
|
postal_code: Map.get(geocoding, "postcode"),
|
||||||
|
type: Map.get(geocoding, "type"),
|
||||||
|
street: street_address(geocoding),
|
||||||
|
origin_id: "nominatim:" <> to_string(Map.get(geocoding, "osm_id"))
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec street_address(map()) :: String.t()
|
@spec street_address(map()) :: String.t()
|
||||||
|
@ -97,8 +111,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
Map.has_key?(body, "road") ->
|
Map.has_key?(body, "road") ->
|
||||||
Map.get(body, "road")
|
Map.get(body, "road")
|
||||||
|
|
||||||
Map.has_key?(body, "road") ->
|
Map.has_key?(body, "street") ->
|
||||||
Map.get(body, "road")
|
Map.get(body, "street")
|
||||||
|
|
||||||
Map.has_key?(body, "pedestrian") ->
|
Map.has_key?(body, "pedestrian") ->
|
||||||
Map.get(body, "pedestrian")
|
Map.get(body, "pedestrian")
|
||||||
|
@ -107,7 +121,7 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
""
|
""
|
||||||
end
|
end
|
||||||
|
|
||||||
Map.get(body, "house_number", "") <> " " <> road
|
Map.get(body, "housenumber", "") <> " " <> road
|
||||||
end
|
end
|
||||||
|
|
||||||
@address29_classes ["amenity", "shop", "tourism", "leisure"]
|
@address29_classes ["amenity", "shop", "tourism", "leisure"]
|
||||||
|
@ -115,14 +129,16 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||||
|
|
||||||
@spec description(map()) :: String.t()
|
@spec description(map()) :: String.t()
|
||||||
defp description(body) do
|
defp description(body) do
|
||||||
if !Map.has_key?(body, "display_name") do
|
description = Map.get(body, "name")
|
||||||
Logger.warn("Address has no display name")
|
|
||||||
raise ArgumentError, message: "Address has no display_name"
|
|
||||||
end
|
|
||||||
|
|
||||||
description = Map.get(body, "display_name")
|
|
||||||
address = Map.get(body, "address")
|
address = Map.get(body, "address")
|
||||||
|
|
||||||
|
description =
|
||||||
|
if Map.has_key?(body, "namedetails"),
|
||||||
|
do: body |> Map.get("namedetails") |> Map.get("name", description),
|
||||||
|
else: description
|
||||||
|
|
||||||
|
description = if is_nil(description), do: street_address(body), else: description
|
||||||
|
|
||||||
if (Map.get(body, "category") in @address29_categories or
|
if (Map.get(body, "category") in @address29_categories or
|
||||||
Map.get(body, "class") in @address29_classes) and Map.has_key?(address, "address29") do
|
Map.get(body, "class") in @address29_classes) and Map.has_key?(address, "address29") do
|
||||||
Map.get(address, "address29")
|
Map.get(address, "address29")
|
||||||
|
|
|
@ -16,6 +16,7 @@ defmodule Mobilizon.Service.Geospatial.Provider do
|
||||||
* `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"`
|
* `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"`
|
||||||
* `:lang` Lang in which to prefer results. Used as a request parameter or
|
* `:lang` Lang in which to prefer results. Used as a request parameter or
|
||||||
through an `Accept-Language` HTTP header. Defaults to `"en"`.
|
through an `Accept-Language` HTTP header. Defaults to `"en"`.
|
||||||
|
* `:country_code` An ISO 3166 country code. String or `nil`
|
||||||
* `:limit` Maximum limit for the number of results returned by the backend.
|
* `:limit` Maximum limit for the number of results returned by the backend.
|
||||||
Defaults to `10`
|
Defaults to `10`
|
||||||
* `:api_key` Allows to override the API key (if the backend requires one) set
|
* `:api_key` Allows to override the API key (if the backend requires one) set
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Mobilizon.Storage.Repo.Migrations.AddTypeToAddresses do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:addresses) do
|
||||||
|
add(:type, :string)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,5 @@
|
||||||
# source: http://localhost:4000/api
|
# source: http://localhost:4000/api
|
||||||
# timestamp: Wed Nov 06 2019 12:50:45 GMT+0100 (Central European Standard Time)
|
# timestamp: Fri Nov 08 2019 17:20:47 GMT+0100 (Central European Standard Time)
|
||||||
|
|
||||||
schema {
|
schema {
|
||||||
query: RootQueryType
|
query: RootQueryType
|
||||||
|
@ -131,6 +131,7 @@ type Address {
|
||||||
|
|
||||||
"""The address's street name (with number)"""
|
"""The address's street name (with number)"""
|
||||||
street: String
|
street: String
|
||||||
|
type: String
|
||||||
url: String
|
url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +151,7 @@ input AddressInput {
|
||||||
|
|
||||||
"""The address's street name (with number)"""
|
"""The address's street name (with number)"""
|
||||||
street: String
|
street: String
|
||||||
|
type: String
|
||||||
url: String
|
url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +189,9 @@ enum CommentVisibility {
|
||||||
|
|
||||||
"""A config object"""
|
"""A config object"""
|
||||||
type Config {
|
type Config {
|
||||||
|
countryCode: String
|
||||||
description: String
|
description: String
|
||||||
|
location: Lonlat
|
||||||
name: String
|
name: String
|
||||||
registrationsOpen: Boolean
|
registrationsOpen: Boolean
|
||||||
}
|
}
|
||||||
|
@ -629,6 +633,12 @@ type Login {
|
||||||
user: User!
|
user: User!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Lonlat {
|
||||||
|
accuracyRadius: Int
|
||||||
|
latitude: Float
|
||||||
|
longitude: Float
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Represents a member of a group
|
Represents a member of a group
|
||||||
|
|
||||||
|
@ -1171,7 +1181,7 @@ type RootQueryType {
|
||||||
reverseGeocode(latitude: Float!, longitude: Float!): [Address]
|
reverseGeocode(latitude: Float!, longitude: Float!): [Address]
|
||||||
|
|
||||||
"""Search for an address"""
|
"""Search for an address"""
|
||||||
searchAddress(limit: Int = 10, page: Int = 1, query: String!): [Address]
|
searchAddress(limit: Int = 10, locale: String = "en", page: Int = 1, query: String!): [Address]
|
||||||
|
|
||||||
"""Search events"""
|
"""Search events"""
|
||||||
searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
|
searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
|
||||||
|
|
|
@ -2,17 +2,19 @@
|
||||||
{
|
{
|
||||||
"request": {
|
"request": {
|
||||||
"body": "",
|
"body": "",
|
||||||
"headers": [],
|
"headers": {
|
||||||
|
"User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1"
|
||||||
|
},
|
||||||
"method": "get",
|
"method": "get",
|
||||||
"options": [],
|
"options": [],
|
||||||
"request_body": "",
|
"request_body": "",
|
||||||
"url": "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=45.751718&lon=4.842569&addressdetails=1"
|
"url": "https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat=45.751718&lon=4.842569&accept-language=en&addressdetails=1&namedetails=1"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"binary": false,
|
"binary": false,
|
||||||
"body": "{\"place_id\":41453794,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":3078260611,\"lat\":\"45.7517141\",\"lon\":\"4.8425657\",\"place_rank\":30,\"category\":\"place\",\"type\":\"house\",\"importance\":\"0\",\"addresstype\":\"place\",\"name\":null,\"display_name\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France métropolitaine, 69007, France\",\"address\":{\"house_number\":\"10\",\"road\":\"Rue Jangot\",\"suburb\":\"La Guillotière\",\"city_district\":\"Lyon 7e Arrondissement\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state_district\":\"Circonscription départementale du Rhône\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"postcode\":\"69007\",\"country_code\":\"fr\"},\"boundingbox\":[\"45.7516141\",\"45.7518141\",\"4.8424657\",\"4.8426657\"]}",
|
"body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"45.751718,4.842569\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"accuracy\":0,\"label\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Date": "Thu, 14 Mar 2019 10:26:11 GMT",
|
"Date": "Tue, 12 Nov 2019 12:21:45 GMT",
|
||||||
"Server": "Apache/2.4.29 (Ubuntu)",
|
"Server": "Apache/2.4.29 (Ubuntu)",
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Access-Control-Allow-Methods": "OPTIONS,GET",
|
"Access-Control-Allow-Methods": "OPTIONS,GET",
|
||||||
|
|
|
@ -2,17 +2,19 @@
|
||||||
{
|
{
|
||||||
"request": {
|
"request": {
|
||||||
"body": "",
|
"body": "",
|
||||||
"headers": [],
|
"headers": {
|
||||||
|
"User-Agent": "Test instance mobilizon.test - Mobilizon 1.0.0-beta.1"
|
||||||
|
},
|
||||||
"method": "get",
|
"method": "get",
|
||||||
"options": [],
|
"options": [],
|
||||||
"request_body": "",
|
"request_body": "",
|
||||||
"url": "https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1"
|
"url": "https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20rue%20Jangot&limit=10&accept-language=en&addressdetails=1&namedetails=1"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"binary": false,
|
"binary": false,
|
||||||
"body": "[{\"place_id\":41453794,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":3078260611,\"boundingbox\":[\"45.7516641\",\"45.7517641\",\"4.8425157\",\"4.8426157\"],\"lat\":\"45.7517141\",\"lon\":\"4.8425657\",\"display_name\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"place_rank\":30,\"category\":\"place\",\"type\":\"house\",\"importance\":0.31100000000000005,\"address\":{\"house_number\":\"10\",\"road\":\"Rue Jangot\",\"suburb\":\"La Guillotière\",\"city_district\":\"Lyon 7e Arrondissement\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state_district\":\"Departemental constituency of Rhône\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"postcode\":\"69007\",\"country_code\":\"fr\"}}]",
|
"body": "{\"type\":\"FeatureCollection\",\"geocoding\":{\"version\":\"0.1.0\",\"attribution\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"licence\":\"ODbL\",\"query\":\"10 rue Jangot\"},\"features\":[{\"type\":\"Feature\",\"properties\":{\"geocoding\":{\"place_id\":41453794,\"osm_type\":\"node\",\"osm_id\":3078260611,\"type\":\"house\",\"label\":\"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France\",\"name\":null,\"housenumber\":\"10\",\"street\":\"Rue Jangot\",\"postcode\":\"69007\",\"city\":\"Lyon\",\"county\":\"Lyon\",\"state\":\"Auvergne-Rhône-Alpes\",\"country\":\"France\",\"admin\":{\"level2\":\"France\",\"level3\":\"Metropolitan France\",\"level4\":\"Auvergne-Rhône-Alpes\",\"level5\":\"Departemental constituency of Rhône\",\"level6\":\"Métropole de Lyon\",\"level7\":\"Lyon\",\"level8\":\"Lyon\",\"level9\":\"Lyon 7e Arrondissement\"}}},\"geometry\":{\"type\":\"Point\",\"coordinates\":[4.8425657,45.7517141]}}]}",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Date": "Thu, 14 Mar 2019 10:24:24 GMT",
|
"Date": "Tue, 12 Nov 2019 12:21:46 GMT",
|
||||||
"Server": "Apache/2.4.29 (Ubuntu)",
|
"Server": "Apache/2.4.29 (Ubuntu)",
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Access-Control-Allow-Methods": "OPTIONS,GET",
|
"Access-Control-Allow-Methods": "OPTIONS,GET",
|
||||||
|
|
|
@ -29,7 +29,7 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do
|
||||||
|
|
||||||
assert_called(
|
assert_called(
|
||||||
HTTPoison.get(
|
HTTPoison.get(
|
||||||
"https://nominatim.openstreetmap.org/search?format=jsonv2&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1",
|
"https://nominatim.openstreetmap.org/search?format=geocodejson&q=10%20Rue%20Jangot&limit=5&accept-language=fr&addressdetails=1&namedetails=1",
|
||||||
@httpoison_headers
|
@httpoison_headers
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -38,10 +38,10 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do
|
||||||
|
|
||||||
test "returns a valid address from search" do
|
test "returns a valid address from search" do
|
||||||
use_cassette "geospatial/nominatim/search" do
|
use_cassette "geospatial/nominatim/search" do
|
||||||
assert %Address{
|
assert [
|
||||||
|
%Address{
|
||||||
locality: "Lyon",
|
locality: "Lyon",
|
||||||
description:
|
description: "10 Rue Jangot",
|
||||||
"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Departemental constituency of Rhône, Auvergne-Rhône-Alpes, Metropolitan France, 69007, France",
|
|
||||||
region: "Auvergne-Rhône-Alpes",
|
region: "Auvergne-Rhône-Alpes",
|
||||||
country: "France",
|
country: "France",
|
||||||
postal_code: "69007",
|
postal_code: "69007",
|
||||||
|
@ -51,17 +51,19 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do
|
||||||
properties: %{},
|
properties: %{},
|
||||||
srid: 4326
|
srid: 4326
|
||||||
},
|
},
|
||||||
origin_id: "osm:3078260611"
|
origin_id: "nominatim:3078260611",
|
||||||
} == Nominatim.search("10 rue Jangot") |> hd
|
type: "house"
|
||||||
|
}
|
||||||
|
] == Nominatim.search("10 rue Jangot")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns a valid address from reverse geocode" do
|
test "returns a valid address from reverse geocode" do
|
||||||
use_cassette "geospatial/nominatim/geocode" do
|
use_cassette "geospatial/nominatim/geocode" do
|
||||||
assert %Address{
|
assert [
|
||||||
|
%Address{
|
||||||
locality: "Lyon",
|
locality: "Lyon",
|
||||||
description:
|
description: "10 Rue Jangot",
|
||||||
"10, Rue Jangot, La Guillotière, Lyon 7e Arrondissement, Lyon, Métropole de Lyon, Circonscription départementale du Rhône, Auvergne-Rhône-Alpes, France métropolitaine, 69007, France",
|
|
||||||
region: "Auvergne-Rhône-Alpes",
|
region: "Auvergne-Rhône-Alpes",
|
||||||
country: "France",
|
country: "France",
|
||||||
postal_code: "69007",
|
postal_code: "69007",
|
||||||
|
@ -71,10 +73,11 @@ defmodule Mobilizon.Service.Geospatial.NominatimTest do
|
||||||
properties: %{},
|
properties: %{},
|
||||||
srid: 4326
|
srid: 4326
|
||||||
},
|
},
|
||||||
origin_id: "osm:3078260611"
|
origin_id: "nominatim:3078260611",
|
||||||
} ==
|
type: "house"
|
||||||
|
}
|
||||||
|
] ==
|
||||||
Nominatim.geocode(4.842569, 45.751718)
|
Nominatim.geocode(4.842569, 45.751718)
|
||||||
|> hd
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,11 +26,6 @@ defmodule MobilizonWeb.Resolvers.AddressResolverTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "geocode/3 reverse geocodes coordinates", %{conn: conn} do
|
test "geocode/3 reverse geocodes coordinates", %{conn: conn} do
|
||||||
address =
|
|
||||||
insert(:address,
|
|
||||||
description: "10 rue Jangot, Lyon"
|
|
||||||
)
|
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
{
|
{
|
||||||
reverseGeocode(longitude: -23.01, latitude: 30.01) {
|
reverseGeocode(longitude: -23.01, latitude: 30.01) {
|
||||||
|
@ -44,7 +39,8 @@ defmodule MobilizonWeb.Resolvers.AddressResolverTest do
|
||||||
conn
|
conn
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["reverseGeocode"] == []
|
assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
|
||||||
|
"Anywhere"
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
{
|
{
|
||||||
|
@ -60,7 +56,7 @@ defmodule MobilizonWeb.Resolvers.AddressResolverTest do
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "address"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
|
assert json_response(res, 200)["data"]["reverseGeocode"] |> hd |> Map.get("description") ==
|
||||||
address.description
|
"10 rue Jangot, Lyon"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,9 @@ defmodule Mobilizon.Service.Geospatial.Mock do
|
||||||
@behaviour Provider
|
@behaviour Provider
|
||||||
|
|
||||||
@impl Provider
|
@impl Provider
|
||||||
def geocode(_lon, _lat, _options \\ []), do: []
|
def geocode(_lon, _lat, _options \\ [])
|
||||||
|
def geocode(45.75, 4.85, _options), do: [%Address{description: "10 rue Jangot, Lyon"}]
|
||||||
|
def geocode(_lon, _lat, _options), do: [%Address{description: "Anywhere"}]
|
||||||
|
|
||||||
@impl Provider
|
@impl Provider
|
||||||
def search(_q, _options \\ []), do: [%Address{description: "10 rue Jangot, Lyon"}]
|
def search(_q, _options \\ []), do: [%Address{description: "10 rue Jangot, Lyon"}]
|
||||||
|
|
Loading…
Reference in a new issue