Add the map in search view

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2022-09-01 10:00:17 +02:00
parent b36ce27bbe
commit eecb04516e
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
26 changed files with 1507 additions and 329 deletions

View File

@ -65,10 +65,12 @@
"floating-vue": "^2.0.0-beta.17",
"graphql": "^15.8.0",
"graphql-tag": "^2.10.3",
"hammerjs": "^2.0.8",
"intersection-observer": "^0.12.0",
"jwt-decode": "^3.1.2",
"leaflet": "^1.4.0",
"leaflet.locatecontrol": "^0.76.0",
"leaflet.markercluster": "^1.5.3",
"lodash": "^4.17.11",
"ngeohash": "^0.6.3",
"p-debounce": "^4.0.0",
@ -98,8 +100,10 @@
"@rushstack/eslint-patch": "^1.1.4",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.4",
"@types/hammerjs": "^2.0.41",
"@types/leaflet": "^1.5.2",
"@types/leaflet.locatecontrol": "^0.74",
"@types/leaflet.markercluster": "^1.5.1",
"@types/lodash": "^4.14.141",
"@types/ngeohash": "^0.6.2",
"@types/phoenix": "^1.5.2",

View File

@ -3,7 +3,7 @@
class="mbz-card snap-center dark:bg-mbz-purple"
:class="{
'sm:flex sm:items-start': mode === 'row',
'max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
'sm:max-w-xs sm:w-[18rem] shrink-0 flex flex-col': mode === 'column',
}"
:to="to"
:isInternal="isInternal"

View File

@ -1,6 +1,6 @@
<template>
<div>
<h2 class="text-2xl">{{ title }}</h2>
<h2>{{ title }}</h2>
<div class="flex items-center mb-3 gap-1 eventMetadataBlock">
<slot name="icon"></slot>
<!-- Custom icons -->
@ -15,7 +15,7 @@
/>
</span>
<o-icon v-else-if="icon" :icon="icon" size="is-medium" /> -->
<div class="content-wrapper">
<div class="content-wrapper overflow-hidden w-full">
<slot></slot>
</div>
</div>
@ -28,13 +28,7 @@ defineProps<{
</script>
<style lang="scss" scoped>
div.eventMetadataBlock {
display: flex;
align-items: center;
margin-bottom: 1.75rem;
.content-wrapper {
overflow: hidden;
width: 100%;
max-width: calc(100vw - 32px - 20px);
&.padding-left {

View File

@ -2,11 +2,11 @@
<div>
<event-metadata-block
v-if="!event.options.isOnline"
:title="$t('Location')"
:icon="physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'"
:title="t('Location')"
:icon="addressPOIInfos?.poiIcon?.icon ?? 'earth'"
>
<div class="address-wrapper">
<span v-if="!physicalAddress">{{ $t("No address defined") }}</span>
<span v-if="!physicalAddress">{{ t("No address defined") }}</span>
<div class="address" v-if="physicalAddress">
<address-info :address="physicalAddress" />
<o-button
@ -15,12 +15,23 @@
@click="$emit('showMapModal', true)"
v-if="physicalAddress.geom"
>
{{ $t("Show map") }}
{{ t("Show map") }}
</o-button>
</div>
</div>
<template #icon>
<o-icon
v-if="addressPOIInfos?.poiIcon?.icon"
:icon="addressPOIInfos?.poiIcon?.icon"
customSize="36"
/>
<Earth v-else :size="36" />
</template>
</event-metadata-block>
<event-metadata-block :title="$t('Date and time')" icon="calendar">
<event-metadata-block :title="t('Date and time')">
<template #icon>
<Calendar :size="36" />
</template>
<event-full-date
:beginsOn="event.beginsOn.toString()"
:show-start-time="event.options.showStartTime"
@ -32,7 +43,7 @@
</event-metadata-block>
<event-metadata-block
class="metadata-organized-by"
:title="$t('Organized by')"
:title="t('Organized by')"
>
<router-link
v-if="event.attributedTo"
@ -66,16 +77,18 @@
</event-metadata-block>
<event-metadata-block
v-if="event.onlineAddress && urlToHostname(event.onlineAddress)"
icon="link"
:title="$t('Website')"
:title="t('Website')"
>
<template #icon>
<Link :size="36" />
</template>
<a
target="_blank"
class="hover:underline"
rel="noopener noreferrer ugc"
:href="event.onlineAddress"
:title="
$t('View page on {hostname} (in a new window)', {
t('View page on {hostname} (in a new window)', {
hostname: urlToHostname(event.onlineAddress),
})
"
@ -85,9 +98,9 @@
<event-metadata-block
v-for="extra in extraMetadata"
:title="extra.title || extra.label"
:icon="extra.icon"
:key="extra.key"
>
<template #icon> <o-icon :icon="extra.icon" customSize="36" /> </template>
<span
v-if="
((extra.type == EventMetadataType.STRING &&
@ -108,7 +121,7 @@
rel="noopener noreferrer ugc"
:href="extra.value"
:title="
$t('View page on {hostname} (in a new window)', {
t('View page on {hostname} (in a new window)', {
hostname: urlToHostname(extra.value),
})
"
@ -123,7 +136,7 @@
rel="noopener noreferrer ugc"
:href="accountURL(extra)"
:title="
$t('View account on {hostname} (in a new window)', {
t('View account on {hostname} (in a new window)', {
hostname: urlToHostname(accountURL(extra)),
})
"
@ -134,7 +147,7 @@
</div>
</template>
<script lang="ts" setup>
import { Address } from "@/types/address.model";
import { Address, addressToPoiInfos } from "@/types/address.model";
import { EventMetadataKeyType, EventMetadataType } from "@/types/enums";
import { IEvent } from "@/types/event.model";
import { computed } from "vue";
@ -147,6 +160,10 @@ import AddressInfo from "../../components/Address/AddressInfo.vue";
import { IEventMetadataDescription } from "@/types/event-metadata";
import { eventMetaDataList } from "../../services/EventMetadata";
import { IUser } from "@/types/current-user.model";
import { useI18n } from "vue-i18n";
import Earth from "vue-material-design-icons/Earth.vue";
import Calendar from "vue-material-design-icons/Calendar.vue";
import Link from "vue-material-design-icons/Link.vue";
const props = withDefaults(
defineProps<{
@ -157,12 +174,19 @@ const props = withDefaults(
{ showMap: false }
);
const { t } = useI18n({ useScope: "global" });
const physicalAddress = computed((): Address | null => {
if (!props.event.physicalAddress) return null;
return new Address(props.event.physicalAddress);
});
const addressPOIInfos = computed(() => {
if (!props.event.physicalAddress) return null;
return addressToPoiInfos(props.event.physicalAddress);
});
const extraMetadata = computed((): IEventMetadataDescription[] => {
return props.event.metadata.map((val) => {
const def = eventMetaDataList.find((dat) => dat.key === val.key);

View File

@ -5,7 +5,7 @@
class="mbz-card shrink-0 dark:bg-mbz-purple dark:text-white rounded-lg shadow-lg my-4 flex items-center flex-col"
:class="{
'sm:flex-row': mode === 'row',
'max-w-xs w-[18rem] shrink-0 flex flex-col': mode === 'column',
'sm:max-w-xs sm:w-[18rem] shrink-0 flex flex-col': mode === 'column',
}"
>
<div class="flex-none p-2 md:p-4">
@ -25,7 +25,7 @@
:class="{ 'sm:flex-1': mode === 'row' }"
>
<div class="flex gap-1 mb-2">
<div class="px-1 overflow-hidden flex-auto">
<div class="overflow-hidden flex-auto">
<h3
class="text-2xl leading-5 line-clamp-3 font-bold text-violet-3 dark:text-white"
dir="auto"

View File

@ -0,0 +1,370 @@
<template>
<div
:class="[
'bottom-sheet',
{
opened: opened,
closed: opened === false,
moving: moving,
},
]"
v-on="handlers"
ref="bottomSheet"
:style="{
'pointer-events':
backgroundClickable && clickToClose === false ? 'none' : 'all',
}"
>
<div
v-if="overlay"
class="bottom-sheet__backdrop"
:style="{ background: overlayColor }"
/>
<div
:style="[
{ bottom: cardP + 'px', maxWidth: maxWidth, maxHeight: maxHeight },
{ height: isFullScreen ? '100%' : 'auto' },
{ 'pointer-events': 'all' },
]"
:class="[
'bottom-sheet__card bg-white dark:bg-gray-800',
{ stripe: stripe, square: !rounded },
effect,
]"
ref="bottomSheetCard"
>
<div class="bottom-sheet__pan" ref="pan">
<div class="bottom-sheet__bar bg-gray-700 dark:bg-gray-400" />
</div>
<div
:style="{ height: contentH }"
ref="bottomSheetCardContent"
class="bottom-sheet__content"
>
<slot />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import Hammer from "hammerjs";
import { onBeforeUnmount, reactive, ref } from "vue";
const inited = ref(false);
const opened = ref(false);
const contentH = ref("auto");
const hammer = reactive<{
pan: any;
content: any;
}>({
pan: null,
content: null,
});
const contentScroll = ref(0);
const cardP = ref<number>(0);
const cardH = ref<number>(0);
const moving = ref(false);
const stripe = ref(0);
const props = withDefaults(
defineProps<{
overlay?: boolean;
maxWidth?: string;
maxHeight?: string;
clickToClose?: boolean;
effect?: string;
rounded?: boolean;
swipeAble?: boolean;
isFullScreen?: boolean;
overlayColor?: string;
backgroundScrollable?: boolean;
backgroundClickable?: boolean;
}>(),
{
overlay: true,
maxWidth: "640px",
maxHeight: "95%",
clickToClose: true,
effect: "fx-default",
rounded: true,
swipeAble: true,
isFullScreen: false,
overlayColor: "#0000004D",
backgroundScrollable: false,
backgroundClickable: false,
}
);
const emit = defineEmits(["closed", "opened"]);
const bottomSheetCardContent = ref();
const bottomSheetCard = ref();
const pan = ref();
const isIphone = () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const iPhone = /iPhone/.test(navigator.userAgent) && !window.MSStream;
const aspect = window.screen.width / window.screen.height;
return iPhone && aspect.toFixed(3) === "0.462";
};
const move = (event: any, type: any) => {
if (props.swipeAble) {
const delta = -event.deltaY;
if (
(type === "content" && event.type === "panup") ||
(type === "content" &&
event.type === "pandown" &&
contentScroll.value > 0)
) {
bottomSheetCardContent.value.scrollTop = contentScroll.value + delta;
} else if (event.type === "panup" || event.type === "pandown") {
moving.value = true;
if (event.deltaY > 0) {
cardP.value = delta;
}
}
if (event.isFinal) {
contentScroll.value = bottomSheetCardContent.value.scrollTop;
moving.value = false;
if (cardP.value < -30) {
opened.value = false;
cardP.value = (-cardH.value ?? 0) - stripe.value;
document.body.style.overflow = "";
emit("closed");
} else {
cardP.value = 0;
}
}
}
};
const init = () => {
return new Promise((resolve) => {
contentH.value = "auto";
stripe.value = isIphone() ? 20 : 0;
cardH.value = bottomSheetCard.value.clientHeight;
contentH.value = `${cardH.value - pan.value.clientHeight}px`;
bottomSheetCard.value.style.maxHeight = props.maxHeight;
cardP.value =
props.effect === "fx-slide-from-right" ||
props.effect === "fx-slide-from-left"
? 0
: -cardH.value - stripe.value;
if (!inited.value) {
inited.value = true;
const options = {
recognizers: [[Hammer.Pan, { direction: Hammer.DIRECTION_VERTICAL }]],
};
hammer.pan = new Hammer(pan.value, options as any);
hammer.pan?.on("panstart panup pandown panend", (e: any) => {
move(e, "pan");
});
hammer.content = new Hammer(bottomSheetCardContent.value, options as any);
hammer.content?.on("panstart panup pandown panend", (e: any) => {
move(e, "content");
});
}
setTimeout(() => {
resolve(undefined);
}, 100);
});
};
const open = async () => {
console.debug("open vue bottom sheet");
await init();
opened.value = true;
cardP.value = 0;
if (!props.backgroundScrollable) {
document.body.style.overflow = "hidden";
}
emit("opened");
};
const close = () => {
opened.value = false;
cardP.value =
props.effect === "fx-slide-from-right" ||
props.effect === "fx-slide-from-left"
? 0
: -cardH.value - stripe.value;
document.body.style.overflow = "";
emit("closed");
};
const clickOnBottomSheet = (event: any) => {
if (props.clickToClose) {
if (
event.target.classList.contains("bottom-sheet__backdrop") ||
event.target.classList.contains("bottom-sheet")
) {
close();
}
}
};
onBeforeUnmount(() => {
hammer?.pan?.destroy();
hammer?.content?.destroy();
});
const handlers = {
mousedown: clickOnBottomSheet,
touchstart: clickOnBottomSheet,
};
defineExpose({ open, close });
</script>
<style lang="scss" scoped>
.bottom-sheet {
z-index: 99999;
transition: all 0.4s ease;
position: relative;
* {
box-sizing: border-box;
}
&__content {
overflow-y: scroll;
}
&__backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
visibility: hidden;
}
&__card {
width: 100%;
position: fixed;
border-radius: 14px 14px 0 0;
left: 50%;
z-index: 9999;
margin: 0 auto;
&.square {
border-radius: 0;
}
&.stripe {
padding-bottom: 20px;
}
&.fx-default {
transform: translate(-50%, 0);
transition: bottom 0.3s ease;
}
&.fx-fadein-scale {
transform: translate(-50%, 0) scale(0.7);
opacity: 0;
transition: all 0.3s;
}
&.fx-slide-from-right {
transform: translate(100%, 0);
opacity: 0;
transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9);
}
&.fx-slide-from-left {
transform: translate(-100%, 0);
opacity: 0;
transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9);
}
}
&__pan {
padding-bottom: 20px;
padding-top: 15px;
height: 38px;
}
&__bar {
display: block;
width: 50px;
height: 3px;
border-radius: 14px;
margin: 0 auto;
cursor: pointer;
}
&.closed {
opacity: 0;
visibility: hidden;
.bottom-sheet__backdrop {
animation: hide 0.3s ease;
}
}
&.moving {
.bottom-sheet__card {
transition: none;
}
}
&.opened {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
.bottom-sheet__backdrop {
animation: show 0.3s ease;
opacity: 1;
visibility: visible;
}
.bottom-sheet__card {
&.fx-fadein-scale {
transform: translate(-50%, 0) scale(1);
opacity: 1;
}
&.fx-slide-from-right {
transform: translate(-50%, 0);
opacity: 1;
}
&.fx-slide-from-left {
transform: translate(-50%, 0);
opacity: 1;
}
}
}
}
@keyframes show {
0% {
opacity: 0;
visibility: hidden;
}
100% {
opacity: 1;
visibility: visible;
}
}
@keyframes hide {
0% {
opacity: 1;
visibility: visible;
}
100% {
opacity: 0;
visibility: hidden;
}
}
</style>

View File

@ -0,0 +1,400 @@
<template>
<div class="relative my-2">
<div style="height: 70vh" id="mapMountPoint" />
<vue-bottom-sheet
v-if="activeElement"
ref="myBottomSheet"
class="md:hidden"
max-height="70%"
:background-scrollable="false"
>
<event-card
v-if="instanceOfIEvent(activeElement)"
:event="(activeElement as IEvent)"
:has-border="false"
view-mode="column"
:options="{
isRemoteEvent: activeElement.__typename === 'EventResult',
isLoggedIn,
}"
/>
<group-card
v-else
:group="(activeElement as IGroup)"
:has-border="false"
view-mode="column"
:isRemoteGroup="activeElement.__typename === 'GroupResult'"
:isLoggedIn="isLoggedIn"
/>
</vue-bottom-sheet>
<div
class="absolute hidden md:block bottom-0 md:top-8 right-0 h-48 w-full md:w-80 overflow-y-visible text-white [box-shadow:0 6px 9px 2px rgba(119,119,119,.75)] -my-4 px-2 z-[1100]"
v-if="activeElement"
>
<event-card
v-if="instanceOfIEvent(activeElement)"
:event="(activeElement as IEvent)"
view-mode="column"
:has-border="false"
:options="{
isRemoteEvent: activeElement.__typename === 'EventResult',
isLoggedIn,
}"
/>
<group-card
v-else
:group="(activeElement as IGroup)"
:has-border="false"
view-mode="column"
:isRemoteGroup="activeElement.__typename === 'GroupResult'"
:isLoggedIn="isLoggedIn"
/>
</div>
</div>
</template>
<script setup lang="ts">
import "leaflet/dist/leaflet.css";
import {
computed,
createApp,
DefineComponent,
h,
onBeforeUnmount,
onMounted,
ref,
watch,
} from "vue";
import VueBottomSheet from "@/components/Map/VueBottomSheet.vue";
import {
map,
LatLngBounds,
tileLayer,
marker,
divIcon,
Map,
Marker,
} from "leaflet";
import { MarkerClusterGroup } from "leaflet.markercluster/src";
import { IGroup } from "@/types/actor";
import { IEvent, instanceOfIEvent } from "@/types/event.model";
import { ContentType } from "@/types/enums";
import Calendar from "vue-material-design-icons/Calendar.vue";
import AccountMultiple from "vue-material-design-icons/AccountMultiple.vue";
import GroupCard from "@/components/Group/GroupCard.vue";
import EventCard from "@/components/Event/EventCard.vue";
import debounce from "lodash/debounce";
import { Paginate } from "@/types/paginate";
import { TypeNamed } from "@/types/apollo";
const mapElement = ref<Map>();
const markers = ref<MarkerClusterGroup>();
const myBottomSheet = ref<typeof VueBottomSheet>();
const props = defineProps<{
contentType: ContentType;
events: Paginate<TypeNamed<IEvent>>;
groups: Paginate<TypeNamed<IGroup>>;
latitude?: number;
longitude?: number;
isLoggedIn: boolean | undefined;
}>();
const emit = defineEmits<{
(
e: "map-updated",
{ bounds, zoom }: { bounds: LatLngBounds; zoom: number }
): void;
}>();
const activeElement = ref<TypeNamed<IEvent> | TypeNamed<IGroup> | null>(null);
const events = computed(() => props.events?.elements ?? []);
const groups = computed(() => props.groups?.elements ?? []);
watch([events, groups], update);
function update() {
if (!mapElement.value || !mapElement.value.getBounds) return;
const rawBounds: LatLngBounds = mapElement.value.getBounds();
const bounds: LatLngBounds = mapElement.value.wrapLatLngBounds(rawBounds);
if (
bounds.getNorthWest().lat === 0 ||
bounds.getNorthWest().lat === bounds.getSouthEast().lat
)
return;
const zoom = mapElement.value.getZoom();
emit("map-updated", { bounds, zoom });
}
onBeforeUnmount(() => {
if (mapElement.value) {
mapElement.value.remove();
}
});
const initialView = computed<[[number, number], number]>(() => {
if (props.latitude && props.longitude) {
return [[props.latitude, props.longitude], 12];
}
return [[0, 0], 3];
});
watch(initialView, ([latlng, zoom]) => {
setLatLng(latlng, zoom);
});
const setLatLng = (latlng: [number, number], zoom: number) => {
console.debug("setting view to ", latlng, zoom);
mapElement.value?.setView(latlng, zoom);
};
onMounted(async () => {
mapElement.value = map("mapMountPoint");
setLatLng(...initialView.value);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line no-underscore-dangle
mapElement.value._onResize();
mapElement.value.on("click", () => {
activeElement.value = null;
if (myBottomSheet.value) {
myBottomSheet.value.close();
}
});
// mapElement.value.on('load', function () {
// console.log('load event')
// setTimeout(() => {
// console.log('invalidate size')
// mapElement.value.invalidateSize()
// }, 1000)
// })
markers.value = new MarkerClusterGroup({ chunkedLoading: true });
mapElement.value.on("zoom", debounce(update, 1000));
mapElement.value.on("moveend", debounce(update, 1000));
tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors',
className: "map-tiles",
}).addTo(mapElement.value);
});
const categoryToColorClass = (element: IEvent | IGroup): string => {
if (instanceOfIEvent(element)) {
return "marker-event";
}
return "marker-group";
};
const pointToLayer = (
element: TypeNamed<IEvent> | TypeNamed<IGroup>,
latlng: { lat: number; lng: number }
): Marker => {
const icon = divIcon({
html: `<div class="marker-container ${categoryToColorClass(element)}">
<div class="pin-icon-container">
<svg width="24" height="24" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 10C20 14.4183 12 22 12 22C12 22 4 14.4183 4 10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10Z" stroke="currentColor" stroke-width="1.5"></path>
</svg>
</div>
<div class="element-icon-container text-black">
${instanceOfIEvent(element) ? calendarHTML : AccountMultipleHTML}
</div>
</div>`,
iconSize: [50, 50],
iconAnchor: [25, 50],
// iconSize: [
// MARKER_TOUCH_TARGET_SIZE * 0.5,
// MARKER_TOUCH_TARGET_SIZE * 0.5,
// ],
});
return marker(latlng, { icon }).on("click", () => {
activeElement.value = element;
if (myBottomSheet.value) {
myBottomSheet.value.open();
}
});
};
// https://stackoverflow.com/a/68319134/10204399
const vueComponentToHTML = (
component: DefineComponent,
localProps: Record<string, any> = {}
) => {
const tempApp = createApp({
render() {
return h(component, localProps);
},
});
// in Vue 3 we need real element to mount to unlike in Vue 2 where mount() could be called without argument...
const el = document.createElement("div");
const mountedApp = tempApp.mount(el);
const html = mountedApp.$el.outerHTML as string;
// tempApp.unmount();
return html;
};
const calendarHTML = vueComponentToHTML(Calendar);
const AccountMultipleHTML = vueComponentToHTML(AccountMultiple);
update();
const eventMarkers = computed(() => {
return events.value?.reduce((acc, event) => {
if (event.physicalAddress?.geom) {
const [lng, lat] = event.physicalAddress.geom.split(";");
return [
...acc,
pointToLayer(event, {
lng: Number.parseFloat(lng),
lat: Number.parseFloat(lat),
}),
];
}
return acc;
}, [] as Marker[]);
});
const groupMarkers = computed(() => {
return groups.value?.reduce((acc: Marker[], group: TypeNamed<IGroup>) => {
if (group.physicalAddress?.geom) {
const [lng, lat] = group.physicalAddress.geom.split(";");
return [
...acc,
pointToLayer(group, {
lng: Number.parseFloat(lng),
lat: Number.parseFloat(lat),
}),
];
}
return acc;
}, [] as Marker[]);
});
watch([markers, eventMarkers, groupMarkers], () => {
if (!markers.value) return;
console.debug(
"something changed in the search map",
markers.value,
eventMarkers.value,
groupMarkers.value
);
markers.value?.clearLayers();
if (props.contentType !== ContentType.GROUPS) {
eventMarkers.value?.forEach((markerToAdd) => {
console.debug("adding event marker layer to markers");
markers.value.addLayer(markerToAdd);
});
}
if (props.contentType !== ContentType.EVENTS) {
groupMarkers.value?.forEach((markerToAdd) => {
console.debug("adding group marker layer to markers");
markers.value.addLayer(markerToAdd);
});
}
mapElement.value?.addLayer(markers.value);
});
</script>
<style>
/*
* https://github.com/mapbox/supercluster/blob/f073fade1caae0b2b1beffd013b74ff024ff413b/demo/cluster.css
*/
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}
:root {
--map-tiles-filter: brightness(0.6) invert(1) contrast(3) hue-rotate(200deg)
saturate(0.3) brightness(0.7);
}
@media (prefers-color-scheme: dark) {
.map-tiles {
filter: var(--map-tiles-filter, none);
}
}
.leaflet-div-icon {
background: none !important;
border: none !important;
}
.marker-container {
position: relative;
}
.marker-container.marker-event .pin-icon-container svg {
fill: yellow;
color: black;
}
.marker-container.marker-group .pin-icon-container svg {
fill: lightblue;
color: white;
}
.pin-icon-container {
position: absolute;
width: 50px;
height: 50px;
}
.pin-icon-container svg path {
stroke-width: 1;
}
.pin-icon-container svg {
width: 100%;
height: 100%;
}
.element-icon-container {
position: absolute;
transform: translate(12px, 8px);
}
</style>

View File

@ -104,6 +104,7 @@ const icons: Record<string, () => Promise<any>> = {
),
Earth: () =>
import(`../../../node_modules/vue-material-design-icons/Earth.vue`),
Map: () => import(`../../../node_modules/vue-material-design-icons/Map.vue`),
MapMarker: () =>
import(`../../../node_modules/vue-material-design-icons/MapMarker.vue`),
Close: () =>
@ -231,6 +232,8 @@ const icons: Record<string, () => Promise<any>> = {
import(`../../../node_modules/vue-material-design-icons/Filter.vue`),
CheckCircle: () =>
import(`../../../node_modules/vue-material-design-icons/CheckCircle.vue`),
ViewList: () =>
import(`../../../node_modules/vue-material-design-icons/ViewList.vue`),
};
const props = withDefaults(

View File

@ -33,6 +33,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
$searchTarget: SearchTarget
$beginsOn: DateTime
$endsOn: DateTime
$bbox: String
$zoom: Int
$eventPage: Int
$groupPage: Int
$limit: Int
@ -49,6 +51,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
searchTarget: $searchTarget
beginsOn: $beginsOn
endsOn: $endsOn
bbox: $bbox
zoom: $zoom
page: $eventPage
limit: $limit
) {
@ -88,6 +92,8 @@ export const SEARCH_EVENTS_AND_GROUPS = gql`
radius: $radius
languageOneOf: $languageOneOf
searchTarget: $searchTarget
bbox: $bbox
zoom: $zoom
page: $groupPage
limit: $limit
) {

View File

@ -1390,5 +1390,20 @@
"The videoconference will be created on {service}": "The videoconference will be created on {service}",
"Search target": "Search target",
"In this instance's network": "In this instance's network",
"On the Fediverse": "On the Fediverse"
"On the Fediverse": "On the Fediverse",
"Report reason": "Report reason",
"Reported content": "Reported content",
"No results found": "No results found",
"{eventsCount} events found": "No events found|One event found|{eventsCount} events found",
"{groupsCount} groups found": "No groups found|One group found|{groupsCount} groups found",
"{resultsCount} results found": "No results found|On result found|{resultsCount} results found",
"Loading map": "Loading map",
"Sort by": "Sort by",
"Map": "Map",
"List": "List",
"Best match": "Best match",
"Most recently published": "Most recently published",
"Least recently published": "Least recently published",
"With the most participants": "With the most participants",
"Number of members": "Number of members"
}

View File

@ -1374,5 +1374,20 @@
"The videoconference will be created on {service}": "La visio-conférence sera créée sur {service}",
"Search target": "Cible de la recherche",
"In this instance's network": "Dans le réseau de cette instance",
"On the Fediverse": "Dans le fediverse"
"On the Fediverse": "Dans le fediverse",
"Report reason": "Raison du signalement",
"Reported content": "Contenu signalé",
"No results found": "Aucun résultat trouvé",
"{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés",
"{groupsCount} groups found": "Aucun groupe trouvé|Un groupe trouvé|{groupsCount} groupes trouvés",
"{resultsCount} results found": "Aucun résultat trouvé|Un résultat trouvé|{resultsCount} résultats trouvés",
"Loading map": "Chargement de la carte",
"Sort by": "Trier par",
"Map": "Carte",
"List": "Liste",
"Best match": "Pertinence",
"Most recently published": "Publié récemment",
"Least recently published": "Le moins récemment publié",
"With the most participants": "Avec le plus de participants",
"Number of members": "Nombre de membres"
}

View File

@ -32,7 +32,7 @@ export const sentry = (environment: any, sentryConfiguration: any) => {
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: sentryConfiguration.tracesSampleRate,
tracesSampleRate: Number.parseFloat(sentryConfiguration.tracesSampleRate),
release: environment.version,
logErrors: true,
});

View File

@ -2,7 +2,7 @@ declare module "*.vue" {
import type { DefineComponent } from "vue";
// eslint-disable-next-line @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
const component: DefineComponent<{}, {}, {}>;
export default component;
}

View File

@ -3,3 +3,7 @@ import { GraphQLError } from "graphql/error/GraphQLError";
export class AbsintheGraphQLError extends GraphQLError {
readonly field: string | undefined;
}
export type TypeNamed<T extends Record<string, any>> = T & {
__typename: string;
};

View File

@ -228,7 +228,6 @@ export class EventModel implements IEvent {
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function removeTypeName(entity: any): any {
if (entity?.__typename) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -292,3 +291,7 @@ export function organizerDisplayName(event: IEvent): string | null {
}
return null;
}
export function instanceOfIEvent(object: any): object is IEvent {
return "organizerActor" in object;
}

View File

@ -322,7 +322,7 @@
<aside class="event-metadata rounded dark:bg-gray-600 shadow-md">
<div class="sticky">
<event-metadata-sidebar
v-if="event && loggedUser"
v-if="event"
:event="event"
:user="loggedUser"
@showMapModal="showMap = true"

View File

@ -461,10 +461,15 @@
<event-metadata-block
v-if="physicalAddress && physicalAddress.url"
:title="t('Location')"
:icon="
physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'
"
>
<template #icon>
<o-icon
v-if="physicalAddress.poiInfos.poiIcon.icon"
:icon="physicalAddress.poiInfos.poiIcon.icon"
customSize="48"
/>
<Earth v-else :size="48" />
</template>
<div class="address-wrapper">
<span
v-if="!physicalAddress || !addressFullName(physicalAddress)"
@ -670,6 +675,7 @@ import CalendarSync from "vue-material-design-icons/CalendarSync.vue";
import Flag from "vue-material-design-icons/Flag.vue";
import ExitToApp from "vue-material-design-icons/ExitToApp.vue";
import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.vue";
import Earth from "vue-material-design-icons/Earth.vue";
import { useI18n } from "vue-i18n";
import { useCreateReport } from "@/composition/apollo/report";
import { useHead } from "@vueuse/head";

View File

@ -30,7 +30,10 @@
<!-- Categories preview -->
<categories-preview />
<!-- Welcome back -->
<section v-if="currentActor?.id && (welcomeBack || newRegisteredUser)">
<section
class="container mx-auto"
v-if="currentActor?.id && (welcomeBack || newRegisteredUser)"
>
<o-notification variant="info" v-if="welcomeBack">{{
$t("Welcome back {username}!", {
username: displayName(currentActor),

View File

@ -422,131 +422,217 @@
</form>
</aside>
<div class="flex-1 px-2">
<template v-if="contentType === ContentType.ALL">
<o-notification v-if="features && !features.groups" variant="danger">
{{ t("Groups are not enabled on this instance.") }}
</o-notification>
<div v-else-if="searchGroups && searchGroups?.total > 0">
<GroupCard
v-for="group in searchGroups?.elements"
:group="group"
:key="group.id"
:isRemoteGroup="group.__typename === 'GroupResult'"
:isLoggedIn="currentUser?.isLoggedIn"
mode="row"
/>
<o-pagination
v-if="searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT"
:total="searchGroups?.total"
v-model:current="groupPage"
:per-page="GROUP_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</div>
<o-notification v-else-if="searchLoading === false" variant="danger">
{{ t("No groups found") }}
</o-notification>
<div v-if="searchEvents && searchEvents.total > 0">
<event-card
mode="row"
v-for="event in searchEvents?.elements"
:event="event"
:key="event.uuid"
:options="{
isRemoteEvent: event.__typename === 'EventResult',
isLoggedIn: currentUser?.isLoggedIn,
}"
class="my-4"
/>
<o-pagination
v-if="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
:total="searchEvents.total"
v-model:current="eventPage"
:per-page="EVENT_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</div>
<o-notification v-else-if="searchLoading === false" variant="info">
<p>{{ t("No events found") }}</p>
<p v-if="searchIsUrl && !currentUser?.id">
<div
id="results-anchor"
class="hidden sm:flex items-center justify-between dark:text-slate-100"
>
<p v-if="totalCount === 0">
<span v-if="contentType === ContentType.EVENTS">{{
t("No events found")
}}</span>
<span v-else-if="contentType === ContentType.GROUPS">{{
t("No groups found")
}}</span>
<span v-else>{{ t("No results found") }}</span>
</p>
<p v-else>
<span v-if="contentType === 'EVENTS'">
{{
t("Only registered users may fetch remote events from their URL.")
t(
"{eventsCount} events found",
{ eventsCount: searchEvents?.total },
searchEvents?.total ?? 0
)
}}
</p>
</o-notification>
</template>
<template v-else-if="contentType === ContentType.EVENTS">
<template v-if="searchEvents && searchEvents.total > 0">
<event-card
mode="row"
v-for="event in searchEvents?.elements"
:event="event"
:key="event.uuid"
:options="{
isRemoteEvent: event.__typename === 'EventResult',
isLoggedIn: currentUser?.isLoggedIn,
}"
class="my-4"
/>
<o-pagination
v-show="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
:total="searchEvents.total"
v-model:current="eventPage"
:per-page="EVENT_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
</span>
<span v-else-if="contentType === 'GROUPS'">
{{
t(
"{groupsCount} groups found",
{ groupsCount: searchGroups?.total },
searchGroups?.total ?? 0
)
}}
</span>
<span v-else>
{{
t(
"{resultsCount} results found",
{ resultsCount: totalCount },
totalCount
)
}}
</span>
</p>
<div class="flex gap-2">
<o-select :placeholder="t('Sort by')" v-model="sortBy">
<option
v-for="sortOption in sortOptions"
:key="sortOption.key"
:value="sortOption.key"
>
{{ sortOption.label }}
</option>
</o-select>
<o-button
v-show="!isOnline"
@click="
() =>
(mode = mode === ViewMode.MAP ? ViewMode.LIST : ViewMode.MAP)
"
:icon-left="mode === ViewMode.MAP ? 'view-list' : 'map'"
>
</o-pagination>
<span v-if="mode === ViewMode.LIST">
{{ t("Map") }}
</span>
<span v-else-if="mode === ViewMode.MAP">
{{ t("List") }}
</span>
</o-button>
</div>
</div>
<div v-if="mode === ViewMode.LIST">
<template v-if="contentType === ContentType.ALL">
<o-notification v-if="features && !features.groups" variant="danger">
{{ t("Groups are not enabled on this instance.") }}
</o-notification>
<div v-else-if="searchGroups && searchGroups?.total > 0">
<GroupCard
v-for="group in searchGroups?.elements"
:group="group"
:key="group.id"
:isRemoteGroup="group.__typename === 'GroupResult'"
:isLoggedIn="currentUser?.isLoggedIn"
mode="row"
/>
<o-pagination
v-if="searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT"
:total="searchGroups?.total"
v-model:current="groupPage"
:per-page="GROUP_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</div>
<o-notification v-else-if="searchLoading === false" variant="danger">
{{ t("No groups found") }}
</o-notification>
<div v-if="searchEvents && searchEvents.total > 0">
<event-card
mode="row"
v-for="event in searchEvents?.elements"
:event="event"
:key="event.uuid"
:options="{
isRemoteEvent: event.__typename === 'EventResult',
isLoggedIn: currentUser?.isLoggedIn,
}"
class="my-4"
/>
<o-pagination
v-if="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
:total="searchEvents.total"
v-model:current="eventPage"
:per-page="EVENT_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</div>
<o-notification v-else-if="searchLoading === false" variant="info">
<p>{{ t("No events found") }}</p>
<p v-if="searchIsUrl && !currentUser?.id">
{{
t(
"Only registered users may fetch remote events from their URL."
)
}}
</p>
</o-notification>
</template>
<o-notification v-else-if="searchLoading === false" variant="info">
<p>{{ t("No events found") }}</p>
<p v-if="searchIsUrl && !currentUser?.id">
{{
t("Only registered users may fetch remote events from their URL.")
}}
</p>
</o-notification>
</template>
<template v-else-if="contentType === ContentType.GROUPS">
<o-notification v-if="features && !features.groups" variant="danger">
{{ t("Groups are not enabled on this instance.") }}
</o-notification>
<template v-else-if="contentType === ContentType.EVENTS">
<template v-if="searchEvents && searchEvents.total > 0">
<event-card
mode="row"
v-for="event in searchEvents?.elements"
:event="event"
:key="event.uuid"
:options="{
isRemoteEvent: event.__typename === 'EventResult',
isLoggedIn: currentUser?.isLoggedIn,
}"
class="my-4"
/>
<o-pagination
v-show="searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT"
:total="searchEvents.total"
v-model:current="eventPage"
:per-page="EVENT_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</template>
<o-notification v-else-if="searchLoading === false" variant="info">
<p>{{ t("No events found") }}</p>
<p v-if="searchIsUrl && !currentUser?.id">
{{
t(
"Only registered users may fetch remote events from their URL."
)
}}
</p>
</o-notification>
</template>
<template v-else-if="contentType === ContentType.GROUPS">
<o-notification v-if="features && !features.groups" variant="danger">
{{ t("Groups are not enabled on this instance.") }}
</o-notification>
<template v-else-if="searchGroups && searchGroups?.total > 0">
<GroupCard
v-for="group in searchGroups?.elements"
:group="group"
:key="group.id"
:isRemoteGroup="group.__typename === 'GroupResult'"
:isLoggedIn="currentUser?.isLoggedIn"
mode="row"
/>
<o-pagination
v-show="searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT"
:total="searchGroups?.total"
v-model:current="groupPage"
:per-page="GROUP_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
<template v-else-if="searchGroups && searchGroups?.total > 0">
<GroupCard
v-for="group in searchGroups?.elements"
:group="group"
:key="group.id"
:isRemoteGroup="group.__typename === 'GroupResult'"
:isLoggedIn="currentUser?.isLoggedIn"
mode="row"
/>
<o-pagination
v-show="searchGroups && searchGroups?.total > GROUP_PAGE_LIMIT"
:total="searchGroups?.total"
v-model:current="groupPage"
:per-page="GROUP_PAGE_LIMIT"
:aria-next-label="t('Next page')"
:aria-previous-label="t('Previous page')"
:aria-page-label="t('Page')"
:aria-current-label="t('Current page')"
>
</o-pagination>
</template>
<o-notification v-else-if="searchLoading === false" variant="danger">
{{ t("No groups found") }}
</o-notification>
</template>
<o-notification v-else-if="searchLoading === false" variant="danger">
{{ t("No groups found") }}
</o-notification>
</template>
</div>
<event-marker-map
v-if="mode === ViewMode.MAP"
:contentType="contentType"
:latitude="latitude"
:longitude="longitude"
:locationName="locationName"
@map-updated="setBounds"
:events="searchEvents"
:groups="searchGroups"
:isLoggedIn="currentUser?.isLoggedIn"
/>
</div>
</div>
</template>
@ -604,6 +690,9 @@ import SearchFields from "@/components/Home/SearchFields.vue";
import { refDebounced } from "@vueuse/core";
import { IAddress } from "@/types/address.model";
import { IConfig } from "@/types/config.model";
import { TypeNamed } from "@/types/apollo";
import EventMarkerMap from "@/components/Search/EventMarkerMap.vue";
import { LatLngBounds } from "leaflet";
const search = useRouteQuery("search", "");
const searchDebounced = refDebounced(search, 1000);
@ -611,11 +700,16 @@ const locationName = useRouteQuery("locationName", null);
const location = ref<IAddress | null>(null);
watch(location, (newLocation) => {
console.debug("location change");
console.debug("location change", newLocation);
if (newLocation?.geom) {
latitude.value = parseFloat(newLocation?.geom.split(";")[1]);
longitude.value = parseFloat(newLocation?.geom.split(";")[0]);
locationName.value = newLocation?.description;
console.debug("set location", [
latitude.value,
longitude.value,
locationName.value,
]);
} else {
console.debug("location emptied");
latitude.value = undefined;
@ -630,6 +724,20 @@ interface ISearchTimeOption {
end?: string | null;
}
enum ViewMode {
LIST = "list",
MAP = "map",
}
enum SortValues {
MATCH_DESC = "-match",
START_TIME_DESC = "-startTime",
CREATED_AT_DESC = "-createdAt",
CREATED_AT_ASC = "createdAt",
PARTICIPANT_COUNT_DESC = "-participantCount",
MEMBER_COUNT_DESC = "-memberCount",
}
const arrayTransformer: RouteQueryTransformer<string[]> = {
fromQuery(query: string) {
return query.split(",");
@ -665,6 +773,14 @@ const searchTarget = useRouteQuery(
SearchTargets.INTERNAL,
enumTransformer(SearchTargets)
);
const mode = useRouteQuery("mode", ViewMode.LIST, enumTransformer(ViewMode));
const sortBy = useRouteQuery(
"sortBy",
SortValues.MATCH_DESC,
enumTransformer(SortValues)
);
const bbox = useRouteQuery("bbox", undefined);
const zoom = useRouteQuery("zoom", undefined, integerTransformer);
const EVENT_PAGE_LIMIT = 16;
@ -905,6 +1021,49 @@ const geoHashLocation = computed(() =>
const radius = computed(() => Number.parseInt(distance.value.slice(0, -3)));
const totalCount = computed(() => {
return (searchEvents.value?.total ?? 0) + (searchGroups.value?.total ?? 0);
});
const sortOptions = computed(() => {
const options = [
{
key: SortValues.MATCH_DESC,
label: t("Best match"),
},
];
if (contentType.value == ContentType.EVENTS) {
options.push(
{
key: SortValues.START_TIME_DESC,
label: t("Event date"),
},
{
key: SortValues.CREATED_AT_DESC,
label: t("Most recently published"),
},
{
key: SortValues.CREATED_AT_ASC,
label: t("Least recently published"),
},
{
key: SortValues.PARTICIPANT_COUNT_DESC,
label: t("With the most participants"),
}
);
}
if (contentType.value == ContentType.GROUPS) {
options.push({
key: SortValues.MEMBER_COUNT_DESC,
label: t("Number of members"),
});
}
return options;
});
const { searchConfig, onResult: onSearchConfigResult } = useSearchConfig();
onSearchConfigResult(({ data }) =>
@ -930,9 +1089,34 @@ const globalSearchEnabled = computed(
() => searchConfig.value?.global?.isEnabled
);
const setBounds = ({
bounds,
zoom: boundsZoom,
}: {
bounds: LatLngBounds;
zoom: number;
}) => {
bbox.value = `${bounds.getNorthWest().lat}, ${bounds.getNorthWest().lng}:${
bounds.getSouthEast().lat
}, ${bounds.getSouthEast().lng}`;
zoom.value = boundsZoom;
};
watch(mode, (newMode) => {
if (newMode === ViewMode.MAP) {
isOnline.value = false;
}
});
watch(isOnline, (newIsOnline) => {
if (newIsOnline) {
mode.value = ViewMode.LIST;
}
});
const { result: searchElementsResult, loading: searchLoading } = useQuery<{
searchEvents: Paginate<IEvent>;
searchGroups: Paginate<IGroup>;
searchEvents: Paginate<TypeNamed<IEvent>>;
searchGroups: Paginate<TypeNamed<IGroup>>;
}>(SEARCH_EVENTS_AND_GROUPS, () => ({
term: searchDebounced.value,
tags: props.tag,
@ -948,5 +1132,7 @@ const { result: searchElementsResult, loading: searchLoading } = useQuery<{
statusOneOf: statusOneOf.value,
languageOneOf: languageOneOf.value,
searchTarget: searchTarget.value,
bbox: bbox.value,
zoom: zoom.value,
}));
</script>

View File

@ -966,14 +966,14 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
"@eslint/eslintrc@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f"
integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==
"@eslint/eslintrc@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.1.tgz#de0807bfeffc37b964a7d0400e0c348ce5a2543d"
integrity sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.3.2"
espree "^9.4.0"
globals "^13.15.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
@ -1061,6 +1061,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
@ -1103,23 +1108,23 @@
source-map "0.6.1"
"@intlify/message-compiler@next":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.2.0.tgz#0516f144bed8274b3ea4c9eede4b9a6c08fd046d"
integrity sha512-KGwwZsl+Nw2O26ZOKdytncxzKnMZ236KmM70u4GePgbizI+pu8yAh0apKxljSPzEJ7WECKTVc9R+laG12EJQYA==
version "9.3.0-beta.1"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.3.0-beta.1.tgz#c8efb0a88af5d97d8ca42ef87561111f59936b0b"
integrity sha512-XHjwJB7qJciYA3T19ehBFpcmC1z+R4sMS43fEp30CLOOFLsrB0xuk0V2XeOFsHovaQ2LsK5x0qk+5+Dy6Hs7fw==
dependencies:
"@intlify/shared" "9.2.0"
"@intlify/shared" "9.3.0-beta.1"
source-map "0.6.1"
"@intlify/shared@9.2.0", "@intlify/shared@next":
version "9.2.0"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.0.tgz#bcd026e419a9eb2e577afe520481ceaca80b3aa9"
integrity sha512-71uObL3Sy2ZiBQBMVETbkspE4Plpy87Hvlj6FAUF3xdD+M82tuxe3MVJjaD3ucqhtHmQWBkAWEurVLdPYr8G2g==
"@intlify/shared@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.2.tgz#5011be9ca2b4ab86f8660739286e2707f9abb4a5"
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
"@intlify/shared@9.3.0-beta.1", "@intlify/shared@next":
version "9.3.0-beta.1"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.1.tgz#4ee8d9f431e5918f256b0198afb18e417a52185b"
integrity sha512-clf9EF4lY0sANjHlEndwfsR2hvYuq0TElq+gO/1xqH3FMGJwv+6lxJPOtoF4r2IE5RV3qX6YyZejZgdfbq2Yfg==
"@intlify/vite-plugin-vue-i18n@^6.0.0":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@intlify/vite-plugin-vue-i18n/-/vite-plugin-vue-i18n-6.0.1.tgz#6beaedc351b6a9fe37f9f23a43c200c56d2c34b6"
@ -1625,6 +1630,11 @@
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==
"@types/hammerjs@^2.0.41":
version "2.0.41"
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa"
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==
"@types/json-schema@*", "@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
@ -1642,6 +1652,13 @@
dependencies:
"@types/leaflet" "*"
"@types/leaflet.markercluster@^1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.1.tgz#d039ada408a30bda733b19a24cba89b81f0ace4b"
integrity sha512-gzJzP10qO6Zkts5QNVmSAEDLYicQHTEBLT9HZpFrJiSww9eDAs5OWHvIskldf41MvDv1gbMukuEBQEawHn+wtA==
dependencies:
"@types/leaflet" "*"
"@types/leaflet@*", "@types/leaflet@^1.5.2":
version "1.7.11"
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.11.tgz#48b33b7a15b015bbb1e8950399298a112c3220c8"
@ -1678,9 +1695,9 @@
integrity sha512-rr20mmx41OkWx4q5du2dv2sESR/6xH2tzScUQXwO8SiaQWa6PYTuan1nqBtA76FR9qkVfZY7nwQwZNC9StX/Ww==
"@types/node@*":
version "18.7.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.13.tgz#23e6c5168333480d454243378b69e861ab5c011a"
integrity sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==
version "18.7.14"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.14.tgz#0fe081752a3333392d00586d815485a17c2cf3c9"
integrity sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==
"@types/node@^10.0.3":
version "10.17.60"
@ -1777,13 +1794,13 @@
integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA==
"@typescript-eslint/eslint-plugin@^5.0.0":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.35.1.tgz#0d822bfea7469904dfc1bb8f13cabd362b967c93"
integrity sha512-RBZZXZlI4XCY4Wzgy64vB+0slT9+yAPQRjj/HSaRwUot33xbDjF1oN9BLwOLTewoOI0jothIltZRe9uJCHf8gg==
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.0.tgz#8f159c4cdb3084eb5d4b72619a2ded942aa109e5"
integrity sha512-X3In41twSDnYRES7hO2xna4ZC02SY05UN9sGW//eL1P5k4CKfvddsdC2hOq0O3+WU1wkCPQkiTY9mzSnXKkA0w==
dependencies:
"@typescript-eslint/scope-manager" "5.35.1"
"@typescript-eslint/type-utils" "5.35.1"
"@typescript-eslint/utils" "5.35.1"
"@typescript-eslint/scope-manager" "5.36.0"
"@typescript-eslint/type-utils" "5.36.0"
"@typescript-eslint/utils" "5.36.0"
debug "^4.3.4"
functional-red-black-tree "^1.0.1"
ignore "^5.2.0"
@ -1792,68 +1809,69 @@
tsutils "^3.21.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.10.0":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.35.1.tgz#bf2ee2ebeaa0a0567213748243fb4eec2857f04f"
integrity sha512-XL2TBTSrh3yWAsMYpKseBYTVpvudNf69rPOWXWVBI08My2JVT5jR66eTt4IgQFHA/giiKJW5dUD4x/ZviCKyGg==
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.0.tgz#c08883073fb65acaafd268a987fd2314ce80c789"
integrity sha512-dlBZj7EGB44XML8KTng4QM0tvjI8swDh8MdpE5NX5iHWgWEfIuqSfSE+GPeCrCdj7m4tQLuevytd57jNDXJ2ZA==
dependencies:
"@typescript-eslint/scope-manager" "5.35.1"
"@typescript-eslint/types" "5.35.1"
"@typescript-eslint/typescript-estree" "5.35.1"
"@typescript-eslint/scope-manager" "5.36.0"
"@typescript-eslint/types" "5.36.0"
"@typescript-eslint/typescript-estree" "5.36.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.35.1":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.35.1.tgz#ccb69d54b7fd0f2d0226a11a75a8f311f525ff9e"
integrity sha512-kCYRSAzIW9ByEIzmzGHE50NGAvAP3wFTaZevgWva7GpquDyFPFcmvVkFJGWJJktg/hLwmys/FZwqM9EKr2u24Q==
"@typescript-eslint/scope-manager@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.0.tgz#f4f859913add160318c0a5daccd3a030d1311530"
integrity sha512-PZUC9sz0uCzRiuzbkh6BTec7FqgwXW03isumFVkuPw/Ug/6nbAqPUZaRy4w99WCOUuJTjhn3tMjsM94NtEj64g==
dependencies:
"@typescript-eslint/types" "5.35.1"
"@typescript-eslint/visitor-keys" "5.35.1"
"@typescript-eslint/types" "5.36.0"
"@typescript-eslint/visitor-keys" "5.36.0"
"@typescript-eslint/type-utils@5.35.1":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.35.1.tgz#d50903b56758c5c8fc3be52b3be40569f27f9c4a"
integrity sha512-8xT8ljvo43Mp7BiTn1vxLXkjpw8wS4oAc00hMSB4L1/jIiYbjjnc3Qp2GAUOG/v8zsNCd1qwcqfCQ0BuishHkw==
"@typescript-eslint/type-utils@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.36.0.tgz#5d2f94a36a298ae240ceca54b3bc230be9a99f0a"
integrity sha512-W/E3yJFqRYsjPljJ2gy0YkoqLJyViWs2DC6xHkXcWyhkIbCDdaVnl7mPLeQphVI+dXtY05EcXFzWLXhq8Mm/lQ==
dependencies:
"@typescript-eslint/utils" "5.35.1"
"@typescript-eslint/typescript-estree" "5.36.0"
"@typescript-eslint/utils" "5.36.0"
debug "^4.3.4"
tsutils "^3.21.0"
"@typescript-eslint/types@5.35.1":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.35.1.tgz#af355fe52a0cc88301e889bc4ada72f279b63d61"
integrity sha512-FDaujtsH07VHzG0gQ6NDkVVhi1+rhq0qEvzHdJAQjysN+LHDCKDKCBRlZFFE0ec0jKxiv0hN63SNfExy0KrbQQ==
"@typescript-eslint/types@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.0.tgz#cde7b94d1c09a4f074f46db99e7bd929fb0a5559"
integrity sha512-3JJuLL1r3ljRpFdRPeOtgi14Vmpx+2JcR6gryeORmW3gPBY7R1jNYoq4yBN1L//ONZjMlbJ7SCIwugOStucYiQ==
"@typescript-eslint/typescript-estree@5.35.1":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.35.1.tgz#db878a39a0dbdc9bb133f11cdad451770bfba211"
integrity sha512-JUqE1+VRTGyoXlDWWjm6MdfpBYVq+hixytrv1oyjYIBEOZhBCwtpp5ZSvBt4wIA1MKWlnaC2UXl2XmYGC3BoQA==
"@typescript-eslint/typescript-estree@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.0.tgz#0acce61b4850bdb0e578f0884402726680608789"
integrity sha512-EW9wxi76delg/FS9+WV+fkPdwygYzRrzEucdqFVWXMQWPOjFy39mmNNEmxuO2jZHXzSQTXzhxiU1oH60AbIw9A==
dependencies:
"@typescript-eslint/types" "5.35.1"
"@typescript-eslint/visitor-keys" "5.35.1"
"@typescript-eslint/types" "5.36.0"
"@typescript-eslint/visitor-keys" "5.36.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.35.1":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.35.1.tgz#ae1399afbfd6aa7d0ed1b7d941e9758d950250eb"
integrity sha512-v6F8JNXgeBWI4pzZn36hT2HXXzoBBBJuOYvoQiaQaEEjdi5STzux3Yj8v7ODIpx36i/5s8TdzuQ54TPc5AITQQ==
"@typescript-eslint/utils@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.36.0.tgz#104c864ecc1448417606359275368bf3872bbabb"
integrity sha512-wAlNhXXYvAAUBbRmoJDywF/j2fhGLBP4gnreFvYvFbtlsmhMJ4qCKVh/Z8OP4SgGR3xbciX2nmG639JX0uw1OQ==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.35.1"
"@typescript-eslint/types" "5.35.1"
"@typescript-eslint/typescript-estree" "5.35.1"
"@typescript-eslint/scope-manager" "5.36.0"
"@typescript-eslint/types" "5.36.0"
"@typescript-eslint/typescript-estree" "5.36.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/visitor-keys@5.35.1":
version "5.35.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.35.1.tgz#285e9e34aed7c876f16ff646a3984010035898e6"
integrity sha512-cEB1DvBVo1bxbW/S5axbGPE6b7FIMAbo3w+AGq6zNDA7+NYJOIkKj/sInfTv4edxd4PxJSgdN4t6/pbvgA+n5g==
"@typescript-eslint/visitor-keys@5.36.0":
version "5.36.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.0.tgz#565d35a5ca00d00a406a942397ead2cb190663ba"
integrity sha512-pdqSJwGKueOrpjYIex0T39xarDt1dn4p7XJ+6FqBWugNQwXlNGC5h62qayAIYZ/RPPtD+ButDWmpXT1eGtiaYg==
dependencies:
"@typescript-eslint/types" "5.35.1"
"@typescript-eslint/types" "5.36.0"
eslint-visitor-keys "^3.3.0"
"@vitejs/plugin-vue@^3.0.3":
@ -1892,47 +1910,47 @@
ts-essentials "^9.1.2"
vue-demi "^0.13.1"
"@vue/compiler-core@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==
"@vue/compiler-core@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7"
integrity sha512-/FsvnSu7Z+lkd/8KXMa4yYNUiqQrI22135gfsQYVGuh5tqEgOB0XqrUdb/KnCLa5+TmQLPwvyUnKMyCpu+SX3Q==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.37"
"@vue/shared" "3.2.38"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-dom@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5"
integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==
"@vue/compiler-dom@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.38.tgz#53d04ed0c0c62d1ef259bf82f9b28100a880b6fd"
integrity sha512-zqX4FgUbw56kzHlgYuEEJR8mefFiiyR3u96498+zWPsLeh1WKvgIReoNE+U7gG8bCUdvsrJ0JRmev0Ky6n2O0g==
dependencies:
"@vue/compiler-core" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-core" "3.2.38"
"@vue/shared" "3.2.38"
"@vue/compiler-sfc@3.2.37", "@vue/compiler-sfc@^3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4"
integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==
"@vue/compiler-sfc@3.2.38", "@vue/compiler-sfc@^3.2.37":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.38.tgz#9e763019471a535eb1fceeaac9d4d18a83f0940f"
integrity sha512-KZjrW32KloMYtTcHAFuw3CqsyWc5X6seb8KbkANSWt3Cz9p2qA8c1GJpSkksFP9ABb6an0FLCFl46ZFXx3kKpg==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.37"
"@vue/compiler-dom" "3.2.37"
"@vue/compiler-ssr" "3.2.37"
"@vue/reactivity-transform" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-core" "3.2.38"
"@vue/compiler-dom" "3.2.38"
"@vue/compiler-ssr" "3.2.38"
"@vue/reactivity-transform" "3.2.38"
"@vue/shared" "3.2.38"
estree-walker "^2.0.2"
magic-string "^0.25.7"
postcss "^8.1.10"
source-map "^0.6.1"
"@vue/compiler-ssr@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff"
integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==
"@vue/compiler-ssr@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.38.tgz#933b23bf99e667e5078eefc6ba94cb95fd765dfe"
integrity sha512-bm9jOeyv1H3UskNm4S6IfueKjUNFmi2kRweFIGnqaGkkRePjwEcfCVqyS3roe7HvF4ugsEkhf4+kIvDhip6XzQ==
dependencies:
"@vue/compiler-dom" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-dom" "3.2.38"
"@vue/shared" "3.2.38"
"@vue/devtools-api@^6.1.4", "@vue/devtools-api@^6.2.1":
version "6.2.1"
@ -1956,53 +1974,53 @@
"@typescript-eslint/parser" "^5.0.0"
vue-eslint-parser "^9.0.0"
"@vue/reactivity-transform@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca"
integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==
"@vue/reactivity-transform@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.38.tgz#a856c217b2ead99eefb6fddb1d61119b2cb67984"
integrity sha512-3SD3Jmi1yXrDwiNJqQ6fs1x61WsDLqVk4NyKVz78mkaIRh6d3IqtRnptgRfXn+Fzf+m6B1KxBYWq1APj6h4qeA==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-core" "3.2.38"
"@vue/shared" "3.2.38"
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.37.tgz#5bc3847ac58828e2b78526e08219e0a1089f8848"
integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==
"@vue/reactivity@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.38.tgz#d576fdcea98eefb96a1f1ad456e289263e87292e"
integrity sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw==
dependencies:
"@vue/shared" "3.2.37"
"@vue/shared" "3.2.38"
"@vue/runtime-core@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz#7ba7c54bb56e5d70edfc2f05766e1ca8519966e3"
integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==
"@vue/runtime-core@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.38.tgz#d19cf591c210713f80e6a94ffbfef307c27aea06"
integrity sha512-kk0qiSiXUU/IKxZw31824rxmFzrLr3TL6ZcbrxWTKivadoKupdlzbQM4SlGo4MU6Zzrqv4fzyUasTU1jDoEnzg==
dependencies:
"@vue/reactivity" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/reactivity" "3.2.38"
"@vue/shared" "3.2.38"
"@vue/runtime-dom@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz#002bdc8228fa63949317756fb1e92cdd3f9f4bbd"
integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==
"@vue/runtime-dom@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.38.tgz#fec711f65c2485991289fd4798780aa506469b48"
integrity sha512-4PKAb/ck2TjxdMSzMsnHViOrrwpudk4/A56uZjhzvusoEU9xqa5dygksbzYepdZeB5NqtRw5fRhWIiQlRVK45A==
dependencies:
"@vue/runtime-core" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/runtime-core" "3.2.38"
"@vue/shared" "3.2.38"
csstype "^2.6.8"
"@vue/server-renderer@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz#840a29c8dcc29bddd9b5f5ffa22b95c0e72afdfc"
integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==
"@vue/server-renderer@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.38.tgz#01a4c0f218e90b8ad1815074208a1974ded109aa"
integrity sha512-pg+JanpbOZ5kEfOZzO2bt02YHd+ELhYP8zPeLU1H0e7lg079NtuuSB8fjLdn58c4Ou8UQ6C1/P+528nXnLPAhA==
dependencies:
"@vue/compiler-ssr" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-ssr" "3.2.38"
"@vue/shared" "3.2.38"
"@vue/shared@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
"@vue/shared@3.2.38":
version "3.2.38"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.38.tgz#e823f0cb2e85b6bf43430c0d6811b1441c300f3c"
integrity sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==
"@vue/test-utils@^2.0.2":
version "2.0.2"
@ -2438,9 +2456,9 @@ bulma@^0.9.4:
integrity sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==
cac@^6.7.12:
version "6.7.12"
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.12.tgz#6fb5ea2ff50bd01490dbda497f4ae75a99415193"
integrity sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==
version "6.7.14"
resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
@ -2461,9 +2479,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001373:
version "1.0.30001383"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001383.tgz#aecf317ccd940690725ae3ae4f28293c5fb8050e"
integrity sha512-swMpEoTp5vDoGBZsYZX7L7nXHe6dsHxi9o6/LKf/f0LukVtnrxly5GVb/fWdCDTqi/yw6Km6tiJ0pmBacm0gbg==
version "1.0.30001385"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001385.tgz#51d5feeb60b831a5b4c7177f419732060418535c"
integrity sha512-MpiCqJGhBkHgpyimE9GWmZTnyHyEEM35u115bD3QBrXpjvL/JgcP8cUhKJshfmg4OtEHFenifcK5sZayEw5tvQ==
case@^1.6.3:
version "1.6.3"
@ -2885,9 +2903,9 @@ ejs@^3.1.6:
jake "^10.8.5"
electron-to-chromium@^1.4.202:
version "1.4.232"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.232.tgz#67a0a874b0057662244230d18d3a9847c135a9d9"
integrity sha512-nd+FW8xHjM+PxNWG44nKnwHaBDdVpJUZuI2sS2JJPt/QpdombnmoCRWEEQNnzaktdIQhsNWdD+dlqxwO8Bn99g==
version "1.4.234"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.234.tgz#ff8fb29c2edac74ef3935ca03a2ecbe62ede400b"
integrity sha512-VqlJ4Ihd9F7eQIfwEtf7C0eZZDl6bQtpez8vx8VHN9iCZEzePZjr7n9OGFHSav4WN9zfLa2CFLowj0siBoc0hQ==
emoji-regex@^8.0.0:
version "8.0.0"
@ -3223,13 +3241,14 @@ eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0:
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@^8.21.0, eslint@^8.7.0:
version "8.22.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.22.0.tgz#78fcb044196dfa7eef30a9d65944f6f980402c48"
integrity sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==
version "8.23.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.0.tgz#a184918d288820179c6041bb3ddcc99ce6eea040"
integrity sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==
dependencies:
"@eslint/eslintrc" "^1.3.0"
"@eslint/eslintrc" "^1.3.1"
"@humanwhocodes/config-array" "^0.10.4"
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
"@humanwhocodes/module-importer" "^1.0.1"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@ -3239,7 +3258,7 @@ eslint@^8.21.0, eslint@^8.7.0:
eslint-scope "^7.1.1"
eslint-utils "^3.0.0"
eslint-visitor-keys "^3.3.0"
espree "^9.3.3"
espree "^9.4.0"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@ -3265,7 +3284,6 @@ eslint@^8.21.0, eslint@^8.7.0:
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^6.0.0:
version "6.2.1"
@ -3276,10 +3294,10 @@ espree@^6.0.0:
acorn-jsx "^5.2.0"
eslint-visitor-keys "^1.1.0"
espree@^9.0.0, espree@^9.3.1, espree@^9.3.2, espree@^9.3.3:
version "9.3.3"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d"
integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==
espree@^9.0.0, espree@^9.3.1, espree@^9.4.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a"
integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==
dependencies:
acorn "^8.8.0"
acorn-jsx "^5.3.2"
@ -3692,6 +3710,11 @@ gray-matter@^4.0.3:
section-matter "^1.0.0"
strip-bom-string "^1.0.0"
hammerjs@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
integrity sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==
happy-dom@^2.55.0:
version "2.55.0"
resolved "https://registry.yarnpkg.com/happy-dom/-/happy-dom-2.55.0.tgz#ad412939fea9b97f2e5985c404cf710638bba66a"
@ -4256,6 +4279,11 @@ leaflet.locatecontrol@^0.76.0:
resolved "https://registry.yarnpkg.com/leaflet.locatecontrol/-/leaflet.locatecontrol-0.76.1.tgz#03eb8e98e0c54fee457f930a63624479a6177de0"
integrity sha512-qA92Mxs2N1jgVx+EdmxtDrdzFD+f2llPJbqaKvmW1epZMSIvD6KNsBjpQYUIxz4XtJkOleqRSwWQcrm5P5NnYw==
leaflet.markercluster@^1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056"
integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==
leaflet@^1.4.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.8.0.tgz#4615db4a22a304e8e692cae9270b983b38a2055e"
@ -5304,9 +5332,9 @@ sanitize-html@^2.5.3:
postcss "^8.3.11"
sass@^1.34.1:
version "1.54.5"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.5.tgz#93708f5560784f6ff2eab8542ade021a4a947b3a"
integrity sha512-p7DTOzxkUPa/63FU0R3KApkRHwcVZYC0PLnLm5iyZACyp15qSi32x7zVUhRdABAATmkALqgGrjCJAcWvobmhHw==
version "1.54.6"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.6.tgz#5a12c268db26555c335028e355d6b7b1a5b9b4c8"
integrity sha512-DUqJjR2WxXBcZjRSZX5gCVyU+9fuC2qDfFzoKX9rV4rCOcec5mPtEafTcfsyL3YJuLONjWylBne+uXVh5rrmFw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@ -5937,11 +5965,6 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
vite-node@0.22.1:
version "0.22.1"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.22.1.tgz#a187e3d73094e29ab82310bb7f5a63da1038a882"
@ -6115,15 +6138,15 @@ vue-use-route-query@^1.1.0:
vue-demi "^0.13.1"
vue@^3.2.37:
version "3.2.37"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.37.tgz#da220ccb618d78579d25b06c7c21498ca4e5452e"
integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==
version "3.2.38"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.38.tgz#cda3a414631745b194971219318a792dbbccdec0"
integrity sha512-hHrScEFSmDAWL0cwO4B6WO7D3sALZPbfuThDsGBebthrNlDxdJZpGR3WB87VbjpPh96mep1+KzukYEhpHDFa8Q==
dependencies:
"@vue/compiler-dom" "3.2.37"
"@vue/compiler-sfc" "3.2.37"
"@vue/runtime-dom" "3.2.37"
"@vue/server-renderer" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-dom" "3.2.38"
"@vue/compiler-sfc" "3.2.38"
"@vue/runtime-dom" "3.2.38"
"@vue/server-renderer" "3.2.38"
"@vue/shared" "3.2.38"
w3c-hr-time@^1.0.2:
version "1.0.2"

View File

@ -52,6 +52,7 @@ defmodule Mobilizon.GraphQL.API.Search do
actor_type: result_type,
radius: Map.get(args, :radius),
location: Map.get(args, :location),
bbox: Map.get(args, :bbox),
minimum_visibility: Map.get(args, :minimum_visibility, :public),
current_actor_id: Map.get(args, :current_actor_id),
exclude_my_groups: Map.get(args, :exclude_my_groups, false),

View File

@ -189,6 +189,9 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "The target of the search (internal or global)"
)
arg(:bbox, :string, description: "The bbox to search groups into")
arg(:zoom, :integer, description: "The zoom level for searching groups")
arg(:page, :integer, default_value: 1, description: "Result page")
arg(:limit, :integer, default_value: 10, description: "Results limit per page")
@ -225,6 +228,9 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "Radius around the location to search in"
)
arg(:bbox, :string, description: "The bbox to search events into")
arg(:zoom, :integer, description: "The zoom level for searching events")
arg(:page, :integer, default_value: 1, description: "Result page")
arg(:limit, :integer, default_value: 10, description: "Results limit per page")
arg(:begins_on, :datetime, description: "Filter events by their start date")

View File

@ -512,7 +512,13 @@ defmodule Mobilizon.Actors do
query
|> distinct([q], q.id)
|> actor_by_username_or_name_query(term)
|> maybe_join_address(
Keyword.get(options, :location),
Keyword.get(options, :radius),
Keyword.get(options, :bbox)
)
|> actors_for_location(Keyword.get(options, :location), Keyword.get(options, :radius))
|> events_for_bounding_box(Keyword.get(options, :bbox))
|> filter_by_type(Keyword.get(options, :actor_type, :Group))
|> filter_by_minimum_visibility(Keyword.get(options, :minimum_visibility, :public))
|> filter_suspended(false)
@ -1385,15 +1391,27 @@ defmodule Mobilizon.Actors do
)
end
@spec maybe_join_address(
Ecto.Queryable.t(),
String.t() | nil,
integer() | nil,
String.t() | nil
) :: Ecto.Query.t()
defp maybe_join_address(query, location, radius, bbox)
when (is_valid_string(location) and not is_nil(radius)) or is_valid_string(bbox) do
join(query, :inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
end
defp maybe_join_address(query, _location, _radius, _bbox), do: query
@spec actors_for_location(Ecto.Queryable.t(), String.t(), integer()) :: Ecto.Query.t()
defp actors_for_location(query, location, radius)
when is_valid_string(location) and not is_nil(radius) do
{lon, lat} = Geohax.decode(location)
point = Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})")
query
|> join(:inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
|> where(
where(
query,
[q],
st_dwithin_in_meters(^point, as(:address).geom, ^(radius * 1000))
)
@ -1401,6 +1419,36 @@ defmodule Mobilizon.Actors do
defp actors_for_location(query, _location, _radius), do: query
defp events_for_bounding_box(query, bbox) when is_valid_string(bbox) do
[top_left, bottom_right] = String.split(bbox, ":")
[ymax, xmin] = String.split(top_left, ",")
[ymin, xmax] = String.split(bottom_right, ",")
where(
query,
[q, ..., a],
fragment(
"? @ ST_MakeEnvelope(?,?,?,?,?)",
a.geom,
^sanitize_bounding_box_params(xmin),
^sanitize_bounding_box_params(ymin),
^sanitize_bounding_box_params(xmax),
^sanitize_bounding_box_params(ymax),
"4326"
)
)
end
defp events_for_bounding_box(query, _args), do: query
@spec sanitize_bounding_box_params(String.t()) :: float()
defp sanitize_bounding_box_params(param) do
param
|> String.trim()
|> String.to_float()
|> Float.floor(10)
end
@spec person_query :: Ecto.Query.t()
defp person_query do
from(a in Actor, where: a.type == ^:Person)

View File

@ -534,7 +534,9 @@ defmodule Mobilizon.Events do
|> events_for_languages(args)
|> events_for_statuses(args)
|> events_for_tags(args)
|> maybe_join_address(args)
|> events_for_location(args)
|> events_for_bounding_box(args)
|> filter_online(args)
|> filter_draft()
|> filter_local_or_from_followed_instances_events()
@ -1355,6 +1357,20 @@ defmodule Mobilizon.Events do
defp events_for_tags(query, _args), do: query
# We add the inner join on address only if we're going to use it in
# events_for_location or events_for_bounding_box
# So we're sure it's only being added once
defp maybe_join_address(query, %{location: location, radius: radius})
when is_valid_string(location) and not is_nil(radius) do
join(query, :inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
end
defp maybe_join_address(query, %{bbox: bbox}) when is_valid_string(bbox) do
join(query, :inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
end
defp maybe_join_address(query, _), do: query
@spec events_for_location(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
defp events_for_location(query, %{radius: radius}) when is_nil(radius),
do: query
@ -1364,9 +1380,8 @@ defmodule Mobilizon.Events do
with {lon, lat} <- Geohax.decode(location),
point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})"),
{{x_min, y_min}, {x_max, y_max}} <- search_box({lon, lat}, radius) do
query
|> join(:inner, [q], a in Address, on: a.id == q.physical_address_id, as: :address)
|> where(
where(
query,
[q, ..., a],
st_x(a.geom) > ^x_min and st_x(a.geom) < ^x_max and
st_y(a.geom) > ^y_min and st_y(a.geom) < ^y_max and
@ -1379,6 +1394,36 @@ defmodule Mobilizon.Events do
defp events_for_location(query, _args), do: query
defp events_for_bounding_box(query, %{bbox: bbox}) when is_valid_string(bbox) do
[top_left, bottom_right] = String.split(bbox, ":")
[ymax, xmin] = String.split(top_left, ",")
[ymin, xmax] = String.split(bottom_right, ",")
where(
query,
[q, ..., a],
fragment(
"? @ ST_MakeEnvelope(?,?,?,?,?)",
a.geom,
^sanitize_bounding_box_params(xmin),
^sanitize_bounding_box_params(ymin),
^sanitize_bounding_box_params(xmax),
^sanitize_bounding_box_params(ymax),
"4326"
)
)
end
defp events_for_bounding_box(query, _args), do: query
@spec sanitize_bounding_box_params(String.t()) :: float()
defp sanitize_bounding_box_params(param) do
param
|> String.trim()
|> String.to_float()
|> Float.floor(10)
end
@spec search_box({float(), float()}, float()) :: {{float, float}, {float, float}}
defp search_box({lon0, lat0}, radius) do
km_per_lat_deg = 111.195

View File

@ -13,6 +13,7 @@ defmodule Mobilizon.Service.GlobalSearch.EventResult do
:category,
:tags,
:organizer_actor,
:participants
:participants,
:physical_address
]
end

View File

@ -40,7 +40,8 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
count: options[:limit],
start: (options[:page] - 1) * options[:limit],
latlon: to_lat_lon(options[:location])
latlon: to_lat_lon(options[:location]),
bbox: options[:bbox]
)
|> Keyword.take([
:search,
@ -53,6 +54,7 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
:distance,
:sort,
:statusOneOf,
:bbox,
:start,
:count
])
@ -86,7 +88,8 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
distance: if(options[:radius], do: "#{options[:radius]}_km", else: nil),
count: options[:limit],
start: (options[:page] - 1) * options[:limit],
latlon: to_lat_lon(options[:location])
latlon: to_lat_lon(options[:location]),
bbox: options[:bbox]
)
|> Keyword.take([
:search,
@ -95,7 +98,8 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
:distance,
:sort,
:start,
:count
:count,
:bbox
])
|> Keyword.reject(fn {_key, val} -> is_nil(val) end)
@ -138,6 +142,27 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
nil
end
address =
if data["location"] do
%Address{
id: data["location"]["id"],
country: data["location"]["address"]["addressCountry"],
locality: data["location"]["address"]["addressLocality"],
region: data["location"]["address"]["addressRegion"],
postal_code: data["location"]["address"]["postalCode"],
street: data["location"]["address"]["streetAddress"],
url: data["location"]["id"],
description: data["location"]["name"],
geom: %Geo.Point{
coordinates:
{data["location"]["location"]["lon"], data["location"]["location"]["lat"]},
srid: 4326
}
}
else
nil
end
%EventResult{
id: data["id"],
uuid: data["uuid"],
@ -153,6 +178,7 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
preferred_username: data["creator"]["name"],
avatar: organizer_actor_avatar
},
physical_address: address,
tags:
Enum.map(data["tags"], fn tag ->
tag = String.trim_leading(tag, "#")
@ -224,12 +250,7 @@ defmodule Mobilizon.Service.GlobalSearch.SearchMobilizon do
defp to_lat_lon(nil), do: nil
defp to_lat_lon(location) do
case Geohax.decode(location) do
{lon, lat} ->
"#{lat}:#{lon}"
_ ->
nil
end
{lon, lat} = Geohax.decode(location)
"#{lat}:#{lon}"
end
end