feat: implement automatic static map image generation
- Add html-to-image for capturing map container with markers - Auto-generate map image on page/marker save via hooks - Use flag system (.regenerate-map-image) to trigger generation on Panel reload - Create file using Kirby API for proper indexing - Add mapStaticImage field in blueprint to display generated image - Wait for map to be fully loaded before capture - Capture entire container (map + custom markers) - Filter MapLibre controls from capture Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1d74105910
commit
9193ac8900
8 changed files with 474 additions and 92 deletions
|
|
@ -308,6 +308,11 @@ export default {
|
|||
// Use nextTick to ensure all reactive updates from loadMapData are done
|
||||
await nextTick();
|
||||
isInitialLoad.value = false;
|
||||
|
||||
// Check if we need to regenerate the map image (multi mode only)
|
||||
if (props.mode === 'multi') {
|
||||
await checkAndRegenerateImage();
|
||||
}
|
||||
});
|
||||
|
||||
// Watch center and zoom for automatic save (multi mode only)
|
||||
|
|
@ -529,6 +534,111 @@ export default {
|
|||
saveMapData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if map image needs regeneration and do it if needed
|
||||
*/
|
||||
async function checkAndRegenerateImage() {
|
||||
if (!pageId.value) return;
|
||||
|
||||
try {
|
||||
// Check if regeneration flag exists
|
||||
const checkResponse = await fetch(
|
||||
`/api/map-editor/pages/${pageId.value}/check-regenerate-flag`
|
||||
);
|
||||
const checkResult = await checkResponse.json();
|
||||
|
||||
if (checkResult.status === 'success' && checkResult.data.needsRegeneration) {
|
||||
console.log('Regeneration flag detected, waiting for map to be ready...');
|
||||
|
||||
// Wait for the map to be fully loaded with markers
|
||||
await waitForMapReady();
|
||||
|
||||
console.log('Map ready, capturing image...');
|
||||
|
||||
// Capture and save the image
|
||||
await captureAndSaveMapImage();
|
||||
|
||||
// Clear the flag
|
||||
await fetch(
|
||||
`/api/map-editor/pages/${pageId.value}/clear-regenerate-flag`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
console.log('Map image regenerated successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking/regenerating map image:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the map to be fully loaded and ready for capture
|
||||
*/
|
||||
async function waitForMapReady(maxAttempts = 10) {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
// Check if map is loaded
|
||||
if (mapPreview.value?.map?.loaded && mapPreview.value.map.loaded()) {
|
||||
// Wait an additional 500ms for markers to render
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wait 500ms before next attempt
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
throw new Error('Map failed to load within timeout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture map as image and send to server
|
||||
*/
|
||||
async function captureAndSaveMapImage() {
|
||||
if (!mapPreview.value || !mapPreview.value.captureMapImage) {
|
||||
console.warn('Map preview not ready for capture');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Starting image capture...');
|
||||
|
||||
// Capture the map as base64 image with timeout
|
||||
const imageDataUrl = await Promise.race([
|
||||
mapPreview.value.captureMapImage(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Capture timeout after 10s')), 10000)
|
||||
)
|
||||
]);
|
||||
console.log('Image captured, size:', imageDataUrl.length, 'bytes');
|
||||
|
||||
// Send to API
|
||||
console.log('Sending to API:', `/api/map-editor/pages/${pageId.value}/capture-image`);
|
||||
const response = await fetch(
|
||||
`/api/map-editor/pages/${pageId.value}/capture-image`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ image: imageDataUrl }),
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
const result = await response.json();
|
||||
console.log('Response data:', result);
|
||||
|
||||
if (result.status === 'error') {
|
||||
throw new Error(result.message || 'Failed to save map image');
|
||||
}
|
||||
|
||||
console.log('Map image saved successfully:', result.data.filename);
|
||||
} catch (error) {
|
||||
console.error('Error capturing and saving map image:', error);
|
||||
throw error; // Re-throw to see the error in checkAndRegenerateImage
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
center,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import { toPng } from "html-to-image";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
|
@ -97,6 +98,7 @@ export default {
|
|||
try {
|
||||
map.value = new maplibregl.Map({
|
||||
container: mapContainer.value,
|
||||
preserveDrawingBuffer: true, // Required for canvas.toDataURL()
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
|
|
@ -326,12 +328,55 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
async function captureMapImage() {
|
||||
console.log('[MapPreview] captureMapImage called');
|
||||
|
||||
if (!map.value) {
|
||||
throw new Error("Map is not initialized");
|
||||
}
|
||||
|
||||
if (!map.value.loaded()) {
|
||||
throw new Error("Map is not loaded");
|
||||
}
|
||||
|
||||
if (!mapContainer.value) {
|
||||
throw new Error("Map container not found");
|
||||
}
|
||||
|
||||
console.log('[MapPreview] Map is loaded, capturing container...');
|
||||
|
||||
try {
|
||||
// Capture the entire map container (includes canvas + markers)
|
||||
const imageDataUrl = await toPng(mapContainer.value, {
|
||||
quality: 0.95,
|
||||
pixelRatio: 2, // Higher quality
|
||||
cacheBust: true,
|
||||
filter: (node) => {
|
||||
// Exclude MapLibre controls (zoom buttons, etc.)
|
||||
if (node.classList && node.classList.contains('maplibregl-ctrl')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MapPreview] Container captured, image size:', imageDataUrl?.length);
|
||||
|
||||
return imageDataUrl;
|
||||
} catch (error) {
|
||||
console.error("[MapPreview] Error capturing map image:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mapContainer,
|
||||
loading,
|
||||
map,
|
||||
getCurrentCenter,
|
||||
getCurrentZoom,
|
||||
centerOnPosition
|
||||
centerOnPosition,
|
||||
captureMapImage
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue