Skip to content

Commit

Permalink
feat: documents as map
Browse files Browse the repository at this point in the history
  • Loading branch information
bpolaszek committed Apr 25, 2024
1 parent c4df887 commit fd12868
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 9 deletions.
87 changes: 87 additions & 0 deletions components/documents/DocumentsAsMap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<template>
<main class="h-full grid grid-cols-12 overflow-hidden">
<div
class="col-span-3 h-full px-4 pb-4 overflow-x-hidden overflow-y-auto space-y-6">
<DocumentCard
v-for="document of documents"
:indexUid="indexUid"
:document
:primary-key="primaryKey"
:key="document[primaryKey]"
:id="`document-${document[primaryKey]}`"
class="w-full" />
</div>
<MapContainer
:center="center"
:zoom="3"
class="col-span-9 size-full"
style="height: 100%; z-index: 0"
@zoomend="onZoomEnd">
<OpenStreetMap>
<ScaleControl />
<template v-for="document of documents">
<Marker
v-if="Object.keys(document).includes('_geo')"
:position="document._geo"
@click="scrollToDocument(document)">
<Popup>{{ document[nameField] }}</Popup>
</Marker>
</template>
</OpenStreetMap>
</MapContainer>
</main>
</template>

<script setup lang="ts">
import DocumentCard from './DocumentCard.vue'
import {
MapContainer,
Marker,
OpenStreetMap,
Popup,
ScaleControl,
} from 'vue3-leaflet'
import { useFields } from '~/composables'
import { AppliedFilters } from '~/utils'
type Props = {
indexUid: string
primaryKey: string
documents: Array<any>
fields: Array<string>
appliedFilters: AppliedFilters
canFilterGeoDocuments: boolean
}
type ZoomEndEvent = {
bounds: {
_northEast: {
lat: number
lng: number
}
_southWest: {
lat: number
lng: number
}
}
}
const props = defineProps<Props>()
const { nameField } = useFields(props.primaryKey, props.fields)
const center = ref([48.3151, 3.68461])
const scrollToDocument = (doc: any) => {
document
.getElementById(`document-${doc[props.primaryKey]}`)
?.scrollIntoView({ behavior: 'smooth' })
}
const onZoomEnd = ({ bounds }: ZoomEndEvent) => {
if (!props.canFilterGeoDocuments) {
return
}
const boundingBox = {
topLeftCorner: Object.values(bounds._northEast) as [number, number],
bottomRightCorner: Object.values(bounds._southWest) as [number, number],
}
props.appliedFilters.applyBoundingBox(boundingBox)
}
</script>
2 changes: 1 addition & 1 deletion components/documents/ValueRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</a>
<span v-else-if="'object' === typeof value">{{ prettify(value) }}</span>
<Badge v-else-if="renderAsBadge">{{ value }}</Badge>
<span v-else>{{ value }}</span>
<span v-else class="line-clamp-1">{{ value }}</span>
</template>

<script setup lang="ts">
Expand Down
2 changes: 1 addition & 1 deletion composables/useIndexLocalSettings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLocalStorage } from '@vueuse/core'
import { type CredentialsRecord, useCredentials } from '~/stores'

type ViewMode = 'table' | 'documents'
type ViewMode = 'table' | 'documents' | 'map'
type UpdateMode = 'replace' | 'update'
type UseIndexLocalSettings = {
attributesAsDateTime: Array<string>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"typescript-eslint": "^7.6.0",
"ulid": "^2.3.0",
"vue-tippy": "v6",
"vue3-leaflet": "git+https://git@github.com/geonativefr/vue3-leaflet",
"yaml": "^2.4.1"
},
"resolutions": {
Expand Down
36 changes: 34 additions & 2 deletions pages/indexes/[indexUid]/documents/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<Layout :title="humanizeString(index.uid)" :subtitle="subtitle">
<Layout
:title="humanizeString(index.uid)"
:subtitle="subtitle"
:nowrap="'map' === viewMode">
<SlideOver
no-padding
v-model:open="filterPanelOpen"
Expand Down Expand Up @@ -41,6 +44,20 @@
'size-6',
]" />
</button>
<button
v-if="hasGeoDocuments"
v-tippy="t('actions.mapView')"
type="button"
@click="viewMode = 'map'">
<Icon
name="gis:poi-map"
:class="[
'map' === viewMode
? 'text-primary-700'
: 'text-gray-600 hover:text-gray-800',
'size-6',
]" />
</button>
<button
v-tippy="t('actions.toggleFilters')"
@click="filterPanelOpen = true">
Expand Down Expand Up @@ -81,6 +98,8 @@
:index-uid="index.uid"
:documents="resultset.hits"
:primary-key="primaryKey"
:applied-filters="appliedFilters"
:can-filter-geo-documents="canFilterGeoDocuments"
:fields="fields" />

<template #footer>
Expand Down Expand Up @@ -115,6 +134,7 @@ import match from 'match-operator'
import DocumentsAsCards from '~/components/documents/DocumentsAsCards.vue'
import DocumentsAsTable from '~/components/documents/DocumentsAsTable.vue'
import Button from '~/components/layout/forms/Button.vue'
import DocumentsAsMap from '~/components/documents/DocumentsAsMap.vue'
const { t } = useI18n()
const route = useRoute()
Expand Down Expand Up @@ -147,11 +167,22 @@ const searchParams = reactive({
filter: computed(() => `${appliedFilters}`),
})
const resultset = ref(await index.search(null, searchParams))
const self = reactive({ resultset, totalItems, viewMode })
const hasGeoDocuments = computed(() =>
Object.keys(stats.fieldDistribution).includes('_geo'),
)
const canFilterGeoDocuments = computed(() =>
filterableAttributes.includes('_geo'),
)
const self = reactive({
resultset,
totalItems,
viewMode,
})
const MainComponent = computed(() =>
match(self.viewMode, [
['documents', DocumentsAsCards],
['table', DocumentsAsTable],
['map', DocumentsAsMap],
]),
)
watch(appliedSort, () => (searchParams.offset = 0))
Expand Down Expand Up @@ -184,6 +215,7 @@ en:
goToSettings: Go to Index Settings
documentView: View as documents
tableView: View as data table
mapView: View as map
toggleFilters: Open filter panel
labels:
filters: Sort & Filter
Expand Down
31 changes: 28 additions & 3 deletions utils/applied-filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import match from 'match-operator'
import { field, filterBuilder } from 'meilisearch-filters'
import { field, filterBuilder, withinGeoBoundingBox } from 'meilisearch-filters'

export enum StringFilterStatus {
EXCLUDE,
Expand All @@ -13,11 +13,20 @@ type AppliedStringFacetValues = Map<FacetValue, StringFilterStatus>
type AppliedRangeFacets = Map<FacetName, AppliedRange>
type AppliedRange = [number, number]

type Latitude = number
type Longitude = number
type Coordinates = [Latitude, Longitude]
type GeoBoundingBox = {
topLeftCorner: Coordinates
bottomRightCorner: Coordinates
}

export class AppliedFilters {
public length: number = 0
constructor(
private appliedStringFilters: AppliedStringFacets = new Map(),
private appliedRangeFilters: AppliedRangeFacets = new Map(),
private appliedBoundingBox: GeoBoundingBox | null = null,
) {}

applyStringFilter(facetName: FacetName, facetValue: FacetValue) {
Expand Down Expand Up @@ -53,6 +62,11 @@ export class AppliedFilters {
this.appliedRangeFilters.set(facetName, range)
}

applyBoundingBox(boundingBox: GeoBoundingBox) {
this.length++
this.appliedBoundingBox = boundingBox
}

getAppliedFacet(facetName: FacetName): AppliedStringFacetValues {
if (!this.appliedStringFilters.has(facetName)) {
this.appliedStringFilters.set(facetName, reactive(new Map()))
Expand Down Expand Up @@ -97,9 +111,20 @@ export class AppliedFilters {
},
)

return filterBuilder(
let expression = filterBuilder(
...appliedStringFilters,
...appliedRangeFilters,
).toString()
)

if (this.appliedBoundingBox) {
expression = expression.and(
withinGeoBoundingBox(
this.appliedBoundingBox.topLeftCorner,
this.appliedBoundingBox.bottomRightCorner,
),
)
}

return expression.toString()
}
}
Loading

0 comments on commit fd12868

Please sign in to comment.