-
- Keine Standorte für die ausgewählte Kampagne gefunden.
+
+
+
+
Filter:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Legende
+
H: Homes (Wohneinheiten)
+
B: Bestellungen
+
+
+
+
+ Fehlerliste ({{ faultsForModal.filter(f => !f.done).length }})
+
+
+
+
+
+
+
+
`
});
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/css/tt-map.css b/public/plugins/vue/tt-components/css/tt-map.css
new file mode 100644
index 000000000..61b021f8c
--- /dev/null
+++ b/public/plugins/vue/tt-components/css/tt-map.css
@@ -0,0 +1,77 @@
+/* tt-map.css */
+.tt-map-wrapper {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background-color: #f0f0f0; /* Placeholder color while map loads */
+}
+
+.tt-map-container {
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+.tt-map-loader {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1001;
+ background: rgba(255, 255, 255, 0.7);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+/* Slot for top controls like filters */
+.tt-map-top-controls {
+ position: absolute;
+ top: 10px;
+ left: 55px; /* Moved to avoid overlapping with zoom controls */
+ z-index: 401;
+}
+
+/* Slot for bottom controls like legend */
+.tt-map-bottom-controls {
+ position: absolute;
+ bottom: 30px;
+ right: 10px;
+ z-index: 401;
+}
+
+/* Container for built-in map buttons (zoom, layer toggle) */
+.tt-map-builtin-controls {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 401;
+ display: flex;
+ gap: 0.5rem;
+ flex-direction: column;
+ align-items: flex-end;
+}
+
+.tt-map-builtin-controls .btn {
+ min-width: 120px;
+ text-align: left;
+ box-shadow: 0 1px 5px rgba(0,0,0,0.4);
+}
+
+.tt-map-settings-panel {
+ background-color: white;
+ padding: 0.75rem;
+ border-radius: 5px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.25);
+ border: 1px solid rgba(0,0,0,0.1);
+}
+
+.tt-map-settings-panel .btn-group .btn {
+ font-size: 0.8rem;
+}
+
+.popup-loader {
+ text-align: center;
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/public/plugins/vue/tt-components/tt-map.js b/public/plugins/vue/tt-components/tt-map.js
index 3fdd93aa9..0c6578523 100644
--- a/public/plugins/vue/tt-components/tt-map.js
+++ b/public/plugins/vue/tt-components/tt-map.js
@@ -2,7 +2,7 @@ Vue.component('tt-map', {
props: {
markersData: {
type: Array,
- default: () => [] // Expecting [{ lat: Number, lng: Number, options: { maki?: Object, popup?: String, asyncPopupContent?: Function, icon?: Object, tooltip?: Object } }, ...]
+ default: () => [] // Expecting [{ lat: Number, lng: Number, options: { ..., noCluster?: Boolean, tooltip: { content: '...', minZoom: 18, ... } } }, ...]
},
config: {
type: Object,
@@ -16,11 +16,17 @@ Vue.component('tt-map', {
data() {
return {
map: null,
- markerLayer: null,
- tileLayers: { streets: null, satellite: null },
- mapType: localStorage.getItem('tt-map-type') || 'streets', // Default to 'streets' or stored preference
+ markerLayer: null, // For clustered markers
+ nonClusteredLayer: null, // For important, always-visible markers
+ tileLayers: {
+ mapbox: { streets: null, satellite: null },
+ basemap: { streets: null, satellite: null }
+ },
+ mapProvider: localStorage.getItem('tt-map-provider') || 'basemap',
+ mapType: localStorage.getItem('tt-map-type') || 'streets',
internalLoading: true,
scriptsLoaded: false,
+ showSettings: false,
};
},
computed: {
@@ -29,14 +35,17 @@ Vue.component('tt-map', {
},
mapConfig() {
const defaults = {
- center: [46.9, 15.4995],
- zoom: 11,
+ center: [47.07, 15.44], // Centered on Graz, Styria
+ zoom: 9,
mapboxKey: window.TT_CONFIG?.MAPBOX_KEY,
streetsTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
streetsTileId: 'mapbox/streets-v11',
satelliteTileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
satelliteTileId: 'mapbox/satellite-streets-v12',
tileAttribution: '©
Mapbox ©
OpenStreetMap ',
+ basemapStreetsTileUrl: 'https://maps.wien.gv.at/basemap/geolandbasemap/normal/google3857/{z}/{y}/{x}.png',
+ basemapSatelliteTileUrl: 'https://maps.wien.gv.at/basemap/bmaporthofoto30cm/normal/google3857/{z}/{y}/{x}.jpeg',
+ basemapAttribution: 'Datenquelle: basemap.at, Stadt Wien - data.wien.gv.at',
clusterOptions: {},
makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" }
};
@@ -73,7 +82,12 @@ Vue.component('tt-map', {
el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject;
} else {
el = document.createElement('link');
- el.rel = 'stylesheet'; el.href = s.url; el.onload = resolve; el.onerror = reject;
+ el.rel = 'stylesheet'; el.href = s.url;
+ el.onload = resolve;
+ el.onerror = () => {
+ console.warn(`Could not load stylesheet: ${s.url}`);
+ resolve();
+ };
}
document.head.appendChild(el);
}));
@@ -90,85 +104,145 @@ Vue.component('tt-map', {
L.MakiMarkers.accessToken = this.mapConfig.mapboxKey;
}
- this.tileLayers.streets = L.tileLayer(this.mapConfig.streetsTileUrl, {
- attribution: this.mapConfig.tileAttribution, maxZoom: 20, id: this.mapConfig.streetsTileId,
- tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey
- });
- this.tileLayers.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, {
- attribution: this.mapConfig.tileAttribution, maxZoom: 20, id: this.mapConfig.satelliteTileId,
- tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey
- });
+ this.map.createPane('fcpPane');
+ this.map.getPane('fcpPane').style.zIndex = 599; // Default markerPane is 600
- this.tileLayers[this.mapType].addTo(this.map);
+ this.tileLayers.mapbox.streets = L.tileLayer(this.mapConfig.streetsTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 22, id: this.mapConfig.streetsTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey });
+ this.tileLayers.mapbox.satellite = L.tileLayer(this.mapConfig.satelliteTileUrl, { attribution: this.mapConfig.tileAttribution, maxZoom: 22, id: this.mapConfig.satelliteTileId, tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey });
+ this.tileLayers.basemap.streets = L.tileLayer(this.mapConfig.basemapStreetsTileUrl, { attribution: this.mapConfig.basemapAttribution, maxZoom: 20 });
+ this.tileLayers.basemap.satellite = L.tileLayer(this.mapConfig.basemapSatelliteTileUrl, { attribution: this.mapConfig.basemapAttribution, maxZoom: 20 });
+ this.setActiveTileLayer();
this.markerLayer = L.markerClusterGroup(this.mapConfig.clusterOptions);
+ this.nonClusteredLayer = L.layerGroup();
this.map.addLayer(this.markerLayer);
+ this.map.addLayer(this.nonClusteredLayer);
- this.$nextTick(() => {
- this.map.invalidateSize();
- });
+ this.map.on('zoomend moveend', this.updateTooltipVisibility);
+
+ this.$nextTick(() => { this.map.invalidateSize(); });
window.addEventListener('resize', this.handleResize);
},
+ setActiveTileLayer() {
+ if (!this.map) return;
+ Object.values(this.tileLayers).forEach(provider => {
+ Object.values(provider).forEach(layer => {
+ if (layer && this.map.hasLayer(layer)) {
+ this.map.removeLayer(layer);
+ }
+ });
+ });
+
+ const activeLayer = this.tileLayers[this.mapProvider]?.[this.mapType];
+ if (activeLayer) {
+ activeLayer.addTo(this.map);
+ } else {
+ this.tileLayers.mapbox.streets.addTo(this.map);
+ }
+ },
updateMarkers() {
if (!this.map || !this.markerLayer || !this.scriptsLoaded) return;
this.markerLayer.clearLayers();
- const markersToAdd = [];
+ this.nonClusteredLayer.clearLayers();
+ const markersToCluster = [];
+
this.markersData.forEach(data => {
- if (data.lat != null && data.lng != null) {
- let icon;
- if (data.options?.icon instanceof L.Icon) {
- icon = data.options.icon;
- } else if (data.options?.icon) {
- icon = L.divIcon(data.options.icon);
- } else if (typeof L.MakiMarkers !== 'undefined') {
- const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) };
- icon = L.MakiMarkers.icon(makiOptions);
- }
+ if (data.lat == null || data.lng == null) return;
- const marker = L.marker([data.lat, data.lng], { icon: icon });
+ let icon;
+ if (data.options?.icon instanceof L.Icon) icon = data.options.icon;
+ else if (data.options?.icon) icon = L.divIcon(data.options.icon);
+ else if (typeof L.MakiMarkers !== 'undefined') {
+ const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) };
+ icon = L.MakiMarkers.icon(makiOptions);
+ }
- if (data.options?.popup) {
- marker.bindPopup(data.options.popup);
- } else if (data.options?.asyncPopupContent && typeof data.options.asyncPopupContent === 'function') {
- marker.bindPopup(() => '');
- marker.on('popupopen', async (e) => {
- const popup = e.popup;
- try {
- const content = await data.options.asyncPopupContent(data);
- popup.setContent(content);
- } catch (error) {
- console.error("Error loading popup content:", error);
- popup.setContent('
Failed to load content.
');
- }
- popup.update();
- });
- }
+ const markerOptions = { icon };
+ if (data.options?.zIndexOffset) markerOptions.zIndexOffset = data.options.zIndexOffset;
- if (data.options?.tooltip) {
- // Check if it's an object with content and options, or just a string
- if (typeof data.options.tooltip === 'object') {
- const tooltipContent = data.options.tooltip.content;
- const tooltipOptions = { ...data.options.tooltip };
- delete tooltipOptions.content; // Remove content to prevent conflicts
- marker.bindTooltip(tooltipContent, tooltipOptions);
- } else {
- marker.bindTooltip(data.options.tooltip);
- }
- }
+ if (data.options?.noCluster) {
+ markerOptions.pane = 'fcpPane';
+ }
- markersToAdd.push(marker);
+ const marker = L.marker([data.lat, data.lng], markerOptions);
+
+ // Attach a unique identifier if provided
+ if (data.hausnummerId) {
+ marker.tt_hausnummerId = data.hausnummerId;
+ }
+
+ if (data.options?.tooltip) {
+ marker.tt_tooltip_options = data.options.tooltip;
+ }
+
+ if (data.options?.popup) marker.bindPopup(data.options.popup);
+ else if (data.options?.asyncPopupContent) {
+ marker.bindPopup(() => '');
+ marker.on('popupopen', async (e) => {
+ const content = await data.options.asyncPopupContent(data);
+ e.popup.setContent(content).update();
+ });
+ }
+
+ if (data.options?.noCluster) {
+ this.nonClusteredLayer.addLayer(marker);
+ } else {
+ markersToCluster.push(marker);
}
});
- if (markersToAdd.length > 0) {
- this.markerLayer.addLayers(markersToAdd);
- this.map.fitBounds(this.markerLayer.getBounds());
+
+ if (markersToCluster.length > 0) {
+ this.markerLayer.addLayers(markersToCluster);
+ }
+
+ this.updateTooltipVisibility();
+
+ const allMarkers = this.markerLayer.getLayers().concat(this.nonClusteredLayer.getLayers());
+ if (allMarkers.length > 0) {
+ const bounds = L.latLngBounds(allMarkers.map(marker => marker.getLatLng()));
+ if (bounds.isValid()) {
+ this.map.fitBounds(bounds, { padding: [50, 50], maxZoom: 17 });
+ }
}
},
+ updateTooltipVisibility() {
+ if (!this.map) return;
+ const currentZoom = this.map.getZoom();
+
+ const processMarker = (marker) => {
+ const tooltipOptions = marker.tt_tooltip_options;
+ if (!tooltipOptions || !tooltipOptions.minZoom) return;
+
+ const isBound = marker.getTooltip();
+
+ if (currentZoom >= tooltipOptions.minZoom) {
+ if (!isBound) {
+ const { content, minZoom, ...options } = tooltipOptions;
+ marker.bindTooltip(content, options);
+ if (options.permanent) {
+ marker.openTooltip();
+ }
+ }
+ } else {
+ if (isBound) {
+ marker.unbindTooltip();
+ }
+ }
+ };
+
+ this.markerLayer.eachLayer(processMarker);
+ this.nonClusteredLayer.eachLayer(processMarker);
+ },
toggleMapType() {
- this.map.removeLayer(this.tileLayers[this.mapType]);
this.mapType = this.mapType === 'streets' ? 'satellite' : 'streets';
- this.tileLayers[this.mapType].addTo(this.map);
localStorage.setItem('tt-map-type', this.mapType);
+ this.setActiveTileLayer();
+ },
+ setMapProvider(provider) {
+ this.mapProvider = provider;
+ localStorage.setItem('tt-map-provider', provider);
+ this.setActiveTileLayer();
+ this.showSettings = false;
},
handleResize() {
if (this.map) {
@@ -187,22 +261,47 @@ Vue.component('tt-map', {
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
if (this.map) {
+ this.map.off('zoomend moveend', this.updateTooltipVisibility);
this.map.remove();
this.map = null;
}
},
template: `
-
-
+
+
-
-
-
- {{ mapType === 'streets' ? 'Satellite' : 'Map' }}
-
+
+
+
+
+
+
+
+
+
+
+ {{ mapType === 'streets' ? 'Satellit' : 'Karte' }}
+
+
+
+ Einstellungen
+
+
+
+
+
+
+
+
`
});
\ No newline at end of file