Skip to content

Commit

Permalink
refactor - tooltips
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitdemaegdt authored and Paul LOPEZ committed Nov 11, 2024
1 parent 005474c commit 55ef8b9
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 148 deletions.
138 changes: 6 additions & 132 deletions components/Map.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,14 @@
</template>

<script setup lang="ts">
import { createApp, defineComponent, h, Suspense } from 'vue';
import { Map, AttributionControl, GeolocateControl, NavigationControl, Popup, type StyleSpecification, type LngLatLike } from 'maplibre-gl';
import { Map, AttributionControl, GeolocateControl, NavigationControl, type StyleSpecification, type LngLatLike } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import style from '@/assets/style.json';
import LegendControl from '@/maplibre/LegendControl';
import FilterControl from '@/maplibre/FilterControl';
import FullscreenControl from '@/maplibre/FullscreenControl';
import ShrinkControl from '@/maplibre/ShrinkControl';
import LineTooltip from '~/components/tooltips/LineTooltip.vue';
import CounterTooltip from '~/components/tooltips/CounterTooltip.vue';
import PumpTooltip from '~/components/tooltips/PumpTooltip.vue';
import DangerTooltip from '~/components/tooltips/DangerTooltip.vue';
import PerspectiveTooltip from '~/components/tooltips/PerspectiveTooltip.vue';
import { isLineStringFeature, type Feature, type LaneStatus, type LaneType } from '~/types';
import config from '~/config.json';
Expand Down Expand Up @@ -58,7 +53,8 @@ const filterModalComponent = ref(null);
const {
loadImages,
plotFeatures,
fitBounds
fitBounds,
handleMapClick
} = useMap();
const statuses = ref(['planned', 'variante', 'done', 'postponed', 'variante-postponed', 'unknown', 'wip', 'tested']);
Expand Down Expand Up @@ -155,130 +151,8 @@ onMounted(() => {
}
);
// must do this to avoid multiple popups
map.on('click', e => {
const layers = map
.queryRenderedFeatures(e.point)
.filter(({ layer }) => !['maptiler_planet', 'openmaptiles'].includes(layer.source));
if (layers.length === 0) {
return;
}
const isPerspectiveLayerClicked = layers.some(({ layer }) => layer.id === 'perspectives');
const isCompteurLayerClicked = layers.some(({ layer }) => layer.id === 'compteurs');
const isPumpLayerClicked = layers.some(({ layer }) => layer.id === 'pumps');
const isDangerLayerClicked = layers.some(({ layer }) => layer.id === 'dangers');
if (isPerspectiveLayerClicked) {
const layer = layers.find(({ layer }) => layer.id === 'perspectives');
const feature = features.value.find(f => {
return f.properties.type === 'perspective' &&
f.properties.line === layer!.properties.line &&
f.properties.imgUrl === layer!.properties.imgUrl;
});
new Popup({ closeButton: false, closeOnClick: true })
.setLngLat(e.lngLat)
.setHTML('<div id="perspective-tooltip-content"></div>')
.addTo(map);
// @ts-ignore:next
const PerspectiveTooltipComponent = defineComponent(PerspectiveTooltip);
nextTick(() => {
// eslint-disable-next-line vue/one-component-per-file
createApp({
render: () => h(Suspense, null, {
default: h(PerspectiveTooltipComponent, { feature }),
fallback: 'Chargement...'
})
}).mount('#perspective-tooltip-content');
});
} else if (isPumpLayerClicked) {
const layer = layers.find(({ layer }) => layer.id === 'pumps');
const feature = features.value.find(f => f.properties.name === layer!.properties.name);
new Popup({ closeButton: false, closeOnClick: true })
.setLngLat(e.lngLat)
.setHTML('<div id="pump-tooltip-content"></div>')
.addTo(map);
// @ts-ignore:next
const PumpTooltipComponent = defineComponent(PumpTooltip);
nextTick(() => {
// eslint-disable-next-line vue/one-component-per-file
createApp({
render: () => h(Suspense, null, {
default: h(PumpTooltipComponent, { feature }),
fallback: 'Chargement...'
})
}).mount('#pump-tooltip-content');
});
} else if (isDangerLayerClicked) {
const layer = layers.find(({ layer }) => layer.id === 'dangers');
const feature = features.value.find(f => f.properties.name === layer!.properties.name);
new Popup({ closeButton: false, closeOnClick: true })
.setLngLat(e.lngLat)
.setHTML('<div id="danger-tooltip-content"></div>')
.addTo(map);
// @ts-ignore:next
const DangerTooltipComponent = defineComponent(DangerTooltip);
nextTick(() => {
// eslint-disable-next-line vue/one-component-per-file
createApp({
render: () => h(Suspense, null, {
default: h(DangerTooltipComponent, { feature }),
fallback: 'Chargement...'
})
}).mount('#danger-tooltip-content');
});
} else if (isCompteurLayerClicked) {
const layer = layers.find(({ layer }) => layer.id === 'compteurs');
const feature = features.value.find(f => f.properties.name === layer!.properties.name);
new Popup({ closeButton: false, closeOnClick: true })
.setLngLat(e.lngLat)
.setHTML('<div id="counter-tooltip-content"></div>')
.addTo(map);
// @ts-ignore:next
const CounterTooltipComponent = defineComponent(CounterTooltip);
nextTick(() => {
// eslint-disable-next-line vue/one-component-per-file
createApp({
render: () => h(Suspense, null, {
default: h(CounterTooltipComponent, { feature }),
fallback: 'Chargement...'
})
}).mount('#counter-tooltip-content');
});
} else {
const { line, name } = layers[0].properties;
// take care layers[0].geometry is truncated (to fit tile size). We need to find the full feature.
const feature = features.value
.filter(isLineStringFeature)
.find(feature => feature.properties.line === line && feature.properties.name === name);
const lines = feature!.properties.id
? [...new Set(layers.filter(f => f.properties.id === feature!.properties.id).map(f => f.properties.line))]
: [feature!.properties.line];
new Popup({ closeButton: false, closeOnClick: true })
.setLngLat(e.lngLat)
.setHTML('<div id="line-tooltip-content"></div>')
.addTo(map);
// @ts-ignore:next
const LineTooltipComponent = defineComponent(LineTooltip);
nextTick(() => {
// eslint-disable-next-line vue/one-component-per-file
createApp({
render: () => h(Suspense, null, {
default: h(LineTooltipComponent, { feature, lines }),
fallback: 'Chargement...'
})
}).mount('#line-tooltip-content');
});
}
map.on('click', clickEvent => {
handleMapClick({ map, features: features.value, clickEvent });
});
});
</script>
Expand Down
14 changes: 0 additions & 14 deletions components/tooltips/PumpTooltip.vue

This file was deleted.

134 changes: 132 additions & 2 deletions composables/useMap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { GeoJSONSource, LngLatBounds, Map } from 'maplibre-gl';
import { GeoJSONSource, LngLatBounds, Map, Popup } from 'maplibre-gl';
import { createApp, defineComponent, h, Suspense } from 'vue';
import type { CounterParsedContent } from '../types/counters';
import { isCompteurFeature, isDangerFeature, isPumpFeature, isLineStringFeature, isPerspectiveFeature, isPointFeature, type Feature, type LineStringFeature, type CompteurFeature } from '~/types';

// Tooltips
import PerspectiveTooltip from '~/components/tooltips/PerspectiveTooltip.vue';
import CounterTooltip from '~/components/tooltips/CounterTooltip.vue';
import DangerTooltip from '~/components/tooltips/DangerTooltip.vue';
import LineTooltip from '~/components/tooltips/LineTooltip.vue';

type ColoredLineStringFeature = LineStringFeature & { properties: { color: string } };

// features plotted last are on top
Expand Down Expand Up @@ -481,6 +488,9 @@ export const useMap = () => {
'icon-color': ['get', 'color']
}
});

// on n'affiche les perspectives qu'à partir d'un certain zoom.
// ceci pour éviter de surcharger la map.
map.setLayoutProperty('perspectives', 'visibility', 'none');
map.on('zoom', () => {
const zoomLevel = map.getZoom();
Expand All @@ -490,6 +500,14 @@ export const useMap = () => {
map.setLayoutProperty('perspectives', 'visibility', 'none');
}
});

// la souris devient un pointer au survol
map.on('mouseenter', 'perspectives', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'perspectives', () => {
map.getCanvas().style.cursor = '';
});
}

function plotDangers({ map, features }: { map: Map; features: Feature[] }) {
Expand Down Expand Up @@ -652,10 +670,122 @@ export const useMap = () => {
plotDangers({ map, features });
}

function handleMapClick({ map, features, clickEvent }: { map: Map; features: Feature[]; clickEvent: any }) {
const layers = [
{
id: 'linestring', // not really a layer id. gather all linestrings.
isClicked: () => {
const mapFeature = map.queryRenderedFeatures(clickEvent.point, {
filter: [
'all',
['==', ['geometry-type'], 'LineString'],
['!=', ['get', 'source'], 'openmaptiles'], // Exclude base map features
['has', 'status'] // All sections in geojson LineStrings have a status
]
});
return mapFeature.length > 0;
},
getTooltipProps: () => {
const mapFeature = map.queryRenderedFeatures(clickEvent.point, {
filter: [
'all',
['==', ['geometry-type'], 'LineString'],
['!=', ['get', 'source'], 'openmaptiles'], // Exclude base map features
['has', 'status'] // All sections in geojson LineStrings have a status
]
})[0];

const line = mapFeature.properties.line;
const name = mapFeature.properties.name;

const lineStringFeatures = features.filter(isLineStringFeature);

const feature = lineStringFeatures
.find(f => f.properties.line === line && f.properties.name === name);

const lines = feature!.properties.id
? [...new Set(lineStringFeatures.filter(f => f.properties.id === feature!.properties.id).map(f => f.properties.line))]
: [feature!.properties.line];

return { feature, lines };
},
component: LineTooltip
},
{
id: 'perspectives',
isClicked: () => {
if (!map.getLayer('perspectives')) { return false; }
const mapFeature = map.queryRenderedFeatures(clickEvent.point, { layers: ['perspectives'] });
return mapFeature.length > 0;
},
getTooltipProps: () => {
const mapFeature = map.queryRenderedFeatures(clickEvent.point, { layers: ['perspectives'] })[0];
const feature = features.find(f => {
return f.properties.type === 'perspective' &&
f.properties.line === mapFeature.properties.line &&
f.properties.imgUrl === mapFeature.properties.imgUrl;
});

return { feature };
},
component: PerspectiveTooltip
},
{
id: 'compteurs',
isClicked: () => {
if (!map.getLayer('compteurs')) { return false; }
const mapFeature = map.queryRenderedFeatures(clickEvent.point, { layers: ['compteurs'] });
return mapFeature.length > 0;
},
getTooltipProps: () => {
const mapFeature = map.queryRenderedFeatures(clickEvent.point, { layers: ['compteurs'] })[0];
const feature = features.find(f => f.properties.name === mapFeature.properties.name);
return { feature };
},
component: CounterTooltip
},
{
id: 'dangers',
isClicked: () => {
if (!map.getLayer('dangers')) { return false; }
const mapFeature = map.queryRenderedFeatures(clickEvent.point, { layers: ['dangers'] });
return mapFeature.length > 0;
},
getTooltipProps: () => {
const mapFeature = map.queryRenderedFeatures(clickEvent.point, { layers: ['dangers'] })[0];
const feature = features.find(f => f.properties.name === mapFeature.properties.name);
return { feature };
},
component: DangerTooltip
}
];

const clickedLayer = layers.find(layer => layer.isClicked());
if (!clickedLayer) { return; }

new Popup({ closeButton: false, closeOnClick: true })
.setLngLat(clickEvent.lngLat)
.setHTML(`<div id="${clickedLayer.id}-tooltip-content"></div>`)
.addTo(map);

const props = clickedLayer.getTooltipProps();
// @ts-ignore:next
const component = defineComponent(clickedLayer.component);
nextTick(() => {
createApp({
render: () => h(Suspense, null, {
default: h(component, props),
fallback: 'Chargement...'
})
}).mount(`#${clickedLayer.id}-tooltip-content`);
});
}

return {
loadImages,
plotFeatures,
getCompteursFeatures,
fitBounds
fitBounds,
handleMapClick
};
};

0 comments on commit 55ef8b9

Please sign in to comment.