feat: add custom marker icons with configurable size

- Add markerIcon files field to marker.yml for custom JPG/PNG/SVG icons
- Add markerIconSize range field (20-500px, default 40px) with unit display
- Layout icon fields side-by-side (50/50 width) in marker blueprint
- Add markerIconUrl prop in index.php to auto-detect uploaded icon
- Add markerIconSize prop in index.php to read size from page data
- Update MapPreview.vue to display custom images instead of default pins
- Set icon dimensions dynamically based on markerIconSize value
- Icon size updates on save/reload (reactive implementation deferred)
- Remove custom tiles background functionality (not needed)

Note: Custom icons show uploaded image, may have white background on
transparent PNGs depending on image processing. Size is non-reactive
and requires save + reload to update in preview.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-01-29 16:14:33 +01:00
parent 925e98aea7
commit b19635f324
6 changed files with 161 additions and 58 deletions

View file

@ -79,6 +79,14 @@ export default {
type: [Number, String],
default: null,
},
markerIconUrl: {
type: String,
default: null,
},
markerIconSize: {
type: Number,
default: 40,
},
},
setup(props, { emit }) {
@ -138,6 +146,8 @@ export default {
id: 'single-marker',
position: { lat, lon },
title: 'Current position',
iconUrl: props.markerIconUrl,
iconSize: props.markerIconSize,
}];
}
return [];

View file

@ -181,19 +181,39 @@ export default {
const el = document.createElement("div");
el.className = "custom-marker";
// Create inner wrapper for visual transforms (isolates from MapLibre transforms)
const inner = document.createElement("div");
inner.className = "marker-inner";
if (props.selectedMarkerId === markerData.id) {
inner.classList.add("selected");
}
// Check if custom icon is provided
if (markerData.iconUrl) {
// Use custom image
el.classList.add("custom-icon");
const img = document.createElement("img");
img.src = markerData.iconUrl;
img.className = "marker-icon-image";
// Add marker number
const numberEl = document.createElement("div");
numberEl.className = "marker-number";
numberEl.textContent = index + 1;
inner.appendChild(numberEl);
el.appendChild(inner);
// Set size from marker data or default to 40px
const size = markerData.iconSize || 40;
img.style.width = `${size}px`;
img.style.height = `${size}px`;
if (props.selectedMarkerId === markerData.id) {
img.classList.add("selected");
}
el.appendChild(img);
} else {
// Use default pin marker
// Create inner wrapper for visual transforms (isolates from MapLibre transforms)
const inner = document.createElement("div");
inner.className = "marker-inner";
if (props.selectedMarkerId === markerData.id) {
inner.classList.add("selected");
}
// Add marker number
const numberEl = document.createElement("div");
numberEl.className = "marker-number";
numberEl.textContent = index + 1;
inner.appendChild(numberEl);
el.appendChild(inner);
}
try {
const coords = [markerData.position.lon, markerData.position.lat];
@ -252,6 +272,7 @@ export default {
function updateMarkerSelection(selectedId) {
markerElements.value.forEach(({ element }, markerId) => {
if (element) {
// Handle default pin marker
const inner = element.querySelector('.marker-inner');
if (inner) {
if (markerId === selectedId) {
@ -260,6 +281,16 @@ export default {
inner.classList.remove("selected");
}
}
// Handle custom icon marker
const img = element.querySelector('.marker-icon-image');
if (img) {
if (markerId === selectedId) {
img.classList.add("selected");
} else {
img.classList.remove("selected");
}
}
}
});
}
@ -399,6 +430,30 @@ export default {
pointer-events: none;
}
/* Custom icon marker */
.custom-marker.custom-icon {
cursor: grab;
}
.custom-marker.custom-icon:active {
cursor: grabbing;
}
.marker-icon-image {
object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
transition: all 0.2s;
}
.marker-icon-image:hover {
transform: scale(1.1);
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.4));
}
.marker-icon-image.selected {
filter: drop-shadow(0 4px 12px rgba(52, 152, 219, 0.8));
}
/* MapLibre controls styling */
.maplibregl-ctrl-group {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);