From f97b7b817097435e79bc20e2b2f8cfe0b5fe2f4e Mon Sep 17 00:00:00 2001 From: dakur Date: Sat, 8 Feb 2025 13:38:57 +0100 Subject: [PATCH] upgrade map --- .../scripts/administrativeUnitsMap/Filters.ts | 46 --- .../administrativeUnitsMap/InfoWindow.ts | 64 ---- .../src/scripts/administrativeUnitsMap/Map.ts | 85 ------ .../scripts/administrativeUnitsMap/index.ts | 49 ---- .../scripts/administrativeUnitsMap/types.ts | 17 -- .../scripts/administrativeUnitsMap/utils.ts | 60 ---- functions.php | 4 +- gulpfile.babel.js | 6 - header.php | 7 + scripts/administrativeUnitsMap.js | 274 ++++++++++++++++++ .../content/content-hnuti-brontosaurus.php | 2 +- 11 files changed, 284 insertions(+), 330 deletions(-) delete mode 100644 frontend/src/scripts/administrativeUnitsMap/Filters.ts delete mode 100644 frontend/src/scripts/administrativeUnitsMap/InfoWindow.ts delete mode 100644 frontend/src/scripts/administrativeUnitsMap/Map.ts delete mode 100644 frontend/src/scripts/administrativeUnitsMap/index.ts delete mode 100644 frontend/src/scripts/administrativeUnitsMap/types.ts delete mode 100644 frontend/src/scripts/administrativeUnitsMap/utils.ts create mode 100644 scripts/administrativeUnitsMap.js diff --git a/frontend/src/scripts/administrativeUnitsMap/Filters.ts b/frontend/src/scripts/administrativeUnitsMap/Filters.ts deleted file mode 100644 index a8388a8..0000000 --- a/frontend/src/scripts/administrativeUnitsMap/Filters.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Map from './Map'; - -export default class Filters { - private /* const */ ACTIVE_FILTER_ITEM_CSS_CLASS_SELECTOR = 'administrativeUnitsMap__filter--active'; - - public constructor( - private mapInstance: Map, - private items: NodeListOf, - ) { - this.attachListeners(); - } - - - public displayAll() - { - this.updateFilterActiveState(document.getElementById('mapa-vse')!); // map exists so this should be present as well - this.mapInstance.displayLayer(); - } - - public displayLayer(item: HTMLElement) - { - this.updateFilterActiveState(item); - this.mapInstance.displayLayer(item.dataset.slug); - } - - - private attachListeners(): void - { - this.items.forEach(item => - item.addEventListener('click', ev => { - window.history.pushState(null, '', item.children[0].getAttribute('href')); - ev.preventDefault(); - - this.displayLayer(item as HTMLElement); - })); - } - - private updateFilterActiveState(activeItem: HTMLElement): void - { - this.items.forEach(item => - item.classList.remove(this.ACTIVE_FILTER_ITEM_CSS_CLASS_SELECTOR)); - - activeItem.classList.add(this.ACTIVE_FILTER_ITEM_CSS_CLASS_SELECTOR); - } - -} diff --git a/frontend/src/scripts/administrativeUnitsMap/InfoWindow.ts b/frontend/src/scripts/administrativeUnitsMap/InfoWindow.ts deleted file mode 100644 index 72b7c45..0000000 --- a/frontend/src/scripts/administrativeUnitsMap/InfoWindow.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {resolveUnitTitle} from './utils'; -import {OrganizationalUnit} from './types'; - - -export class InfoWindow -{ - infoWindow: google.maps.InfoWindow; - - public constructor( - private readonly mapInstance: google.maps.Map, - ) - { - // We want only one instance of InfoWindow due to closing previous opened windows - this.infoWindow = new google.maps.InfoWindow(); - } - - public display(unit: OrganizationalUnit, marker: google.maps.Marker): void - { - this.infoWindow.close(); // Close previously opened infowindow - this.infoWindow.setContent(InfoWindow.buildContent(unit)); // set content of info window - this.infoWindow.open(this.mapInstance, marker); // open window, which was clicked by user - } - - private static buildContent(unit: OrganizationalUnit): HTMLDivElement - { - const containerEl = document.createElement('div'); - containerEl.id = 'infowindow'; - containerEl.classList.add('administrativeUnitsMap__infoWindow') - - if (unit.image !== null) { - const imageContainerEl = containerEl.appendChild(document.createElement('div')); - imageContainerEl.classList.add('administrativeUnitsMap__infoWindowImageContainer'); - const imageEl = imageContainerEl.appendChild(document.createElement('img')); - imageEl.classList.add('administrativeUnitsMap__infoWindowImage'); - imageEl.src = unit.image; - } - - const contentEl = containerEl.appendChild(document.createElement('div')); - const metaEl = contentEl.appendChild(document.createElement('div')); - - metaEl.innerHTML = resolveUnitTitle(unit); - metaEl.innerHTML += `
Adresa: ${unit.address}`; - - if (unit.chairman !== null) { - metaEl.innerHTML += `
Předseda: ${unit.chairman}`; - } - - if (unit.website !== null ) { - metaEl.innerHTML += `
Web: ${unit.website}` - } - - if (unit.email !== null) { - metaEl.innerHTML += `
E-mail: ${unit.email}`; - } - - if (unit.description !== null) { - const descriptionEl = contentEl.appendChild(document.createElement('p')); - descriptionEl.classList.add('administrativeUnitsMap__infoWindowDescription') - descriptionEl.innerHTML += unit.description; - } - - return containerEl; - } -} diff --git a/frontend/src/scripts/administrativeUnitsMap/Map.ts b/frontend/src/scripts/administrativeUnitsMap/Map.ts deleted file mode 100644 index d5e5825..0000000 --- a/frontend/src/scripts/administrativeUnitsMap/Map.ts +++ /dev/null @@ -1,85 +0,0 @@ -import {InfoWindow} from './InfoWindow'; -import {resolveIconFileName, resolveUnitTitle, resolveUnitTypeSlug} from './utils'; -import {OverlappingMarkerSpiderfier} from 'ts-overlapping-marker-spiderfier'; -import {OrganizationalUnit} from './types'; - -export default class Map { - map: google.maps.Map; - mapLayers: google.maps.MVCObject; - infoWindow: InfoWindow; - markerCluster: OverlappingMarkerSpiderfier; - slugs: Array; - organizationalUnits: Array; - - public constructor(mapElement: HTMLElement) - { - // Maps Google map to place where we want display our map - this.map = new google.maps.Map(mapElement); - - // For layers by type, @source: https://stackoverflow.com/a/23036174 - this.mapLayers = new google.maps.MVCObject(); - - this.infoWindow = new InfoWindow(this.map); - - this.markerCluster = new OverlappingMarkerSpiderfier(this.map); - - this.slugs = []; - - const organizationalUnitsInJSON = mapElement.getAttribute('data-organizationalUnits'); - if (organizationalUnitsInJSON === null) { - throw new Error('Organizational units has not been passed.'); - } - this.organizationalUnits = JSON.parse(organizationalUnitsInJSON); - - this.placeMarkers(); - this.centerAndZoom(); - } - - - // place markers and set layers by type for filter - public placeMarkers(): void - { - this.organizationalUnits.forEach(unit => { - const slug = resolveUnitTypeSlug(unit); - - if (typeof slug !== 'undefined' && this.slugs.indexOf(slug) === -1) { - this.slugs.push(slug); - this.mapLayers.set(slug, this.map); - } - - this.placeMarker(unit); - }); - } - - // Inspiration from: https://stackoverflow.com/a/30013345 - private placeMarker(unit: OrganizationalUnit): void - { - // make marker and set option - const marker = new google.maps.Marker({ - position: {lat: unit.lat, lng: unit.lng}, - map: this.map, - title: resolveUnitTitle(unit), - icon: `https://brontosaurus.cz/wp-content/uploads/2024/12/${resolveIconFileName(unit)}`, - }); - - // Bind marker.map to the specific slug property of unitTypesLayers which is set and unset below [1] - marker.bindTo('map', this.mapLayers, resolveUnitTypeSlug(unit)); - - // Add spider listener for opening and closing info window - this.markerCluster.addMarker(marker, () => { - this.infoWindow.display(unit, marker); - }); - } - - public displayLayer(filter: string|null = null): void - { - this.slugs.forEach(slug => - this.mapLayers.set(slug, slug === filter || filter === null ? this.map : null)); - } - - public centerAndZoom(): void - { - this.map.setCenter(new google.maps.LatLng(49.7437572, 15.3386383)); // Czechia geographic center, see https://en.mapy.cz/s/gupehogeha - this.map.setZoom(7); - } -} diff --git a/frontend/src/scripts/administrativeUnitsMap/index.ts b/frontend/src/scripts/administrativeUnitsMap/index.ts deleted file mode 100644 index 5fc7aac..0000000 --- a/frontend/src/scripts/administrativeUnitsMap/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @ts-ignore -const loadGoogleMapsApi = require('load-google-maps-api'); // must be this way, because of exporting using `export =` -import Map from './Map'; -import Filters from './Filters'; - -document.addEventListener('DOMContentLoaded', async () => { - try { - await loadGoogleMapsApi({key: 'AIzaSyDsxejbWcsI1eb4eoQJq47Eq9qxvSCMXSc'}); - initialize(); - - } catch (e) { - console.error(e); - } -}); - -// Initialize and add the mapInstance -function initialize(): void -{ - const mapEl = document.getElementById('map'); - if (mapEl === null) { // don't do anything if no map - return; - } - - const map = new Map(mapEl); // initialize map - const filters = new Filters(map, document.querySelectorAll('.administrativeUnitsMap__filter')); // initialize filters (listen to click events) - - // custom behavior for about-structure page - // ideally, administrativeUnitsMap should export API, but there's no time play with it now - const unitBaseLinkEl = document.getElementById('about-structure-unit-base-link'); - if (unitBaseLinkEl !== null) { - unitBaseLinkEl.addEventListener('click', _ => // listen to base unit link as well - filters.displayLayer(document.getElementById('mapa-zakladni-clanky')!)); // map exists so this should be present as well - } - - window.addEventListener('load', () => { // finally once the page is loaded, check if a layer filter should be activated - const hash = window.location.hash.substring(1); - - const selectedFilterLinkEl = hash !== '' - ? document.querySelector(`.administrativeUnitsMap__filters #${hash}`) - : null; - - if (selectedFilterLinkEl !== null) { // no filtering element with given hash found => do not filter - filters.displayLayer(selectedFilterLinkEl); - - } else { - filters.displayAll(); - } - }); -} diff --git a/frontend/src/scripts/administrativeUnitsMap/types.ts b/frontend/src/scripts/administrativeUnitsMap/types.ts deleted file mode 100644 index 8f7723d..0000000 --- a/frontend/src/scripts/administrativeUnitsMap/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface OrganizationalUnit -{ - name: string; - description: string|null, - image: string|null, - lat: number; - lng: number; - address: string; - chairman: string|null; - website: string|null; - email: string|null; - isOfTypeClub: boolean; - isOfTypeBase: boolean; - isOfTypeRegional: boolean; - isOfTypeOffice: boolean; - isOfTypeChildren: boolean; -} diff --git a/frontend/src/scripts/administrativeUnitsMap/utils.ts b/frontend/src/scripts/administrativeUnitsMap/utils.ts deleted file mode 100644 index 5119ac1..0000000 --- a/frontend/src/scripts/administrativeUnitsMap/utils.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {OrganizationalUnit} from './types'; - -export function resolveUnitTypeSlug(unit: OrganizationalUnit): string | undefined -{ - if (unit.isOfTypeClub) { - return 'club'; - - } else if (unit.isOfTypeBase) { - return 'base'; - - } else if (unit.isOfTypeRegional) { - return 'regional'; - - } else if (unit.isOfTypeOffice) { - return 'office'; - - } else if (unit.isOfTypeChildren) { - return 'children'; - - } else { // no option selected, fall back to Google Maps default marker - return; - } -} - -export function resolveIconFileName(unit: OrganizationalUnit): string -{ - return `icon-marker-${resolveUnitTypeSlug(unit)}.svg`; -} - -export function resolveUnitTitle(unit: OrganizationalUnit): string -{ - const type = resolveUnitTypeLabel(unit); - if (type === null) { - return unit.name; - } - - return `${unit.name} – ${type}`; -} - -function resolveUnitTypeLabel(unit: OrganizationalUnit): string|null -{ - switch (true) { - case unit.isOfTypeClub: - return 'klub'; - - case unit.isOfTypeBase: - return 'základní článek'; - - case unit.isOfTypeRegional: - return 'regionální centrum'; - - case unit.isOfTypeOffice: - return 'ústředí'; - - case unit.isOfTypeChildren: - return 'dětský oddíl'; - } - - return null; -} diff --git a/functions.php b/functions.php index ba1ae45..e428ca9 100644 --- a/functions.php +++ b/functions.php @@ -86,8 +86,8 @@ Assets::staticScript('lazyLoad', $theme); Assets::staticScript('menuHandler', $theme); Assets::staticScript('lightbox', $theme); + Assets::staticScript('administrativeUnitsMap', $theme); Assets::script('references', $theme); - Assets::script('administrativeUnitsMap', $theme); Assets::style('style', $theme); }); @@ -149,7 +149,7 @@ function hb_administrative_units_map(string $administrationUnitsInJson, bool $ha
diff --git a/gulpfile.babel.js b/gulpfile.babel.js index ff37389..b02e10b 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -17,12 +17,6 @@ const paths = { const buildScriptsTask = (cb) => { buildScripts(cb, [ - { - distFileName: 'administrativeUnitsMap.js', - distPath: paths.scripts.global.dist, // folder to save the compiled js file into - sourceFileName: 'index.ts', - sourcePath: paths.scripts.global.src + '/administrativeUnitsMap', - }, { distFileName: 'references.js', distPath: paths.scripts.global.dist, // folder to save the compiled js file into diff --git a/header.php b/header.php index ded0868..e631874 100644 --- a/header.php +++ b/header.php @@ -25,6 +25,13 @@ + + diff --git a/scripts/administrativeUnitsMap.js b/scripts/administrativeUnitsMap.js new file mode 100644 index 0000000..95335a8 --- /dev/null +++ b/scripts/administrativeUnitsMap.js @@ -0,0 +1,274 @@ +/** + * administrative unit schema: +{ + name: string; + description: string|null, + image: string|null, + lat: number; + lng: number; + address: string; + chairman: string|null; + website: string|null; + email: string|null; + isOfTypeClub: boolean; + isOfTypeBase: boolean; + isOfTypeRegional: boolean; + isOfTypeOffice: boolean; + isOfTypeChildren: boolean; +} +*/ + +document.addEventListener('DOMContentLoaded', async function () { + const map = await initializeMap(); + const filters = initializeFilters(map); + + const getSelectedFilter = () =>{ + const hash = window.location.hash.substring(1); + return hash !== '' + ? document.querySelector(`.administrativeUnitsMap__filters #${hash}`) + : null; + } + + window.addEventListener('load', () => { + const selectedEl = getSelectedFilter(); + if (selectedEl !== null) { + filters.displayLayer(selectedEl); + + } else { + filters.displayAll(); + } + }); + + window.addEventListener('hashchange', () => { + const selectedEl = getSelectedFilter(); + if (selectedEl === null) { + return; + } + + filters.displayLayer(selectedEl); + }); +}); + + +/* MAP */ + +async function initializeMap() +{ + const { Map, Data } = await google.maps.importLibrary("maps"); + const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker"); + + const mapEl = document.getElementById("map"); + const administrativeUnits = JSON.parse(mapEl.getAttribute('data-administrativeUnits')); + + const map = new Map(mapEl, { + center: { lat: 49.7437572, lng: 15.3386383 }, // Czechia geographic center, see https://en.mapy.cz/s/gupehogeha + zoom: 7, + mapId: "b80d048e42b74f71", + }); + const bounds = new google.maps.LatLngBounds(); + let currentInfoWindow; + + const slugs = []; + const layersObj = {}; + + for (const unit of administrativeUnits) { + + // collect slugs + const slug = resolveUnitTypeSlug(unit); + if ( !! slug && ! slugs.includes(slug)) { + slugs.push(slug); + } + + // customize pin style + const color = resolveColor(unit); + const pinEl = new PinElement({ + background: color, + borderColor: "#fff", + glyphColor: color, + }); + + // set marker + const coords = { lat: unit.lat, lng: unit.lng }; + const marker = new AdvancedMarkerElement({ + map: map, + position: coords, + title: unit.name, + content: pinEl.element, + }); + bounds.extend(new google.maps.LatLng(unit.lat, unit.lng)); + + // create info window + const infoWindow = new google.maps.InfoWindow({ + content: buildInfoWindow(unit).outerHTML, + ariaLabel: unit.name, + }); + marker.addListener('click', () => { + currentInfoWindow?.close(); + currentInfoWindow = infoWindow; + + infoWindow.open({ + anchor: marker, + map, + }) + }); + + // add to layer + if ( ! layersObj.hasOwnProperty(slug)) { + layersObj[slug] = []; + } + layersObj[slug].push(marker); + } + + map.fitBounds(bounds); + + return { + displayLayer(filterSlug) { + const allVisible = typeof filterSlug === 'undefined'; + slugs.forEach(slug => { + layersObj[slug].forEach(marker => marker.setMap(allVisible || slug === filterSlug ? map : null)); + }); + }, + }; +} + +function resolveColor(unit) +{ + if (unit.isOfTypeClub) return "#9C0CBE"; + if (unit.isOfTypeBase) return "#FF8F00"; + if (unit.isOfTypeRegional) return "#009BF1"; + if (unit.isOfTypeOffice) return "#00A651"; + if (unit.isOfTypeChildren) return "#D1015F"; + throw new Error("Unsupported unit type"); +} + + +function buildInfoWindow(unit) +{ + const containerEl = document.createElement('div'); + containerEl.id = 'infowindow'; + containerEl.classList.add('administrativeUnitsMap__infoWindow') + + if (unit.image !== null) { + const imageContainerEl = containerEl.appendChild(document.createElement('div')); + imageContainerEl.classList.add('administrativeUnitsMap__infoWindowImageContainer'); + const imageEl = imageContainerEl.appendChild(document.createElement('img')); + imageEl.classList.add('administrativeUnitsMap__infoWindowImage'); + imageEl.src = unit.image; + } + + const contentEl = containerEl.appendChild(document.createElement('div')); + const metaEl = contentEl.appendChild(document.createElement('div')); + + metaEl.innerHTML = resolveUnitTitle(unit); + metaEl.innerHTML += `
Adresa: ${unit.address}`; + + if (unit.chairman !== null) { + metaEl.innerHTML += `
Předseda: ${unit.chairman}`; + } + + if (unit.website !== null ) { + metaEl.innerHTML += `
Web: ${unit.website}` + } + + if (unit.email !== null) { + metaEl.innerHTML += `
E-mail: ${unit.email}`; + } + + if (unit.description !== null) { + const descriptionEl = contentEl.appendChild(document.createElement('p')); + descriptionEl.classList.add('administrativeUnitsMap__infoWindowDescription') + descriptionEl.innerHTML += unit.description; + } + + return containerEl; +} + +function resolveUnitTitle(unit) +{ + const type = resolveUnitTypeLabel(unit); + if (type === null) { + return unit.name; + } + + return `${unit.name} – ${type}`; +} + +function resolveUnitTypeLabel(unit) +{ + switch (true) { + case unit.isOfTypeClub: + return 'klub'; + + case unit.isOfTypeBase: + return 'základní článek'; + + case unit.isOfTypeRegional: + return 'regionální centrum'; + + case unit.isOfTypeOffice: + return 'ústředí'; + + case unit.isOfTypeChildren: + return 'dětský oddíl'; + } + + return null; +} + +function resolveUnitTypeSlug(unit) +{ + if (unit.isOfTypeClub) { + return 'club'; + + } else if (unit.isOfTypeBase) { + return 'base'; + + } else if (unit.isOfTypeRegional) { + return 'regional'; + + } else if (unit.isOfTypeOffice) { + return 'office'; + + } else if (unit.isOfTypeChildren) { + return 'children'; + + } else { // no option selected, fall back to Google Maps default marker + return; + } +} + + +/* FILTERS */ + +function initializeFilters({displayLayer}) +{ + const ActiveFilterSelector = 'administrativeUnitsMap__filter--active'; + + const filters = document.querySelectorAll('.administrativeUnitsMap__filter'); + + const makeActive = (el) => { + filters.forEach(el => el.classList.remove(ActiveFilterSelector)); + el.classList.add(ActiveFilterSelector); + } + + // initialize filters (listen to click events) + filters.forEach(el => + el.addEventListener('click', ev => { + window.history.pushState(null, '', el.children[0].getAttribute('href')); + ev.preventDefault(); + + makeActive(el); + displayLayer(el.dataset.slug); + })); + + return { + displayAll: () => { + makeActive(document.getElementById('mapa-vse')); + displayLayer(); + }, + displayLayer: (el) => { + makeActive(el); + displayLayer(el.dataset.slug); + }, + }; +} diff --git a/template-parts/content/content-hnuti-brontosaurus.php b/template-parts/content/content-hnuti-brontosaurus.php index 65676df..ed6b88c 100644 --- a/template-parts/content/content-hnuti-brontosaurus.php +++ b/template-parts/content/content-hnuti-brontosaurus.php @@ -167,7 +167,7 @@

Na jakoukoliv brontosauří akci může přijet kdokoliv, pokud se však budeš chtít zapojit více - a třeba i nějakou akci také pomáhat pořádat, můžeš se přidat k některému ze základních článků (mapa, + a třeba i nějakou akci také pomáhat pořádat, můžeš se přidat k některému ze základních článků (mapa, seznam).