Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modals and tooltips #25

Merged
merged 9 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions app/Helpers/WikipediaSearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,24 @@ protected function getExtractByTitle($title)
'action' => 'query',
'titles' => $title,
'format' => 'json',
'prop' => 'extracts',
'prop' => 'extracts|info',
'explaintext' => 'true',
'exintro' => true,
'inprop' => 'url',
];

$response = $this->performRequest($params);

$data = json_decode($response->getBody(), true);
$page = current($data['query']['pages']);

return isset($page['extract']) ? $page['extract'] : null;
$extract = isset($page['extract']) ? $page['extract'] : null;
$fullUrl = isset($page['fullurl']) ? $page['fullurl'] : null;

return [
'extract' => $extract,
'fullurl' => $fullUrl,
];
}

protected function getSearchResults($query, $limit = 5)
Expand Down Expand Up @@ -66,12 +73,12 @@ protected function findBestMatch($query, $results)
return $bestMatch;
}

public function query($query): ?string
public function query($query): ?array
{
$exactMatchExtract = $this->getExtractByTitle($query);
$exactMatchData = $this->getExtractByTitle($query);

if (! empty($exactMatchExtract)) {
return $exactMatchExtract;
if (! empty($exactMatchData['extract'])) {
return $exactMatchData;
}

$searchResults = $this->getSearchResults($query);
Expand All @@ -86,8 +93,8 @@ public function query($query): ?string
return null;
}

$extract = $this->getExtractByTitle($bestMatch['title']);
$extractData = $this->getExtractByTitle($bestMatch['title']);

return $extract;
return $extractData;
}
}
10 changes: 8 additions & 2 deletions app/Jobs/UpdatePlacesDescriptionsJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ public function handle(): void
foreach ($spatialFeatures as $spatialFeature) {
$data = $wikipediaSearch->query($spatialFeature->name);

$spatialFeature->description = $data;
if ($data !== null) {
$description = $data['extract'];
$sourceLink = $data['fullurl'];

$spatialFeature->save();
$spatialFeature->description = $description;
$spatialFeature->description_source = $sourceLink;

$spatialFeature->save();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('spatial_features', function (Blueprint $table) {
$table->string('description_source')->nullable()->after('description');
});
}

/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('spatial_features', function (Blueprint $table) {
$table->dropColumn('description_source');
});
}
};
12 changes: 11 additions & 1 deletion resources/js/Components/Maplibre/Map.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, defineProps } from 'vue';
import { ref, onMounted, onBeforeUnmount, defineProps, watch } from 'vue';
import { addControls } from './Partials/mapControls.js';
import { loadGeojsonFeatures, filterPoints } from './Partials/geojsonFeatures.js';
import ProgressSpinner from 'primevue/progressspinner';
Expand All @@ -13,6 +13,7 @@ const map = ref(null);

const props = defineProps({
geojson: Object,
coordinates: Object,
});

const filterMap = (ids) => {
Expand Down Expand Up @@ -70,6 +71,15 @@ const destroyMap = () => {
onBeforeUnmount(() => {
destroyMap();
});

watch(() => props.coordinates, (coordinates) => {
if (coordinates) {
map.value.flyTo({
center: coordinates,
zoom: 9,
});
}
});
</script>

<template>
Expand Down
26 changes: 26 additions & 0 deletions resources/js/Components/Maplibre/Partials/Tooltip.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup>
defineProps({
place: Object,
});
</script>

<template>
<div class="flex flex-col gap-2">
<div class="flex gap-2 items-center justify-between">
<span class="flex font-semibold">{{ place.name }}</span>
<div class="flex gap-2">
<a
class="pi pi-map-marker"
target="_blank"
:href="`https://www.google.com/maps/place/${place._geo[1]},${place._geo[0]}/@${place._geo[1]},${place._geo[0]},9z`"
/>
</div>
</div>
<a
:href="`https://google.com/search?q=${place.name}`"
class="text-primary hover:underline"
target="_blank"
>For more information, see Google</a>
</div>
</template>

7 changes: 6 additions & 1 deletion resources/js/Components/Maplibre/Partials/geojsonFeatures.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import maplibregl from 'maplibre-gl';
import { createApp } from 'vue';
import Tooltip from './Tooltip.vue';

export function filterPoints(map, ids){
map.setFilter('natural-features-filter-match', ['in', 'id'].concat(ids));
map.setFilter('natural-features-filter-no-match', ids.length > 0 ? ['match', ['get', 'id'], ids, false, true] : null);
}

function buildTooltip(place) {
return `<b>${place.name}</b>`;
const app = createApp(Tooltip, { place });
const div = document.createElement('div');
app.mount(div);
return div.outerHTML;
}

export function loadGeojsonFeatures(map, geojson, color) {
Expand Down
95 changes: 95 additions & 0 deletions resources/js/Components/PlaceInformationDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<script setup>
import Dialog from 'primevue/dialog';
import Avatar from 'primevue/avatar';

import { defineProps, defineEmits } from 'vue';

const props = defineProps({
place: Object,
visible: Boolean,
});

const emit = defineEmits(
['update:visible', 'showCoordinates'],
);

const closeDialog = () => {
emit('update:visible', false);
};

const emitCoordinates = () => {
emit('showCoordinates', props.place._geo);
emit('update:visible', false);
};
</script>

<template>
<Dialog
:visible="visible"
modal
header="Header"
:style="{ width: '50rem' }"
:breakpoints="{ '1199px': '75vw', '575px': '90vw' }"
@update:visible="closeDialog"
>
<template #header>
<div class="flex items-center justify-center gap-2">
<Avatar
image="/images/place-default-thumbnail.png"
shape="circle"
/>
<span class="font-bold">{{ props.place.name }}</span>
</div>
</template>
<p class="m-0">
{{ props.place.description ? props.place.description : 'No description found' }}
</p>
<div
v-if="props.place.description_source"
class="mt-2 overflow-hidden overflow-ellipsis w-full"
>
<span class="font-bold">Source: </span>
<a
:href="props.place.description_source"
target="_blank"
class="overflow-ellipsis overflow-hidden"
>{{ decodeURI(props.place.description_source) }}</a>
</div>
<div class="flex mt-4 flex-wrap gap-2 items-center">
<a
:href="'https://www.google.com/search?q=' + props.place.name"
target="_blank"
class="border border-blue-500 hover:bg-blue-50 text-blue-500 font-semibold py-2 px-4 rounded"
>
<div class="flex gap-2 items-center">
<i class="pi pi-google" />
<span>Google more information</span>
</div>
</a>
</div>

<template #footer>
<button
type="button"
class="mt-3 border border-emerald-500 hover:bg-emerald-600 bg-emerald-500 text-white font-bold py-2 px-3 rounded text-sm"
@click="emitCoordinates"
>
<div class="flex gap-2 items-center">
<i class="pi pi-map-marker" />
<span>Show on map</span>
</div>
</button>
<button
type="button"
class="mt-3 border border-emerald-500 hover:bg-emerald-50 text-emerald-500 font-bold py-2 px-3 rounded text-sm"
@click="closeDialog"
>
<div class="flex gap-2 items-center">
<i class="pi pi-times" />
<span>Close</span>
</div>
</button>
</template>
</Dialog>
</template>

39 changes: 36 additions & 3 deletions resources/js/Components/PlacesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import _ from 'lodash';
import axios from 'axios';
import { usePage } from '@inertiajs/vue3';
import { useToast } from 'primevue/usetoast';
import PlaceInformationDialog from '@/Components/PlaceInformationDialog.vue';

const toast = useToast();

Expand All @@ -24,6 +25,8 @@ const visitedOptions = [
{ name: 'Visited', value: 'visited' },
{ name: 'Not Visited', value: 'not_visited' },
];
const selectedPlace = ref({});
const isDialogVisible = ref(false);

const isLoggedIn = computed(() => {
const page = usePage();
Expand All @@ -40,7 +43,7 @@ const props = defineProps({
categories: Array,
});

const emit = defineEmits(['filter:geojson'])
const emit = defineEmits(['filter:geojson', 'showCoordinates']);

const search = () => {
places.value = [];
Expand Down Expand Up @@ -154,9 +157,35 @@ function unvisit(index, feature) {
});
}

function handleItemClick(feature) {
axios.get(`/features/id/${feature.id}`)
.then(response => {
selectedPlace.value = response.data;
isDialogVisible.value = true;
})
.catch(() => {
toast.add({
severity: 'error',
summary: 'Error',
detail: `Failed to get information about ${feature.name}.`,
life: 3000,
});
});
}

const handleShowCoordinates = (coordinates) => {
emit('showCoordinates', coordinates);
};

</script>

<template>
<PlaceInformationDialog
:visible="isDialogVisible"
:place="selectedPlace"
@update:visible="isDialogVisible = $event"
@show-coordinates="handleShowCoordinates"
/>
<div class="bg-white rounded border md:max-w-md w-full md:w-screen flex flex-col h-full">
<div class="flex flex-row sticky top-0 z-10">
<span class="p-input-icon-left w-full">
Expand Down Expand Up @@ -222,12 +251,16 @@ function unvisit(index, feature) {
>
<div class="flex items-center bg-white rounded-lg shadow-md border overflow-hidden hover:bg-surface-100 p-3">
<img
class="h-20 w-20 object-cover rounded-lg opacity-60"
class="h-20 w-20 object-cover rounded-lg opacity-60 cursor-pointer"
src="/images/place-default-thumbnail.png"
alt="Place"
@click="handleItemClick(feature)"
>

<div class="flex-grow p-2">
<div
class="flex-grow p-2 cursor-pointer"
@click="handleItemClick(feature)"
>
<h3 class="font-semibold text-lg">
{{ feature.name }}
</h3>
Expand Down
6 changes: 6 additions & 0 deletions resources/js/Pages/Welcome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ const props = defineProps({
});

const map = ref(null);
const coorinates = ref(null);

function onDataUpdate(data) {
map.value.filterMap(data);
}

const handleShowCoordinates = (coordinates) => {
coorinates.value = coordinates;
};
</script>

<template>
Expand All @@ -24,12 +28,14 @@ function onDataUpdate(data) {
<PlacesList
:categories="props.categories"
@filter:geojson="onDataUpdate"
@show-coordinates="handleShowCoordinates"
/>
</div>
<div class="w-full h-2/5 md:h-full">
<Map
ref="map"
:geojson="props.geojson"
:coordinates="coorinates"
/>
</div>
</div>
Expand Down
Loading