> */ private array $tableColumns = []; public function __construct( private readonly Connection $connection, private readonly ContaoFramework $framework, private readonly ContentUrlGenerator $urlGenerator, ) { } /** * @return list}> */ public function getMapItems(bool $includeOrganizations = true, bool $includeEvents = true, bool $includeExternalOrganizations = false, array $selectedOrganizationTagIds = []): array { if (!$includeOrganizations && !$includeEvents) { return []; } $selectedOrganizationTagIds = array_values(array_unique(array_map( static fn (int|string $tagId): string => (string) (int) $tagId, array_filter($selectedOrganizationTagIds, static fn (int|string $tagId): bool => (int) $tagId > 0), ))); $locationTable = $this->resolveExistingTable(['tl_location']); $organizationTable = $this->resolveExistingTable(['tl_organization', 'tl_organisation']); $locationGeoColumns = null !== $locationTable ? $this->resolveGeoColumns($locationTable) : null; if (null === $locationTable || null === $locationGeoColumns) { return []; } $items = []; $seen = []; $organizationRows = $includeOrganizations ? $this->fetchOrganizationRows($organizationTable, $locationTable, $locationGeoColumns, $includeExternalOrganizations) : []; $organizationTagMap = $this->fetchOrganizationTagMap(array_values(array_unique(array_map( static fn (array $row): int => (int) ($row['id'] ?? 0), $organizationRows, )))); if ($includeOrganizations) { foreach ($organizationRows as $row) { $id = (int) ($row['id'] ?? 0); if ($id <= 0 || isset($seen['organisation'][$id])) { continue; } $locationId = (int) ($row['location_id'] ?? 0); if ($locationId > 0) { $coords = $this->extractCoordinates($row['location_latitude'] ?? null, $row['location_longitude'] ?? null); if (null === $coords) { continue; } } else { $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null); } if (null === $coords) { continue; } $seen['organisation'][$id] = true; $tagData = $organizationTagMap[$id] ?? ['ids' => [], 'labels' => []]; if ([] !== $selectedOrganizationTagIds && [] === array_intersect($selectedOrganizationTagIds, $tagData['ids'] ?? [])) { continue; } $items[] = [ 'type' => 'organisation', 'markerType' => $this->buildOrganizationMarkerType($tagData['ids']), 'id' => $id, 'title' => trim((string) ($row['title'] ?? '')), 'latitude' => $coords['latitude'], 'longitude' => $coords['longitude'], 'extra' => [ 'organizationTagIds' => $tagData['ids'], 'organizationTagLabels' => $tagData['labels'], ], ]; } } if ($includeEvents) { foreach ($this->fetchEventRows($locationTable, $locationGeoColumns) as $row) { $id = (int) ($row['event_id'] ?? 0); if ($id <= 0 || isset($seen['event'][$id])) { continue; } $coords = $this->extractCoordinates($row['latitude'] ?? null, $row['longitude'] ?? null); if (null === $coords) { continue; } $seen['event'][$id] = true; $items[] = [ 'type' => 'event', 'markerType' => 'event', 'id' => $id, 'title' => trim((string) ($row['event_title'] ?? '')), 'latitude' => $coords['latitude'], 'longitude' => $coords['longitude'], 'extra' => [ 'locationTitle' => trim((string) ($row['location_title'] ?? '')), 'startDate' => $this->formatStartDateTime( (int) ($row['startDate'] ?? 0), (string) ($row['addTime'] ?? ''), (int) ($row['startTime'] ?? 0), ), 'detailUrl' => $this->resolveEventDetailUrl($id), ], ]; } } return $items; } /** * @return list */ public function getOrganizationTags(array $selectedTagIds = []): array { if (!$this->tableExists('tl_tags')) { return []; } $selectedTagIds = array_values(array_unique(array_filter( array_map('intval', $selectedTagIds), static fn (int $tagId): bool => $tagId > 0, ))); $columns = $this->getColumnMap('tl_tags'); $labelColumn = isset($columns['title']) ? 'title' : (isset($columns['tag']) ? 'tag' : null); if (null === $labelColumn) { return []; } $qb = $this->connection->createQueryBuilder(); $qb ->select('t.id', sprintf('t.%s AS label', $labelColumn)) ->from('tl_tags', 't') ->orderBy(sprintf('t.%s', $labelColumn), 'ASC'); if ([] !== $selectedTagIds) { $qb ->andWhere('t.id IN (:selectedTagIds)') ->setParameter('selectedTagIds', $selectedTagIds, ArrayParameterType::INTEGER); } if (isset($columns['invisible'])) { $qb->andWhere("(t.invisible IS NULL OR t.invisible = '' OR t.invisible = '0')"); } $rows = $qb->executeQuery()->fetchAllAssociative(); $tags = []; $seen = []; foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); $label = trim((string) ($row['label'] ?? '')); if ($id <= 0 || '' === $label || isset($seen[$id])) { continue; } $seen[$id] = true; $tags[] = [ 'id' => $id, 'label' => $label, ]; } return $tags; } /** * @param list $organizationIds * @return array, labels: list}> */ private function fetchOrganizationTagMap(array $organizationIds): array { $organizationIds = array_values(array_unique(array_filter($organizationIds, static fn (int $id): bool => $id > 0))); if ([] === $organizationIds || !$this->tableExists('tl_tags_rel') || !$this->tableExists('tl_tags')) { return []; } $rows = $this->connection->executeQuery( 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE r.ptable = ? AND r.field = ? AND r.pid IN (?) ORDER BY r.pid ASC, r.tag_id ASC', ['tl_organization', 'tags', $organizationIds], [ParameterType::STRING, ParameterType::STRING, ArrayParameterType::INTEGER], )->fetchAllAssociative(); if ([] === $rows) { $rows = $this->connection->executeQuery( 'SELECT r.pid AS organization_id, r.tag_id, t.tag AS label FROM tl_tags_rel r INNER JOIN tl_tags t ON t.id = r.tag_id WHERE r.ptable = ? AND r.pid IN (?) ORDER BY r.pid ASC, r.tag_id ASC', ['tl_organization', $organizationIds], [ParameterType::STRING, ArrayParameterType::INTEGER], )->fetchAllAssociative(); } $map = []; $seen = []; foreach ($rows as $row) { $organizationId = (int) ($row['organization_id'] ?? 0); $tagId = (int) ($row['tag_id'] ?? 0); $label = trim((string) ($row['label'] ?? '')); if ($organizationId <= 0 || $tagId <= 0 || '' === $label || isset($seen[$organizationId][$tagId])) { continue; } $seen[$organizationId][$tagId] = true; $map[$organizationId]['ids'][] = (string) $tagId; $map[$organizationId]['labels'][] = $label; } foreach ($map as $organizationId => $tagData) { $map[$organizationId]['ids'] = array_values(array_unique($tagData['ids'] ?? [])); $map[$organizationId]['labels'] = array_values(array_unique($tagData['labels'] ?? [])); } return $map; } /** * @param list $tagIds */ private function buildOrganizationMarkerType(array $tagIds): string { $firstTagId = (string) ($tagIds[0] ?? ''); if ('' !== $firstTagId && ctype_digit($firstTagId) && (int) $firstTagId > 0) { return sprintf('organisation-tag-%d', (int) $firstTagId); } return 'organisation'; } /** * @return list> */ private function fetchOrganizationRows(?string $table, string $locationTable, array $locationGeoColumns, bool $includeExternalOrganizations): array { if (null === $table) { return []; } $organizationGeoColumns = $this->resolveGeoColumns($table); $organizationColumns = $this->getColumnMap($table); $hasLocationReference = isset($organizationColumns['location_id']); if (null === $organizationGeoColumns && !$hasLocationReference) { return []; } $qb = $this->connection->createQueryBuilder(); $qb ->select('o.id', 'o.title') ->from($table, 'o') ->orderBy('o.id', 'ASC'); if (null !== $organizationGeoColumns) { $qb ->addSelect(sprintf('o.%s AS latitude', $organizationGeoColumns['latitude'])) ->addSelect(sprintf('o.%s AS longitude', $organizationGeoColumns['longitude'])); } if ($hasLocationReference) { $qb ->addSelect('o.location_id') ->leftJoin('o', $locationTable, 'ol', 'ol.id = o.location_id') ->addSelect(sprintf('ol.%s AS location_latitude', $locationGeoColumns['latitude'])) ->addSelect(sprintf('ol.%s AS location_longitude', $locationGeoColumns['longitude'])); $this->applyPublicationConstraints($qb, 'ol', $locationTable); } if (!$includeExternalOrganizations && isset($organizationColumns['isexternal'])) { $qb->andWhere("(o.isExternal IS NULL OR o.isExternal = '' OR o.isExternal = '0')"); } $this->applyPublicationConstraints($qb, 'o', $table); return $qb->executeQuery()->fetchAllAssociative(); } /** * @return list> */ private function fetchLocationRows(string $locationTable, array $locationGeoColumns): array { $qb = $this->connection->createQueryBuilder(); $qb ->select('l.id', 'l.title') ->addSelect(sprintf('l.%s AS latitude', $locationGeoColumns['latitude'])) ->addSelect(sprintf('l.%s AS longitude', $locationGeoColumns['longitude'])) ->from($locationTable, 'l') ->orderBy('l.id', 'ASC'); $this->applyPublicationConstraints($qb, 'l', $locationTable); return $qb->executeQuery()->fetchAllAssociative(); } /** * @return list> */ private function fetchEventRows(string $locationTable, array $locationGeoColumns): array { if (!$this->tableExists('tl_calendar_events')) { return []; } $eventColumns = $this->getColumnMap('tl_calendar_events'); $today = strtotime('today'); $qb = $this->connection->createQueryBuilder(); $qb ->select( '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']) ) ->from('tl_calendar_events', 'e') ->innerJoin('e', $locationTable, 'l', 'l.id = e.location_id') ->andWhere('e.location_id > 0') ->orderBy('e.id', 'ASC'); if (isset($eventColumns['startdate'])) { $qb ->andWhere('e.startDate >= :event_start_date_from') ->setParameter('event_start_date_from', $today); } $this->applyPublicationConstraints($qb, 'e', 'tl_calendar_events'); $this->applyPublicationConstraints($qb, 'l', $locationTable); return $qb->executeQuery()->fetchAllAssociative(); } private function applyPublicationConstraints(QueryBuilder $qb, string $alias, string $table): void { $columns = $this->getColumnMap($table); $now = time(); if (isset($columns['published'])) { $qb ->andWhere(sprintf('%s.published = :%s_published', $alias, $alias)) ->setParameter(sprintf('%s_published', $alias), '1'); } if (isset($columns['start'])) { $qb ->andWhere(sprintf('(%1$s.start IS NULL OR %1$s.start = 0 OR %1$s.start <= :%1$s_start_now)', $alias)) ->setParameter(sprintf('%s_start_now', $alias), $now); } if (isset($columns['stop'])) { $qb ->andWhere(sprintf('(%1$s.stop IS NULL OR %1$s.stop = 0 OR %1$s.stop > :%1$s_stop_now)', $alias)) ->setParameter(sprintf('%s_stop_now', $alias), $now); } } /** * @return array{latitude:float,longitude:float}|null */ private function extractCoordinates(mixed $latitude, mixed $longitude): ?array { if (null === $latitude || null === $longitude) { return null; } $lat = trim(str_replace(',', '.', (string) $latitude)); $lng = trim(str_replace(',', '.', (string) $longitude)); if ('' === $lat || '' === $lng || !is_numeric($lat) || !is_numeric($lng)) { return null; } $latitudeFloat = (float) $lat; $longitudeFloat = (float) $lng; if ( ($latitudeFloat < -90.0 || $latitudeFloat > 90.0) || ($longitudeFloat < -180.0 || $longitudeFloat > 180.0) || (0.0 === $latitudeFloat && 0.0 === $longitudeFloat) ) { return null; } return [ 'latitude' => $latitudeFloat, 'longitude' => $longitudeFloat, ]; } private function formatStartDateTime(int $startDate, string $addTime, int $startTime): string { if ($startDate <= 0) { return ''; } $formattedDate = date('d.m.Y', $startDate); 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 ''; } } /** * @param list $candidates */ private function resolveExistingTable(array $candidates): ?string { foreach ($candidates as $candidate) { if ($this->tableExists($candidate)) { return $candidate; } } return null; } /** * @return array{latitude:string,longitude:string}|null */ private function resolveGeoColumns(string $table): ?array { $columns = $this->getColumnMap($table); if (isset($columns['latitude'], $columns['longitude'])) { return [ 'latitude' => 'latitude', 'longitude' => 'longitude', ]; } if (isset($columns['lat'], $columns['lng'])) { return [ 'latitude' => 'lat', 'longitude' => 'lng', ]; } return null; } private function tableExists(string $table): bool { try { return $this->connection->createSchemaManager()->tablesExist([$table]); } catch (\Throwable) { return false; } } /** * @return array */ private function getColumnMap(string $table): array { if (isset($this->tableColumns[$table])) { return $this->tableColumns[$table]; } $columns = []; try { foreach ($this->connection->createSchemaManager()->listTableColumns($table) as $name => $column) { $columns[strtolower((string) $name)] = true; } } catch (\Throwable) { $columns = []; } $this->tableColumns[$table] = $columns; return $columns; } }