Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc2508af22 |
@@ -162,7 +162,7 @@ $GLOBALS['TL_DCA']['tl_module']['fields']['eventListDomId'] = [
|
||||
|
||||
$GLOBALS['TL_DCA']['tl_module']['palettes']['__selector__'][] = 'mapCenterMode';
|
||||
$GLOBALS['TL_DCA']['tl_module']['palettes']['__selector__'][] = 'mapInitialDisplay';
|
||||
$GLOBALS['TL_DCA']['tl_module']['subpalettes']['mapCenterMode_custom'] = 'mapCenterLat,mapCenterLng,mapCenterZoom';
|
||||
$GLOBALS['TL_DCA']['tl_module']['subpalettes']['mapCenterMode_custom'] = 'mapCenterLat,mapCenterLng,mapCenterZoom,mapPitch';
|
||||
$GLOBALS['TL_DCA']['tl_module']['subpalettes']['mapInitialDisplay_organization_tag'] = 'mapInitialOrganizationTagId';
|
||||
|
||||
$GLOBALS['TL_DCA']['tl_module']['fields']['mapShowOrganizations'] = [
|
||||
@@ -271,6 +271,14 @@ $GLOBALS['TL_DCA']['tl_module']['fields']['mapCenterZoom'] = [
|
||||
'sql' => ['type' => 'smallint', 'unsigned' => true, 'default' => 12],
|
||||
];
|
||||
|
||||
$GLOBALS['TL_DCA']['tl_module']['fields']['mapPitch'] = [
|
||||
'label' => &$GLOBALS['TL_LANG']['tl_module']['mapPitch'],
|
||||
'exclude' => true,
|
||||
'inputType' => 'text',
|
||||
'eval' => ['rgxp' => 'digit', 'maxlength' => 2, 'tl_class' => 'w50'],
|
||||
'sql' => ['type' => 'smallint', 'unsigned' => true, 'default' => 0],
|
||||
];
|
||||
|
||||
if (isset($GLOBALS['TL_DCA']['tl_module']['fields']['list_layout'])) {
|
||||
$GLOBALS['TL_DCA']['tl_module']['fields']['list_layout']['options_callback'] = static function (): array {
|
||||
$options = Controller::getTemplateGroup('list_');
|
||||
|
||||
@@ -33,3 +33,4 @@ $GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapCenterLat'] = ['Breitengrad (Center)', 'Breitengrad für die feste Kartenzentrierung, z. B. 51.0538'];
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapCenterLng'] = ['Längengrad (Center)', 'Längengrad für die feste Kartenzentrierung, z. B. 13.3080'];
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapCenterZoom'] = ['Zoom-Level (Center)', 'Zoom-Level für die feste Kartenzentrierung (z. B. 12).'];
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapPitch'] = ['Pitch (Neigung)', 'Kartenneigung in Grad (0-85). Standard bei leerer Angabe: 0.'];
|
||||
|
||||
@@ -33,3 +33,4 @@ $GLOBALS['TL_LANG']['tl_module']['mapCenterMode_options'] = [
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapCenterLat'] = ['Center latitude', 'Latitude for fixed map centering, e.g. 51.0538'];
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapCenterLng'] = ['Center longitude', 'Longitude for fixed map centering, e.g. 13.3080'];
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapCenterZoom'] = ['Center zoom level', 'Zoom level for fixed map centering (e.g. 12).'];
|
||||
$GLOBALS['TL_LANG']['tl_module']['mapPitch'] = ['Pitch', 'Map pitch in degrees (0-85). Default when empty: 0.'];
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
data-map-center-lat="{{ mapCenterLat|default('')|e('html_attr') }}"
|
||||
data-map-center-lng="{{ mapCenterLng|default('')|e('html_attr') }}"
|
||||
data-map-center-zoom="{{ mapCenterZoom|default(12)|e('html_attr') }}"
|
||||
data-map-pitch="{{ mapPitch|default(0)|e('html_attr') }}"
|
||||
></div>
|
||||
|
||||
<style>
|
||||
@@ -107,4 +108,4 @@
|
||||
</style>
|
||||
|
||||
<script type="application/json" id="{{ mapDataElementId|e('html_attr') }}">{{ mapItemsJson|raw }}</script>
|
||||
<script type="module" src="/bundles/mummertmediaeventmanager/assets/map-module.js?v=20260226g"></script>
|
||||
<script type="module" src="/bundles/mummertmediaeventmanager/assets/map-module.js?v=20260226h"></script>
|
||||
|
||||
@@ -118,6 +118,21 @@ const escapeHtml = (value) => {
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const decodeHtmlEntities = (value) => {
|
||||
const stringValue = String(value ?? '');
|
||||
|
||||
if ('undefined' === typeof document || !document.createElement) {
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.innerHTML = stringValue;
|
||||
|
||||
return textarea.value;
|
||||
};
|
||||
|
||||
const sanitizeDisplayText = (value) => escapeHtml(decodeHtmlEntities(value));
|
||||
|
||||
const parseItems = (container) => {
|
||||
const dataElementId = container.dataset.mapDataId || '';
|
||||
|
||||
@@ -406,17 +421,24 @@ const ensureMarkerImage = (map, color, prefix = 'eventmanager-marker', options =
|
||||
|
||||
const popupHtmlFor = (item) => {
|
||||
if (item.type === 'event') {
|
||||
const locationTitle = escapeHtml(item.extra.locationTitle || '');
|
||||
const startDate = escapeHtml(item.extra.startDate || '');
|
||||
const title = sanitizeDisplayText(item.title || '');
|
||||
const locationTitle = sanitizeDisplayText(item.extra.locationTitle || '');
|
||||
const startDate = sanitizeDisplayText(item.extra.startDate || '');
|
||||
const detailUrl = String(item.extra.detailUrl || '').trim();
|
||||
const hasDetailUrl = /^(https?:\/\/|\/)/i.test(detailUrl);
|
||||
const detailLink = hasDetailUrl
|
||||
? `<div><a href="${escapeHtml(detailUrl)}">mehr Infos</a></div>`
|
||||
: '';
|
||||
|
||||
return [
|
||||
`<strong>${escapeHtml(item.title)}</strong>`,
|
||||
`<strong>${title}</strong>`,
|
||||
locationTitle ? `<div>${locationTitle}</div>` : '',
|
||||
startDate ? `<div>${startDate}</div>` : '',
|
||||
detailLink,
|
||||
].join('');
|
||||
}
|
||||
|
||||
return `<strong>${escapeHtml(item.title)}</strong>`;
|
||||
return `<strong>${sanitizeDisplayText(item.title)}</strong>`;
|
||||
};
|
||||
|
||||
const toFeature = (item) => ({
|
||||
@@ -432,6 +454,7 @@ const toFeature = (item) => ({
|
||||
markerTagIds: Array.isArray(item.extra?.organizationTagIds) ? item.extra.organizationTagIds : [],
|
||||
markerTagLabels: Array.isArray(item.extra?.organizationTagLabels) ? item.extra.organizationTagLabels : [],
|
||||
title: item.title,
|
||||
latitude: item.latitude,
|
||||
popupHtml: popupHtmlFor(item),
|
||||
},
|
||||
});
|
||||
@@ -445,7 +468,9 @@ const initOrganizationMarkers = (map, organizationItems, organizationColor) => {
|
||||
};
|
||||
}
|
||||
|
||||
const markerEntries = organizationItems.map((item) => {
|
||||
const sortedOrganizationItems = [...organizationItems].sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude);
|
||||
|
||||
const markerEntries = sortedOrganizationItems.map((item) => {
|
||||
const popupHtml = popupHtmlFor(item);
|
||||
const marker = new maplibregl.Marker(organizationColor ? { color: organizationColor } : undefined)
|
||||
.setLngLat([item.longitude, item.latitude]);
|
||||
@@ -456,6 +481,12 @@ const initOrganizationMarkers = (map, organizationItems, organizationColor) => {
|
||||
|
||||
marker.addTo(map);
|
||||
|
||||
const markerElement = marker.getElement();
|
||||
|
||||
if (markerElement) {
|
||||
markerElement.style.zIndex = String(Math.round((90 - item.latitude) * 1000));
|
||||
}
|
||||
|
||||
return {
|
||||
marker,
|
||||
tagIds: normalizeTagIds(item.extra?.organizationTagIds),
|
||||
@@ -776,6 +807,7 @@ const initLocationLayers = (map, locationItems, markerImageId) => {
|
||||
'icon-anchor': 'bottom',
|
||||
'icon-allow-overlap': true,
|
||||
'icon-ignore-placement': true,
|
||||
'symbol-sort-key': ['*', -1, ['to-number', ['get', 'latitude'], 0]],
|
||||
},
|
||||
paint: {
|
||||
'icon-opacity': 1,
|
||||
@@ -915,6 +947,7 @@ const initEventLayers = (map, eventItems, eventColor, markerImageId, clusterMark
|
||||
'icon-anchor': 'bottom',
|
||||
'icon-allow-overlap': true,
|
||||
'icon-ignore-placement': true,
|
||||
'symbol-sort-key': ['*', -1, ['to-number', ['get', 'latitude'], 0]],
|
||||
},
|
||||
paint: {
|
||||
'icon-opacity': 1,
|
||||
@@ -1132,8 +1165,12 @@ const initSingleMap = (container) => {
|
||||
const customLat = parseCoordinate(container.dataset.mapCenterLat);
|
||||
const customLng = parseCoordinate(container.dataset.mapCenterLng);
|
||||
const customZoom = Number(container.dataset.mapCenterZoom);
|
||||
const customPitch = Number(container.dataset.mapPitch);
|
||||
const eventColor = normalizeHexColor(container.dataset.mapEventColor, DEFAULT_EVENT_COLOR);
|
||||
const hasCustomCenter = null !== customLat && null !== customLng;
|
||||
const initialPitch = Number.isFinite(customPitch)
|
||||
? Math.max(0, Math.min(85, customPitch))
|
||||
: 0;
|
||||
|
||||
let initialCenter = DEFAULT_CENTER;
|
||||
let initialZoom = DEFAULT_ZOOM;
|
||||
@@ -1165,7 +1202,7 @@ const initSingleMap = (container) => {
|
||||
container,
|
||||
style,
|
||||
zoom: initialZoom,
|
||||
pitch: 0,
|
||||
pitch: initialPitch,
|
||||
bearing: 0,
|
||||
center: initialCenter,
|
||||
transformRequest: (url, resourceType) => {
|
||||
@@ -1227,9 +1264,15 @@ const initSingleMap = (container) => {
|
||||
map.on('load', async () => {
|
||||
applyDefaultProjection(map);
|
||||
|
||||
const organizationItems = items.filter((item) => item.type === 'organisation');
|
||||
const locationItems = items.filter((item) => item.type === 'location');
|
||||
const eventItems = items.filter((item) => item.type === 'event');
|
||||
const organizationItems = items
|
||||
.filter((item) => item.type === 'organisation')
|
||||
.sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude);
|
||||
const locationItems = items
|
||||
.filter((item) => item.type === 'location')
|
||||
.sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude);
|
||||
const eventItems = items
|
||||
.filter((item) => item.type === 'event')
|
||||
.sort((firstItem, secondItem) => secondItem.latitude - firstItem.latitude);
|
||||
const allCoordinates = items.map((item) => [item.longitude, item.latitude]);
|
||||
const organizationColor = normalizeHexColor(
|
||||
container.dataset.mapOrganizationColor,
|
||||
|
||||
@@ -47,6 +47,7 @@ class EventMapController extends AbstractFrontendModuleController
|
||||
$initialDisplay = (string) ($model->mapInitialDisplay ?? self::DEFAULT_INITIAL_DISPLAY);
|
||||
$initialOrganizationTagId = (int) ($model->mapInitialOrganizationTagId ?? 0);
|
||||
$centerMode = (string) ($model->mapCenterMode ?? self::DEFAULT_CENTER_MODE);
|
||||
$mapPitch = $this->normalizePitch($model->mapPitch ?? 0);
|
||||
$eventColor = $this->normalizeHexColor((string) ($model->mapEventColor ?? self::DEFAULT_EVENT_COLOR));
|
||||
$organizationColor = $this->normalizeHexColor((string) ($model->mapOrganizationColor ?? $eventColor), $eventColor);
|
||||
|
||||
@@ -77,6 +78,7 @@ class EventMapController extends AbstractFrontendModuleController
|
||||
$template->set('mapCenterLat', trim((string) ($model->mapCenterLat ?? '')));
|
||||
$template->set('mapCenterLng', trim((string) ($model->mapCenterLng ?? '')));
|
||||
$template->set('mapCenterZoom', (int) ($model->mapCenterZoom ?? 12));
|
||||
$template->set('mapPitch', $mapPitch);
|
||||
$template->set('mapItemsJson', json_encode(
|
||||
$this->mapModuleDataProvider->getMapItems($showOrganizations, $showEvents, $showExternalOrganizations, $selectedOrganizationTagIds),
|
||||
\JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR,
|
||||
@@ -118,4 +120,23 @@ class EventMapController extends AbstractFrontendModuleController
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
private function normalizePitch(mixed $value): int
|
||||
{
|
||||
if (!is_scalar($value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$parsedPitch = (int) round((float) str_replace(',', '.', (string) $value));
|
||||
|
||||
if ($parsedPitch < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($parsedPitch > 85) {
|
||||
return 85;
|
||||
}
|
||||
|
||||
return $parsedPitch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace MummertMedia\EventManagerBundle\Service;
|
||||
|
||||
use Contao\Config;
|
||||
use Contao\Date;
|
||||
use Contao\CalendarEventsModel;
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Contao\CoreBundle\Routing\ContentUrlGenerator;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Symfony\Component\Routing\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class MapModuleDataProvider
|
||||
{
|
||||
@@ -20,6 +23,8 @@ class MapModuleDataProvider
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly ContaoFramework $framework,
|
||||
private readonly ContentUrlGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -126,7 +131,12 @@ class MapModuleDataProvider
|
||||
'longitude' => $coords['longitude'],
|
||||
'extra' => [
|
||||
'locationTitle' => trim((string) ($row['location_title'] ?? '')),
|
||||
'startDate' => $this->formatStartDate((int) ($row['startDate'] ?? 0)),
|
||||
'startDate' => $this->formatStartDateTime(
|
||||
(int) ($row['startDate'] ?? 0),
|
||||
(string) ($row['addTime'] ?? ''),
|
||||
(int) ($row['startTime'] ?? 0),
|
||||
),
|
||||
'detailUrl' => $this->resolveEventDetailUrl($id),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -351,6 +361,8 @@ class MapModuleDataProvider
|
||||
'e.id AS event_id',
|
||||
'e.title AS event_title',
|
||||
'e.startDate',
|
||||
'e.addTime',
|
||||
'e.startTime',
|
||||
'l.title AS location_title',
|
||||
sprintf('l.%s AS latitude', $locationGeoColumns['latitude']),
|
||||
sprintf('l.%s AS longitude', $locationGeoColumns['longitude'])
|
||||
@@ -429,15 +441,40 @@ class MapModuleDataProvider
|
||||
];
|
||||
}
|
||||
|
||||
private function formatStartDate(int $timestamp): string
|
||||
private function formatStartDateTime(int $startDate, string $addTime, int $startTime): string
|
||||
{
|
||||
if ($timestamp <= 0) {
|
||||
if ($startDate <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$format = (string) Config::get('dateFormat');
|
||||
$formattedDate = date('d.m.Y', $startDate);
|
||||
|
||||
return Date::parse('' !== $format ? $format : 'd.m.Y', $timestamp);
|
||||
if ('1' !== $addTime || $startTime <= 0) {
|
||||
return $formattedDate;
|
||||
}
|
||||
|
||||
return sprintf('%s %s Uhr', $formattedDate, date('H:i', $startTime));
|
||||
}
|
||||
|
||||
private function resolveEventDetailUrl(int $eventId): string
|
||||
{
|
||||
if ($eventId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$eventModel = $this->framework
|
||||
->getAdapter(CalendarEventsModel::class)
|
||||
->findById($eventId);
|
||||
|
||||
if (null === $eventModel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return (string) $this->urlGenerator->generate($eventModel, [], UrlGeneratorInterface::ABSOLUTE_PATH);
|
||||
} catch (ExceptionInterface) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user