Working Map with overlaping spider and leaflet

This commit is contained in:
Jérémy Munsch 2024-03-25 15:43:13 +01:00
parent f55b01b02c
commit 6cd2a87655
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
15 changed files with 359 additions and 44 deletions

View file

@ -66,3 +66,9 @@ MAIL_CONTACT_REMARK="equipe.web@ldc.fr"
MAIL_CONTACT_SPONSORING="equipe.web@ldc.fr"
MAIL_CONTACT_SERVICE="equipe.web@ldc.fr"
MAIL_CONTACT_OTHER="equipe.web@ldc.fr"
# GOOGLE
# - RECAPTCHA
GOOGLE_RECAPTCHA_PUBLIC=
GOOGLE_RECAPTCHA_PRIVATE=
GOOGLE_RECAPTCHA_VERSION=

View file

@ -71,25 +71,37 @@ class StaticPageController extends Controller
$staticPageModel = StaticPage::query()->where('type', StaticPageType::unveil)->first();
$sectionModels = Section::query()->where('page_id', $staticPageModel->id)->get();
$historicalMilestoneModels = HistoricalMilestone::query()->orderBy('year', 'ASC')->get();
$siteModel = Site::query()->first();
$keySection = $sectionModels->where('section', PageSectionEnum::unveil_pole)->first();
$missionsSection = $sectionModels->where('section', PageSectionEnum::unveil_missions)->first();
$localSection = $sectionModels->where('section', PageSectionEnum::unveil_local)->first();
$siteSection = $sectionModels->where('section', PageSectionEnum::unveil_sites)->first();
$historySection = $sectionModels->where('section', PageSectionEnum::unveil_history)->first();
$commitmentSection = $sectionModels->where('section', PageSectionEnum::unveil_commitments)->first();
/** @var \Illuminate\Database\Eloquent\Collection<\App\Models\Site> */
$siteModelList = Site::query()->orderBy('order')->get();
$keySection = $sectionModels->where('section', PageSectionEnum::unveil_pole)->first();
$missionsSection = $sectionModels->where('section', PageSectionEnum::unveil_missions)->first();
$localSection = $sectionModels->where('section', PageSectionEnum::unveil_local)->first();
$siteSection = $sectionModels->where('section', PageSectionEnum::unveil_sites)->first();
$historySection = $sectionModels->where('section', PageSectionEnum::unveil_history)->first();
$commitmentSection = $sectionModels->where('section', PageSectionEnum::unveil_commitments)->first();
// * Calcul Viewbox leaflet map.
$viewBox = (object)[
// @phpstan-ignore-next-line
'lat' => \round($siteModelList->sum(fn (Site $siteModel) =>
$siteModel->latitude) / $siteModelList->count(), 6),
// @phpstan-ignore-next-line
'lng' => \round($siteModelList->sum(fn (Site $siteModel) =>
$siteModel->longitude) / $siteModelList->count(), 6)
];
return view('fo.pages.unveil', compact(
'viewBox',
'staticPageModel',
'sectionModels',
'historicalMilestoneModels',
'siteModel',
'siteModelList',
'keySection',
'missionsSection',
'localSection',
'siteSection',
'historySection',
'commitmentSection',
'commitmentSection'
));
}
@ -269,4 +281,4 @@ class StaticPageController extends Controller
'nextSiteModel' => $nextSiteModel
]);
}
}
}

View file

@ -59,6 +59,8 @@ class SecureHeaders
'https://yl.s-web.fr'
]) . ';',
"img-src 'self' data: blob: " . implode(' ', [
// * Leaftlet With OpenStreetmaps
'https://tile.openstreetmap.org',
// * VITE DEV
config('app.debug') ? "https://{$host}:4443" : '',
]) . ';',
@ -66,7 +68,6 @@ class SecureHeaders
"frame-src 'self' " . implode(' ', [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
'https://www.google.com/maps/',
'https://www.youtube.com',
]) . ';',
"font-src 'self' data: " . \implode(' ', [

0
artisan Executable file → Normal file
View file

67
package-lock.json generated
View file

@ -13,11 +13,10 @@
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@googlemaps/js-api-loader": "^1.16.6",
"@ldc-web/cookie-notice": "^4.0.1",
"@sipec/vue3-tags-input": "^3.0.4",
"@types/bootstrap": "^5.2.7",
"@types/google.maps": "^3.55.4",
"@types/leaflet": "^1.9.8",
"@types/node": "^20.8.2",
"@types/toastify-js": "^1.12.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
@ -37,6 +36,9 @@
"eslint-plugin-vue": "latest",
"fslightbox": "^3.4.1",
"laravel-vite-plugin": "^0.8.1",
"leaflet": "^1.9.4",
"leaflet.fullscreen": "^3.0.1",
"overlapping-marker-spiderfier-leaflet": "^0.2.7",
"postcss-discard-comments": "^6.0.0",
"prettier": "latest",
"print-js": "^1.6.0",
@ -54,6 +56,7 @@
"terser": "latest",
"toastify-js": "^1.12.0",
"ts-loader": "^9.3.1",
"ts-overlapping-marker-spiderfier": "^1.0.3",
"typescript": ">=3.3.1 <5.2.0",
"typescript-eslint": "latest",
"vite": "^4.5.1",
@ -968,15 +971,6 @@
"vue": ">= 3.0.0 < 4"
}
},
"node_modules/@googlemaps/js-api-loader": {
"version": "1.16.6",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.6.tgz",
"integrity": "sha512-V8p5W9DbPQx74jWUmyYJOerhiB4C+MHekaO0ZRmc6lrOYrvY7+syLhzOWpp55kqSPeNb+qbC2h8i69aLIX6krQ==",
"dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -1195,6 +1189,12 @@
"@types/node": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.14",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@ -1205,10 +1205,11 @@
"@types/node": "*"
}
},
"node_modules/@types/google.maps": {
"version": "3.55.4",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.55.4.tgz",
"integrity": "sha512-Ip3IfRs3RZjeC88V8FGnWQTQXeS5gkJedPSosN6DMi9Xs8buGTpsPq6UhREoZsGH+62VoQ6jiRBUR8R77If69w==",
"node_modules/@types/googlemaps": {
"version": "3.43.3",
"resolved": "https://registry.npmjs.org/@types/googlemaps/-/googlemaps-3.43.3.tgz",
"integrity": "sha512-ZWNoz/O8MPEpiajvj7QiqCY8tTLFNqNZ/a+s+zTV58wFVNAvvqV4bdGfnsjTb5Cs4V6wEsLrX8XRhmnyYJ2Tdg==",
"deprecated": "Types for the Google Maps browser API have moved to @types/google.maps. Note: these types are not for the googlemaps npm package, which is a Node API.",
"dev": true
},
"node_modules/@types/json-schema": {
@ -1224,6 +1225,15 @@
"dev": true,
"peer": true
},
"node_modules/@types/leaflet": {
"version": "1.9.8",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.8.tgz",
"integrity": "sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==",
"dev": true,
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
@ -4984,6 +4994,18 @@
"vite": "^3.0.0 || ^4.0.0"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"dev": true
},
"node_modules/leaflet.fullscreen": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/leaflet.fullscreen/-/leaflet.fullscreen-3.0.1.tgz",
"integrity": "sha512-Lsg49e4AYSjHOhmy+VOm4+O6Urim2UvthydKJI3KzfwC3InYkfvJtv1mWuOb9K7qG4H2MtEut09Dz6noQyJARQ==",
"dev": true
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -5506,6 +5528,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/overlapping-marker-spiderfier-leaflet": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/overlapping-marker-spiderfier-leaflet/-/overlapping-marker-spiderfier-leaflet-0.2.7.tgz",
"integrity": "sha512-U2biV2Ge0SU+4IEmq4BZOglvzA8Aj8G7/hp5v6lBnF9Kd3/Xf6ZEPsJyPOExtLvMxOqlrlTAfl55s6JJDND7Ew==",
"dev": true
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -7524,6 +7552,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/ts-overlapping-marker-spiderfier": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/ts-overlapping-marker-spiderfier/-/ts-overlapping-marker-spiderfier-1.0.3.tgz",
"integrity": "sha512-WqA+tpJHZHpGS8fL1C/cOPue72doG2yfNoxK7cUuLwH108hoQkjd6R52c3hYiFdVc1UvGuA/wXzAJYXPi+L9YQ==",
"dev": true,
"dependencies": {
"@types/googlemaps": "^3.30.7"
}
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",

View file

@ -21,11 +21,10 @@
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@googlemaps/js-api-loader": "^1.16.6",
"@ldc-web/cookie-notice": "^4.0.1",
"@sipec/vue3-tags-input": "^3.0.4",
"@types/bootstrap": "^5.2.7",
"@types/google.maps": "^3.55.4",
"@types/leaflet": "^1.9.8",
"@types/node": "^20.8.2",
"@types/toastify-js": "^1.12.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
@ -45,6 +44,9 @@
"eslint-plugin-vue": "latest",
"fslightbox": "^3.4.1",
"laravel-vite-plugin": "^0.8.1",
"leaflet": "^1.9.4",
"leaflet.fullscreen": "^3.0.1",
"overlapping-marker-spiderfier-leaflet": "^0.2.7",
"postcss-discard-comments": "^6.0.0",
"prettier": "latest",
"print-js": "^1.6.0",
@ -62,6 +64,7 @@
"terser": "latest",
"toastify-js": "^1.12.0",
"ts-loader": "^9.3.1",
"ts-overlapping-marker-spiderfier": "^1.0.3",
"typescript": ">=3.3.1 <5.2.0",
"typescript-eslint": "latest",
"vite": "^4.5.1",

View file

@ -8,7 +8,7 @@ return [
'section' => 'section',
'news' => 'actualité',
'site' => 'site',
'logo' => 'logo',
'logo' => 'logo de marques',
'historicalMilestone' => 'jalon historique|jalons historiques',
'slide' => 'slide',
'job_video' => 'vidéo métier|vidéos métiers',

View file

@ -224,6 +224,8 @@ return [
],
'youtube_id' => ':attribute doit être un ID de vidéo Youtube valide',
'vimeo_id' => ':attribute doit être un ID de vidéo Vimeo valide',
'latitude' => 'Latitude entre -90 et 90 - ex : 48.858502757201',
'longitude' => 'Longitude entre -180 et 180 - ex : 2.2944636160339',
'custom' => [
'of_the' => [
'male' => 'du ',

View file

@ -176,7 +176,7 @@ body {
> header {
position: fixed;
z-index: 50;
z-index: 10001;
top: 0;
left: 0;
width: 100%;

View file

@ -8,6 +8,7 @@
@import "./vendor/bootstrap";
@import "./vendor/swiper";
@import "./vendor/cookie-notice";
@import "leaflet";
// * BASE
@import "./base/bootstrap-overload";

View file

@ -0,0 +1,233 @@
<template>
<div
ref="maps"
:class="wrapperClasses.concat(' leafleat-wrapper-class')"
>
<div
ref="marker"
class="d-none marker-pin text-bg-white px-3 py-1 d-flex justify-content-center align-items-center f-barlow rounded-pill border border-1 border-primary"
>
<img
class="marker-logo mx-1"
src=""
alt=""
>
<span>
<span class="mx-1 marker-title text-primary fw-bold text-uppercase text-break" />
</span>
</div>
<div
ref="marker-pannel"
class="d-none"
>
<span class="h4 marker-title" />
<div class="marker-desc" />
<span class="marker-people">
<FontAwesomeIcon icon="fa-solid fa-users" />
</span>
</div>
</div>
</template>
<script lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { DivIcon, Icon, Map, Marker, TileLayer, type LatLngExpression, type LatLngTuple } from "leaflet";
// @ts-ignore
import "overlapping-marker-spiderfier-leaflet";
import type { OverlappingMarkerSpiderfier } from "ts-overlapping-marker-spiderfier";
// @ts-ignore
import "leaflet.fullscreen";
import { defineComponent } from "vue";
import trans from "../../modules/trans";
type Site = {
name: string;
/** WYSIWYG text */
description: string;
employees: number;
coords: {
lat: number,
lng: number,
},
logo: {
src: string,
alt: string;
title: string;
},
divIcon: DivIcon
};
export default defineComponent({
name: "LeafletMapVueComponent",
mixins: [trans],
components: {FontAwesomeIcon},
data(): {
wrapperClasses: string[];
map: Map;
oms: OverlappingMarkerSpiderfier;
initialPosition: LatLngExpression;
initialZoom: number;
siteList: Array<Site>;
markerTextLength: number;
} {
return {
wrapperClasses:[],
map: {} as Map,
oms: {} as OverlappingMarkerSpiderfier,
initialPosition: [0, 0],
initialZoom: 3,
siteList: [],
markerTextLength: 20
};
},
computed: {},
mounted() {
const json = String(this.$attrs.json ?? "{}"),
data = JSON.parse(json);
this.wrapperClasses = data.wrapperClasses ?? [];
this.initialPosition = [Number.parseFloat(data.coordinates.lat), Number.parseFloat(data.coordinates.lng)];
this.parseSiteModelList(data.siteModelList);
this.$nextTick(() => {
this.displayMap();
this.setupMarkers();
});
},
methods: {
displayMap() {
const mapWrapper = this.$refs.maps as HTMLDivElement;
this.map = new Map(mapWrapper, {
// @ts-ignore
fullscreenControl: true,
fullscreenControlOptions: {
position: "topleft"
}
});
(new TileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "&copy; <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>"
})).addTo(this.map as Map);
// * Auto set viewbox
(this.map as Map).fitBounds(this.siteList.map(site => Object.values(site.coords)) as LatLngTuple[]);
// * Setup OMS
// @ts-ignore
this.oms = new OverlappingMarkerSpiderfier(this.map as Map, {
// spiderfy markers within this range of the one clicked, in px
nearbyDistance: 120,
// circleSpiralSwitchover: 250,
/**
* show spiral instead of circle from this marker count upwards
* 0 -> always spiral; Infinity -> always circle
*/
circleSpiralSwitchover: 12,
// related to size of spiral (experiment!)
circleFootSeparation: 80,
// related to size of spiral (experiment!)
circleStartAngle: (Math.PI * 2) / 24,
// ditto
spiralLengthStart: 130,
// ditto
spiralLengthFactor: 5,
});
this.oms.addListener("click", (marker: Marker) => {
console.log(marker);
});
this.oms.addListener("spiderfy", () => {
this.map.closePopup();
});
},
parseSiteModelList(siteModelList: LaravelModelList) {
siteModelList.forEach((site) => {
this.siteList.push({
name: String(site.name),
description: String(site.description),
employees: Number.parseInt(String(site.employees)),
coords: {
lat: Number.parseFloat(String(site.latitude)),
lng: Number.parseFloat(String(site.longitude)),
},
logo: {
src: String(site.logo),
alt: String(site.logo_alt),
title: String(site.logo_title),
},
divIcon: new DivIcon({
className: "leaf-div-icon",
html: this.fetchNewMarkerElement(
String(site.name),
{
src: String(site.logo),
alt: String(site.logo_alt),
title: String(site.logo_title),
}
),
iconSize: [30, 42],
iconAnchor: [15, 42]
})
});
});
},
fetchNewMarkerElement(
title: string,
logo: {src: string,alt: string,title: string}
): HTMLDivElement {
const marker = (this.$refs.marker as HTMLDivElement).cloneNode(true) as HTMLDivElement;
const logoEl = marker.querySelector(".marker-logo") as HTMLImageElement|null;
marker.querySelector(".marker-title")!.textContent = this.truncateString(title, this.markerTextLength);
logoEl!.src = logo.src;
logoEl!.alt = logo.alt;
logoEl!.title = logo.title;
marker.classList.remove("d-none");
return marker;
},
setupMarkers() {
this.siteList.forEach((site) => {
const marker = new Marker(site.coords, {
title: site.name,
alt: site.name,
icon: site.divIcon as Icon
});
this.map.addLayer(marker);
// @ts-ignore
this.oms.addMarker(marker);
});
},
truncateString(str: string, limit: number, end: string = "..") {
if (str.length <= limit) {
return str;
}
return str.slice(0, limit) + end;
}
},
});
</script>
<style>
@import "../../../../node_modules/leaflet.fullscreen/Control.FullScreen.css";
</style>
<style lang="scss" scoped>
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/vendor/rfs";
.leafleat-wrapper-class {
.marker-pin {
@include rfs(10rem, min-width);
}
.marker-logo {
object-fit: contain;
height: auto;
@include rfs(2rem, width);
}
.marker-title {
@include rfs(0.8rem, font-size);
}
}
</style>

View file

@ -15,7 +15,8 @@ import {
faPlay,
faPlus,
faQuestionCircle,
faUserGroup
faUserGroup,
faUsers
} from "@fortawesome/free-solid-svg-icons";
config.autoReplaceSvg = false;
@ -35,5 +36,6 @@ library.add(faMinus);
library.add(faPlus);
library.add(faPlay);
library.add(faPause);
library.add(faUsers);
dom.watch();

View file

@ -11,6 +11,13 @@ const sassSafelist: UserDefinedSafelist = [
/^bs-popover/,
/^popover/,
/^bg-.*/,
/^img-.*/,
// * Fonts
/^f-.*/,
// * Leaflet
/^leaflet-.*/,
// * Leaflet Fullscreen plugin
/^fullscreen-icon$/,
// Vue
/-(leave|enter|appear)(|-(to|from|active))$/,
/^(?!(|.*?:)cursor-move).+-move$/,

View file

@ -503,7 +503,7 @@
'name' => 'longitude',
'required' => true,
'model' => $siteModel ?? null,
'helper' => __('crud.helpers.longitude'),
'helper' => __('validation.longitude'),
])
</div>
</div>

View file

@ -18,10 +18,12 @@
@section('content')
<section class="unveil">
{{-- KEY NUMBERS --}}
<section class="key-numbers container my-5 text-center">
<div class="row">
<div class="col-12">
<h2 class="h2 fw-bold text-primary">{{ $keySection->h2_title }}</h2>
<section class="key-numbers my-5">
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="h2 fw-bold text-primary text-center">{{ $keySection->h2_title }}</h2>
</div>
</div>
</div>
<div class="row justify-content-center">
@ -103,14 +105,23 @@
</div>
@endif
</div>
<div class="col-12 col-lg-6 d-flex justify-content-center">
{{-- FIXME: remplacer map --}}
<div class="embed-map w-100 my-3">
<iframe class="rounded-3"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d10661.469000249905!2d-0.7577479264599797!3d48.083855231712434!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x4809013bc7a7e43f%3A0xe01d7907f663610d!2sSNV%20Laval!5e0!3m2!1sfr!2sfr!4v1709025460315!5m2!1sfr!2sfr"
title="{{ __('Carte de nos sites') }}" style="border:0;" allowfullscreen="" loading="lazy"
referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
<div class="col-12 col-lg-6 order-lg-first d-flex justify-content-center">
@php
$data = [
'wrapperClasses' => 'rounded-3 embed-map w-100 my-3',
'coordinates' => [
'lat' => $viewBox->lat,
'lng' => $viewBox->lng,
],
'siteModelList' => $siteModelList,
];
@endphp
<div class="leaflet-map-vue w-100" data-json='@json($data)'></div>
{{-- FIXME: Quelles coordonnées pour la map ? --}}
{{-- <iframe class="rounded-3 embed-map w-100 my-3"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d10661.469000249905!2d-0.7577479264599797!3d48.083855231712434!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x4809013bc7a7e43f%3A0xe01d7907f663610d!2sSNV%20Laval!5e0!3m2!1sfr!2sfr!4v1709025460315!5m2!1sfr!2sfr"
title="{{ __('Carte de nos sites') }}" style="border:0;" allowfullscreen="" loading="lazy"
referrerpolicy="no-referrer-when-downgrade"></iframe> --}}
</div>
</div>
</div>