added new adbrimofcp map
This commit is contained in:
@@ -48,9 +48,12 @@
|
||||
<th>Extref</th>
|
||||
<td><?=$address->extref?></td>
|
||||
</tr><tr>
|
||||
<th>Rimo External ID</th>
|
||||
<td><?=$address->rimo_id?></td>
|
||||
</tr><tr>
|
||||
<th>Rimo External ID</th>
|
||||
<td><?=$address->rimo_id?></td>
|
||||
</tr><tr>
|
||||
<th>Rimo Type</th>
|
||||
<td><?=$address->rimo_type?></td>
|
||||
</tr><tr>
|
||||
<th>Netzgebiet</th>
|
||||
<td><?=$address->netzgebiet->name?></td>
|
||||
</tr><tr>
|
||||
|
||||
@@ -24,7 +24,7 @@ $pagination_entity_name = "Vorbestellungen";
|
||||
}
|
||||
|
||||
.preorder-campaign-header-buttons {
|
||||
max-width: 900px;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.tr-highlight {
|
||||
@@ -458,6 +458,8 @@ $pagination_entity_name = "Vorbestellungen";
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a id="rimo-types-link" target="_blank" style="display:none" href="#" class="btn btn-outline-success"><i class="fas fa-map-marked-alt"></i>Rimo-Typen Karte anzeigen</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1992,6 +1994,21 @@ $pagination_entity_name = "Vorbestellungen";
|
||||
});
|
||||
});
|
||||
campaignSelect.trigger("change");
|
||||
|
||||
// for the Rimo-Typen Karte <a> only show this <a> button if a preordercampaign is selected and change the display and href dynamically
|
||||
const rimoTypesLink = $("#rimo-types-link");
|
||||
function updateRimoTypesLink() {
|
||||
const campaignId = campaignSelect.val();
|
||||
if (campaignId) {
|
||||
rimoTypesLink.show();
|
||||
rimoTypesLink.attr("href", "<?=self::getUrl("Preorder", "RimoTypeMap")?>?preordercampaign_id=" + campaignId);
|
||||
} else {
|
||||
rimoTypesLink.hide();
|
||||
rimoTypesLink.attr("href", "#");
|
||||
}
|
||||
}
|
||||
campaignSelect.on("change", updateRimoTypesLink);
|
||||
updateRimoTypesLink();
|
||||
});
|
||||
</script>
|
||||
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>
|
||||
|
||||
@@ -1788,4 +1788,26 @@ class PreorderController extends mfBaseController {
|
||||
$this->layout()->set("JSGlobals", $JSGlobals);
|
||||
$this->layout()->setTemplate("VueViews/Vue"); // Assuming a generic Vue template
|
||||
}
|
||||
|
||||
public function RimoTypeMapAction() {
|
||||
$allowedCampaigns = Helper::getPreorderCampaignFromUser($this->me);
|
||||
$campaignId = $this->request->preordercampaign_id ?? null;
|
||||
if (!$campaignId || !in_array($campaignId, $allowedCampaigns)) {
|
||||
$this->layout()->setFlash("Ungültige oder keine Kampagne ausgewählt.", "warning");
|
||||
$this->redirect("Preorder", "Index");
|
||||
}
|
||||
|
||||
Helper::renderVue($this, "PreorderRimoTypeMap", "PreorderRimoTypeMap", ["MAPBOX_KEY" => TT_MAPBOX_TILE_API_TOKEN]);
|
||||
}
|
||||
|
||||
public function RimoTypeMapDataAction() {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$campaignId = $input['campaignId'] ?? null;
|
||||
$allowedCampaigns = Helper::getPreorderCampaignFromUser($this->me);
|
||||
|
||||
if (!$campaignId || !in_array($campaignId, $allowedCampaigns)) self::sendError('Ungültige oder keine Kampagne ausgewählt.');
|
||||
|
||||
$data = PreorderModel::getPreorderRimoTypeData($campaignId);
|
||||
self::returnJson(['success' => true, 'data' => $data]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1230,7 +1230,7 @@ class PreorderModel
|
||||
public static function countTotalUnits($preorderCampaignId = null) {
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
$where = "1=1";
|
||||
$where = " h.rimo_type != 'greenfield' ";
|
||||
if ($preorderCampaignId) {
|
||||
$where .= " AND pc.id = $preorderCampaignId";
|
||||
}
|
||||
@@ -1348,5 +1348,35 @@ ORDER BY
|
||||
return $items;
|
||||
}
|
||||
|
||||
public static function getPreorderRimoTypeData(int $campaignId): array {
|
||||
$db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
|
||||
$fronkDbName = defined('FRONKDB_DBNAME') ? FRONKDB_DBNAME : 'thetool';
|
||||
$addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb';
|
||||
$safeCampaignId = (int)$campaignId;
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
h.id AS hausnummer_id, h.gps_lat, h.gps_long, h.rimo_type, h.rimo_op_state, h.rimo_ex_state, h.hausnummer,
|
||||
s.name AS strasse_name, plz.plz AS plz_name, o.name AS ortschaft_name,
|
||||
COUNT(DISTINCT we.id) AS wohneinheit_count,
|
||||
COUNT(DISTINCT pr.id) AS preorder_count
|
||||
FROM `{$addressDbName}`.`Hausnummer` AS h
|
||||
LEFT JOIN `{$addressDbName}`.`Wohneinheit` AS we ON h.id = we.hausnummer_id
|
||||
LEFT JOIN `{$fronkDbName}`.`Preorder` AS pr ON we.id = pr.adb_wohneinheit_id AND pr.preordercampaign_id = {$safeCampaignId} AND pr.deleted = 0
|
||||
LEFT JOIN `{$fronkDbName}`.`Preorderstatus` AS ps ON pr.status_id = ps.id AND ps.code < 899
|
||||
LEFT JOIN `{$addressDbName}`.`Strasse` AS s ON h.strasse_id = s.id
|
||||
LEFT JOIN `{$addressDbName}`.`Plz` AS plz ON h.plz_id = plz.id
|
||||
LEFT JOIN `{$addressDbName}`.`Ortschaft` AS o ON h.ortschaft_id = o.id
|
||||
WHERE h.netzgebiet_id = (
|
||||
SELECT n.adb_netzgebiet_id FROM `{$fronkDbName}`.`Preordercampaign` pc
|
||||
JOIN `{$fronkDbName}`.`Network` n ON pc.network_id = n.id
|
||||
WHERE pc.id = {$safeCampaignId}
|
||||
) AND h.gps_lat IS NOT NULL AND h.gps_long IS NOT NULL
|
||||
GROUP BY h.id
|
||||
ORDER BY h.id
|
||||
";
|
||||
|
||||
$result = $db->query($sql);
|
||||
return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
|
||||
}
|
||||
}
|
||||
|
||||
89
public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css
Normal file
89
public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.css
Normal file
@@ -0,0 +1,89 @@
|
||||
.preorder-map-container {
|
||||
height: 100%; /* Changed from 85vh to fill parent */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.map-filter-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.marker-label {
|
||||
/* This is now the outer, transparent container provided by Leaflet. */
|
||||
/* We remove its background and border to let the inner div's style show through. */
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
|
||||
/* These properties are for Leaflet's positioning and events */
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
transition: visibility 0.2s, opacity 0.2s linear;
|
||||
}
|
||||
|
||||
/* Base style for the new inner div that holds the content */
|
||||
.tooltip-content-wrapper {
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 2px 5px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Style for greenfield markers with at least one preorder */
|
||||
.tooltip-content-wrapper.marker-label-highlight {
|
||||
background-color: rgba(248, 215, 218, 0.9); /* Soft red */
|
||||
border-color: #e57373;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Style for markers where preorders match housing units */
|
||||
.tooltip-content-wrapper.marker-label-saturated {
|
||||
background-color: rgba(212, 237, 218, 0.9); /* Light green */
|
||||
border-color: #81c784;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
/* * This rule is now more specific to ensure it overrides Leaflet's default styles.
|
||||
* It targets the DIV element that has BOTH .leaflet-marker-icon AND .custom-div-icon classes.
|
||||
*/
|
||||
div.leaflet-marker-icon.custom-div-icon {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Base style for all markers */
|
||||
.rimo-marker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||
clip-path: inset(0 round 50%);
|
||||
}
|
||||
|
||||
.rimo-icon {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Specific styles for each rimo_type */
|
||||
.marker-greenfield { background-color: #28a745; }
|
||||
.marker-residential { background-color: #007bff; }
|
||||
.marker-company { background-color: #ffc107; }
|
||||
.marker-multiple-dwelling { background-color: #6f42c1; }
|
||||
.marker-public { background-color: #17a2b8; }
|
||||
.marker-other { background-color: #6c757d; }
|
||||
236
public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
Normal file
236
public/js/pages/PreorderRimoTypeMap/PreorderRimoTypeMap.js
Normal file
@@ -0,0 +1,236 @@
|
||||
Vue.component('PreorderRimoTypeMap', {
|
||||
data() {
|
||||
return {
|
||||
mapMarkers: [],
|
||||
isLoading: false,
|
||||
window,
|
||||
fetchUrl: window.TT_CONFIG.BASE_PATH + '/Preorder/RimoTypeMapData',
|
||||
selectedCampaign: null,
|
||||
mapConfig: {
|
||||
clusterOptions: {
|
||||
spiderfyOnMaxZoom: false,
|
||||
disableClusteringAtZoom: 17,
|
||||
}
|
||||
},
|
||||
mapInstance: null, // Reference to the map instance
|
||||
activeFilters: [], // To store active rimo_type filters
|
||||
filterOptions: [
|
||||
{ value: 'greenfield', text: 'Greenfield', icon: 'fas fa-tree' },
|
||||
{ value: 'residential', text: 'Wohngebiet', icon: 'fas fa-home' },
|
||||
{ value: 'company', text: 'Gewerbe', icon: 'fas fa-building' },
|
||||
{ value: 'multiple-dwelling', text: 'Mehrfamilienhaus', icon: 'fas fa-city' },
|
||||
{ value: 'public', text: 'Öffentlich', icon: 'fas fa-school' },
|
||||
{ value: 'other', text: 'Andere', icon: 'fas fa-question-circle' }
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Filters markers based on the activeFilters array.
|
||||
* If no filters are active, all markers are shown.
|
||||
*/
|
||||
filteredMapMarkers() {
|
||||
if (this.activeFilters.length === 0) {
|
||||
return this.mapMarkers;
|
||||
}
|
||||
return this.mapMarkers.filter(marker => this.activeFilters.includes(marker.rimoType));
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.selectedCampaign = urlParams.get('preordercampaign_id');
|
||||
if (this.selectedCampaign) {
|
||||
await this.fetchAndPrepareData();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const ttMapComponent = this.$refs.ttMap;
|
||||
if (ttMapComponent && ttMapComponent.map) {
|
||||
this.mapInstance = ttMapComponent.map;
|
||||
this.mapInstance.on('zoomend', this.checkZoomLevel);
|
||||
this.checkZoomLevel();
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.mapInstance) {
|
||||
this.mapInstance.off('zoomend', this.checkZoomLevel);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchAndPrepareData() {
|
||||
if (!this.selectedCampaign) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.mapMarkers = [];
|
||||
try {
|
||||
const response = await axios.post(this.fetchUrl, { campaignId: this.selectedCampaign });
|
||||
if (response.data.success && Array.isArray(response.data.data)) {
|
||||
this.mapMarkers = this.processData(response.data.data);
|
||||
} else {
|
||||
window.notify('error', 'Ungültiges Datenformat von der API empfangen.');
|
||||
}
|
||||
} catch (err) {
|
||||
window.notify('error', 'Laden der Kartendaten fehlgeschlagen.');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
processData(data) {
|
||||
const groupedData = {};
|
||||
|
||||
data.forEach(item => {
|
||||
const latLngKey = `${item.gps_lat},${item.gps_long}`;
|
||||
if (!groupedData[latLngKey]) {
|
||||
groupedData[latLngKey] = {
|
||||
...item,
|
||||
wohneinheit_count: parseInt(item.wohneinheit_count, 10),
|
||||
preorder_count: parseInt(item.preorder_count, 10),
|
||||
original_items: [item],
|
||||
};
|
||||
} else {
|
||||
groupedData[latLngKey].wohneinheit_count += parseInt(item.wohneinheit_count, 10);
|
||||
groupedData[latLngKey].preorder_count += parseInt(item.preorder_count, 10);
|
||||
groupedData[latLngKey].original_items.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.values(groupedData).map(group => {
|
||||
const rimoType = this.getNormalizedRimoType(group.rimo_type);
|
||||
const markerIcon = this.getMarkerIcon(rimoType);
|
||||
|
||||
let tooltipInnerClass = '';
|
||||
if (rimoType !== 'greenfield' && group.wohneinheit_count > 0 && group.wohneinheit_count === group.preorder_count) {
|
||||
tooltipInnerClass = ' marker-label-saturated';
|
||||
} else if (rimoType === 'greenfield' && group.preorder_count > 0) {
|
||||
tooltipInnerClass = ' marker-label-highlight';
|
||||
}
|
||||
|
||||
return {
|
||||
lat: group.gps_lat,
|
||||
lng: group.gps_long,
|
||||
rimoType: rimoType, // Add normalized rimoType for filtering
|
||||
options: {
|
||||
icon: {
|
||||
className: `custom-div-icon marker-${rimoType}`,
|
||||
html: `<div class="rimo-marker ${markerIcon.class}"><i class="${markerIcon.icon} rimo-icon"></i></div>`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 30],
|
||||
},
|
||||
tooltip: {
|
||||
content: `<div class="tooltip-content-wrapper${tooltipInnerClass}">H: ${group.wohneinheit_count}<br>B: ${group.preorder_count}</div>`,
|
||||
direction: 'bottom',
|
||||
className: 'marker-label',
|
||||
permanent: true,
|
||||
},
|
||||
asyncPopupContent: async () => {
|
||||
let content = `<div style="font-size: 0.85rem;">`;
|
||||
group.original_items.forEach(item => {
|
||||
content += `
|
||||
<div class="mb-2">
|
||||
<h5 class="mb-2 mt-1">${item.strasse_name} ${item.hausnummer}, ${item.plz_name} ${item.ortschaft_name}</h5>
|
||||
<strong>Rimo Type:</strong> ${item.rimo_type || 'N/A'}<br>
|
||||
<strong>Rimo Op State:</strong> ${item.rimo_op_state || 'N/A'}<br>
|
||||
<strong>Rimo Ex State:</strong> ${item.rimo_ex_state || 'N/A'}<br>
|
||||
<strong>Wohn. gesamt:</strong> ${item.wohneinheit_count}<br>
|
||||
<strong>Bestellungen:</strong> ${item.preorder_count}<br>
|
||||
<strong>Koordinaten:</strong>
|
||||
<a href="https://www.google.com/maps/search/?api=1&query=${item.gps_lat},${item.gps_long}" target="_blank" class="text-primary">
|
||||
<i class="fas fa-map-marker-alt mr-1"></i>Karte
|
||||
</a>
|
||||
<a href="https://thetool.xinon.at/AddressDB/View?id=${item.hausnummer_id}" target="_blank" class="text-primary ml-2">
|
||||
<i class="fas fa-info-circle mr-1"></i>AddressDB
|
||||
</a>
|
||||
</div><hr class="my-1">`;
|
||||
});
|
||||
return content.slice(0, -16) + `</div>`; // Remove last <hr>
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getNormalizedRimoType(type) {
|
||||
const lowerType = (type || '').toLowerCase();
|
||||
if (lowerType.includes('greenfield')) return 'greenfield';
|
||||
if (lowerType.includes('residential')) return 'residential';
|
||||
if (lowerType.includes('multiple dwelling')) return 'multiple-dwelling';
|
||||
if (lowerType.includes('company') || lowerType.includes('commercial')) return 'company';
|
||||
if (lowerType.includes('public') || lowerType.includes('school')) return 'public';
|
||||
return 'other';
|
||||
},
|
||||
|
||||
getMarkerIcon(rimoType) {
|
||||
const icons = {
|
||||
greenfield: { class: 'marker-greenfield', icon: 'fas fa-tree' },
|
||||
residential: { class: 'marker-residential', icon: 'fas fa-home' },
|
||||
company: { class: 'marker-company', icon: 'fas fa-building' },
|
||||
'multiple-dwelling': { class: 'marker-multiple-dwelling', icon: 'fas fa-city' },
|
||||
public: { class: 'marker-public', icon: 'fas fa-school' },
|
||||
other: { class: 'marker-other', icon: 'fas fa-question-circle' }
|
||||
};
|
||||
return icons[rimoType] || icons.other;
|
||||
},
|
||||
|
||||
checkZoomLevel() {
|
||||
if (!this.mapInstance) return;
|
||||
const currentZoom = this.mapInstance.getZoom();
|
||||
const minZoomForLabel = 16;
|
||||
const visibility = currentZoom >= minZoomForLabel ? 'visible' : 'hidden';
|
||||
const opacity = currentZoom >= minZoomForLabel ? '1' : '0';
|
||||
|
||||
document.querySelectorAll('.marker-label').forEach(el => {
|
||||
el.style.visibility = visibility;
|
||||
el.style.opacity = opacity;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles a filter on or off.
|
||||
* @param {string} filterValue - The rimo_type to toggle.
|
||||
*/
|
||||
toggleFilter(filterValue) {
|
||||
const index = this.activeFilters.indexOf(filterValue);
|
||||
if (index > -1) {
|
||||
this.activeFilters.splice(index, 1);
|
||||
} else {
|
||||
this.activeFilters.push(filterValue);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if a filter is currently active.
|
||||
* @param {string} filterValue - The rimo_type to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFilterActive(filterValue) {
|
||||
return this.activeFilters.includes(filterValue);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<tt-card style="height: 75vh; position: relative; display: flex; flex-direction: column;">
|
||||
<div v-if="!selectedCampaign" class="alert alert-warning m-3">
|
||||
Bitte eine Kampagne über den URL-Parameter 'preordercampaign_id' auswählen (z.B. ?preordercampaign_id=44).
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="map-filter-container">
|
||||
<button v-for="filter in filterOptions"
|
||||
:key="filter.value"
|
||||
@click="toggleFilter(filter.value)"
|
||||
:class="['btn', 'btn-sm', isFilterActive(filter.value) ? 'btn-primary' : 'btn-outline-secondary']"
|
||||
:title="filter.text">
|
||||
<i :class="filter.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div style="height: 100%; position: relative; flex-grow: 1;">
|
||||
<div v-if="!isLoading && mapMarkers.length === 0" class="alert alert-info m-3">
|
||||
Keine Standorte für die ausgewählte Kampagne gefunden.
|
||||
</div>
|
||||
<tt-map ref="ttMap" :markers-data="filteredMapMarkers" :loading="isLoading" :config="mapConfig" class="preorder-map-container"></tt-map>
|
||||
</div>
|
||||
</template>
|
||||
</tt-card>
|
||||
`
|
||||
});
|
||||
@@ -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 } }, ...]
|
||||
default: () => [] // Expecting [{ lat: Number, lng: Number, options: { maki?: Object, popup?: String, asyncPopupContent?: Function, icon?: Object, tooltip?: Object } }, ...]
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
@@ -35,7 +35,7 @@ Vue.component('tt-map', {
|
||||
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', // Or 'mapbox/satellite-v9'
|
||||
satelliteTileId: 'mapbox/satellite-streets-v12',
|
||||
tileAttribution: '© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
clusterOptions: {},
|
||||
makiMarkerOptions: { icon: "marker", color: "#3b82f6", size: "m" }
|
||||
@@ -73,37 +73,40 @@ 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; resolve();
|
||||
el.rel = 'stylesheet'; el.href = s.url; el.onload = resolve; el.onerror = reject;
|
||||
}
|
||||
if (el) document.head.appendChild(el); else reject();
|
||||
document.head.appendChild(el);
|
||||
}));
|
||||
return Promise.all(promises);
|
||||
},
|
||||
initializeMap() {
|
||||
if (!this.scriptsLoaded || !L || !L.MarkerClusterGroup || !L.MakiMarkers || !this.mapConfig.mapboxKey) return;
|
||||
if (!this.scriptsLoaded || typeof L === 'undefined' || typeof L.MarkerClusterGroup === 'undefined' || !this.mapConfig.mapboxKey) {
|
||||
console.error("Leaflet or MarkerCluster is not loaded. Cannot initialize map.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.map = L.map(this.$refs.mapContainer, { preferCanvas: true }).setView(this.mapConfig.center, this.mapConfig.zoom);
|
||||
L.MakiMarkers.accessToken = this.mapConfig.mapboxKey;
|
||||
if (typeof L.MakiMarkers !== 'undefined') {
|
||||
L.MakiMarkers.accessToken = this.mapConfig.mapboxKey;
|
||||
}
|
||||
|
||||
this.tileLayers.streets = L.tileLayer(this.mapConfig.streetsTileUrl, {
|
||||
attribution: this.mapConfig.tileAttribution, maxZoom: 18, id: this.mapConfig.streetsTileId,
|
||||
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: 18, id: this.mapConfig.satelliteTileId,
|
||||
attribution: this.mapConfig.tileAttribution, maxZoom: 20, id: this.mapConfig.satelliteTileId,
|
||||
tileSize: 512, zoomOffset: -1, accessToken: this.mapConfig.mapboxKey
|
||||
});
|
||||
|
||||
this.tileLayers[this.mapType].addTo(this.map); // Add initial layer based on preference
|
||||
this.tileLayers[this.mapType].addTo(this.map);
|
||||
|
||||
this.markerLayer = L.markerClusterGroup(this.mapConfig.clusterOptions);
|
||||
this.map.addLayer(this.markerLayer);
|
||||
|
||||
// Invalidate size after initial load if container might not have been ready
|
||||
this.$nextTick(() => {
|
||||
this.map.invalidateSize();
|
||||
});
|
||||
// Add resize listener
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
updateMarkers() {
|
||||
@@ -112,32 +115,54 @@ Vue.component('tt-map', {
|
||||
const markersToAdd = [];
|
||||
this.markersData.forEach(data => {
|
||||
if (data.lat != null && data.lng != null) {
|
||||
const makiOptions = { ...this.mapConfig.makiMarkerOptions, ...(data.options?.maki || {}) };
|
||||
const icon = L.MakiMarkers.icon(makiOptions);
|
||||
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);
|
||||
}
|
||||
|
||||
const marker = L.marker([data.lat, data.lng], { icon: icon });
|
||||
|
||||
if (data.options?.popup) {
|
||||
marker.bindPopup(data.options.popup);
|
||||
} else if (data.options?.asyncPopupContent && typeof data.options.asyncPopupContent === 'function') {
|
||||
marker.bindPopup(() => '<div class="popup-loader">Loading...</div>'); // Initial content
|
||||
marker.bindPopup(() => '<div class="popup-loader">Loading...</div>');
|
||||
marker.on('popupopen', async (e) => {
|
||||
const popup = e.popup;
|
||||
try {
|
||||
const content = await data.options.asyncPopupContent(data); // Pass marker data to function
|
||||
const content = await data.options.asyncPopupContent(data);
|
||||
popup.setContent(content);
|
||||
} catch (error) {
|
||||
console.error("Error loading popup content:", error);
|
||||
popup.setContent('<div class="text-danger">Failed to load content.</div>');
|
||||
}
|
||||
popup.update(); // Adjust size
|
||||
popup.update();
|
||||
});
|
||||
}
|
||||
|
||||
if (data.options?.tooltip) marker.bindTooltip(data.options.tooltip);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
markersToAdd.push(marker);
|
||||
}
|
||||
});
|
||||
if (markersToAdd.length > 0) this.markerLayer.addLayers(markersToAdd);
|
||||
if (markersToAdd.length > 0) {
|
||||
this.markerLayer.addLayers(markersToAdd);
|
||||
this.map.fitBounds(this.markerLayer.getBounds());
|
||||
}
|
||||
},
|
||||
toggleMapType() {
|
||||
this.map.removeLayer(this.tileLayers[this.mapType]);
|
||||
@@ -147,7 +172,6 @@ Vue.component('tt-map', {
|
||||
},
|
||||
handleResize() {
|
||||
if (this.map) {
|
||||
// Use debounce if resize events fire too rapidly
|
||||
this.map.invalidateSize();
|
||||
}
|
||||
}
|
||||
@@ -155,7 +179,6 @@ Vue.component('tt-map', {
|
||||
watch: {
|
||||
markersData: { handler() { this.updateMarkers(); }, deep: true },
|
||||
loading(newVal) {
|
||||
// Optional: Invalidate map size when loading finishes, in case container size changed
|
||||
if (!newVal && this.map) {
|
||||
this.$nextTick(() => this.map.invalidateSize());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user