Compare commits
2 commits
1d74105910
...
590c842072
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
590c842072 | ||
|
|
9193ac8900 |
10 changed files with 574 additions and 229 deletions
|
|
@ -19,6 +19,14 @@ columns:
|
|||
defaultCenter: [43.836699, 4.360054]
|
||||
defaultZoom: 13
|
||||
maxMarkers: 50
|
||||
mapStaticImage:
|
||||
label: Image statique générée
|
||||
type: files
|
||||
multiple: false
|
||||
query: page.files.filterBy("name", "map-static")
|
||||
layout: cards
|
||||
disabled: true
|
||||
help: Cette image est automatiquement générée à la sauvegarde de la page ou d'un marqueur
|
||||
sidebar:
|
||||
width: 1/3
|
||||
sections:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -50,6 +50,28 @@ Kirby::plugin('geoproject/map-editor', [
|
|||
]
|
||||
],
|
||||
'api' => [
|
||||
'routes' => require __DIR__ . '/api/routes.php'
|
||||
'routes' => [
|
||||
require __DIR__ . '/routes/markers.php',
|
||||
require __DIR__ . '/routes/position.php',
|
||||
require __DIR__ . '/routes/image.php',
|
||||
]
|
||||
],
|
||||
'hooks' => [
|
||||
'page.update:after' => function ($newPage, $oldPage) {
|
||||
// Mark map page for image regeneration
|
||||
if ($newPage->intendedTemplate()->name() === 'map') {
|
||||
$markerFile = $newPage->root() . '/.regenerate-map-image';
|
||||
file_put_contents($markerFile, time());
|
||||
}
|
||||
|
||||
// If a marker is updated, mark the parent map page for regeneration
|
||||
if ($newPage->intendedTemplate()->name() === 'marker') {
|
||||
$mapPage = $newPage->parent();
|
||||
if ($mapPage && $mapPage->intendedTemplate()->name() === 'map') {
|
||||
$markerFile = $mapPage->root() . '/.regenerate-map-image';
|
||||
file_put_contents($markerFile, time());
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
|
|
|||
16
public/site/plugins/map-editor/package-lock.json
generated
16
public/site/plugins/map-editor/package-lock.json
generated
|
|
@ -8,8 +8,10 @@
|
|||
"name": "map-editor",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"js-yaml": "^4.1.0",
|
||||
"maplibre-gl": "^3.6.0"
|
||||
"maplibre-gl": "^3.6.0",
|
||||
"maplibre-gl-map-to-image": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"kirbyup": "^3.3.0"
|
||||
|
|
@ -2410,6 +2412,12 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
|
@ -3194,6 +3202,12 @@
|
|||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl-map-to-image": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl-map-to-image/-/maplibre-gl-map-to-image-1.2.0.tgz",
|
||||
"integrity": "sha512-U4IKKalUd/rudZMHDkpNWqHlOtdLOANDD/s7t8dBPiCAL14zmLAEJ/PE+yFRyl4ZsVymIPAhEPHg/n+7Rq47mA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@
|
|||
"build": "npx -y kirbyup src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"js-yaml": "^4.1.0",
|
||||
"maplibre-gl": "^3.6.0",
|
||||
"js-yaml": "^4.1.0"
|
||||
"maplibre-gl-map-to-image": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"kirbyup": "^3.3.0"
|
||||
|
|
|
|||
185
public/site/plugins/map-editor/routes/image.php
Normal file
185
public/site/plugins/map-editor/routes/image.php
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Image Capture Routes
|
||||
* POST for capturing map image, GET/DELETE for regeneration flag
|
||||
*/
|
||||
|
||||
return [
|
||||
// POST capture and save map image
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/capture-image',
|
||||
'method' => 'POST',
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized',
|
||||
'code' => 401
|
||||
];
|
||||
}
|
||||
|
||||
$mapPage = kirby()->page($pageId);
|
||||
if (!$mapPage) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Map page not found',
|
||||
'code' => 404
|
||||
];
|
||||
}
|
||||
|
||||
if (!$mapPage->permissions()->can('update')) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Forbidden',
|
||||
'code' => 403
|
||||
];
|
||||
}
|
||||
|
||||
$data = kirby()->request()->data();
|
||||
|
||||
if (!isset($data['image'])) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Image data is required',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$imageData = $data['image'];
|
||||
if (preg_match('/^data:image\/(png|jpeg|jpg);base64,(.+)$/', $imageData, $matches)) {
|
||||
$imageData = $matches[2];
|
||||
$extension = $matches[1] === 'jpeg' ? 'jpg' : $matches[1];
|
||||
} else {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid image format',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$decodedImage = base64_decode($imageData);
|
||||
if ($decodedImage === false) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to decode image',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$filename = 'map-static.' . $extension;
|
||||
$tempPath = sys_get_temp_dir() . '/' . uniqid() . '.' . $extension;
|
||||
file_put_contents($tempPath, $decodedImage);
|
||||
|
||||
$existingFile = $mapPage->files()->filterBy('name', 'map-static')->first();
|
||||
if ($existingFile) {
|
||||
$existingFile->delete();
|
||||
}
|
||||
|
||||
try {
|
||||
$file = $mapPage->createFile([
|
||||
'source' => $tempPath,
|
||||
'filename' => $filename
|
||||
]);
|
||||
|
||||
@unlink($tempPath);
|
||||
} catch (Exception $e) {
|
||||
@unlink($tempPath);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'message' => 'Image saved successfully',
|
||||
'filename' => $filename,
|
||||
'path' => $file->root()
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => 500
|
||||
];
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// GET check if regeneration flag exists
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/check-regenerate-flag',
|
||||
'method' => 'GET',
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
$page = kirby()->page($pageId);
|
||||
if (!$page) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Page not found',
|
||||
'code' => 404
|
||||
];
|
||||
}
|
||||
|
||||
$markerFile = $page->root() . '/.regenerate-map-image';
|
||||
$needsRegeneration = file_exists($markerFile);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'needsRegeneration' => $needsRegeneration
|
||||
]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => 500
|
||||
];
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// DELETE clear regeneration flag
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/clear-regenerate-flag',
|
||||
'method' => 'DELETE',
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
$page = kirby()->page($pageId);
|
||||
if (!$page) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Page not found',
|
||||
'code' => 404
|
||||
];
|
||||
}
|
||||
|
||||
$markerFile = $page->root() . '/.regenerate-map-image';
|
||||
if (file_exists($markerFile)) {
|
||||
unlink($markerFile);
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'message' => 'Flag cleared'
|
||||
]
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => 500
|
||||
];
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
|
@ -1,23 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* API Routes for Map Editor Plugin
|
||||
*
|
||||
* Provides CRUD operations for marker subpages
|
||||
* Markers CRUD Routes
|
||||
* GET, POST, PATCH, DELETE operations for markers
|
||||
*/
|
||||
|
||||
return [
|
||||
// GET all markers for a map page
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/markers',
|
||||
'method' => 'GET',
|
||||
'auth' => false, // Allow Panel session auth
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
// Get user from session (Panel context)
|
||||
$user = kirby()->user();
|
||||
|
||||
// For Panel requests, we trust the session is valid
|
||||
// The Panel itself already requires authentication
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -26,7 +23,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Get the map page
|
||||
$mapPage = kirby()->page($pageId);
|
||||
if (!$mapPage) {
|
||||
return [
|
||||
|
|
@ -36,7 +32,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Check if user can read the page
|
||||
if (!$mapPage->isReadable()) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -45,21 +40,16 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Get all marker subpages, listed only, sorted by num
|
||||
$markerPages = $mapPage
|
||||
->children()
|
||||
->listed()
|
||||
->filterBy('intendedTemplate', 'marker')
|
||||
->sortBy('num', 'asc');
|
||||
|
||||
// Format markers for response
|
||||
$markers = [];
|
||||
foreach ($markerPages as $marker) {
|
||||
// Get custom icon if available
|
||||
$iconFile = $marker->markerIcon()->toFile();
|
||||
$iconUrl = $iconFile ? $iconFile->url() : null;
|
||||
|
||||
// Get icon size if set
|
||||
$iconSize = $marker->markerIconSize()->isNotEmpty()
|
||||
? (int) $marker->markerIconSize()->value()
|
||||
: 40;
|
||||
|
|
@ -96,16 +86,15 @@ return [
|
|||
}
|
||||
],
|
||||
|
||||
// POST create new marker
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/markers',
|
||||
'method' => 'POST',
|
||||
'auth' => false, // Allow Panel session auth
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
// Get user from session (Panel context)
|
||||
$user = kirby()->user();
|
||||
|
||||
// For Panel requests, we trust the session is valid
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -114,10 +103,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Note: CSRF verification skipped for Panel session requests
|
||||
// The Panel session itself is already authenticated and secure
|
||||
|
||||
// Get the map page
|
||||
$mapPage = kirby()->page($pageId);
|
||||
if (!$mapPage) {
|
||||
return [
|
||||
|
|
@ -127,7 +112,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Check if user can create children
|
||||
if (!$mapPage->permissions()->can('create')) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -136,8 +120,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Get position from request body
|
||||
// Use data() instead of body() - Kirby automatically parses JSON
|
||||
$data = kirby()->request()->data();
|
||||
|
||||
if (!isset($data['position']['lat']) || !isset($data['position']['lon'])) {
|
||||
|
|
@ -151,7 +133,6 @@ return [
|
|||
$lat = (float) $data['position']['lat'];
|
||||
$lon = (float) $data['position']['lon'];
|
||||
|
||||
// Validate coordinates
|
||||
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -160,17 +141,14 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Get existing markers to determine next num
|
||||
$existingMarkers = $mapPage
|
||||
->children()
|
||||
->filterBy('intendedTemplate', 'marker');
|
||||
$nextNum = $existingMarkers->count() + 1;
|
||||
|
||||
// Generate title and slug based on title
|
||||
$title = 'Marqueur ' . $nextNum;
|
||||
$slug = Str::slug($title);
|
||||
|
||||
// Create the new marker page
|
||||
$newMarker = $mapPage->createChild([
|
||||
'slug' => $slug,
|
||||
'template' => 'marker',
|
||||
|
|
@ -181,10 +159,8 @@ return [
|
|||
]
|
||||
]);
|
||||
|
||||
// Publish the page as listed with the correct num
|
||||
$newMarker->changeStatus('listed', $nextNum);
|
||||
|
||||
// Get custom icon if available (new markers won't have one initially)
|
||||
$iconFile = $newMarker->markerIcon()->toFile();
|
||||
$iconUrl = $iconFile ? $iconFile->url() : null;
|
||||
$iconSize = $newMarker->markerIconSize()->isNotEmpty()
|
||||
|
|
@ -220,13 +196,13 @@ return [
|
|||
}
|
||||
],
|
||||
|
||||
// PATCH update marker position
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/markers/(:all)',
|
||||
'method' => 'PATCH',
|
||||
'auth' => false, // Allow Panel session auth
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId, string $markerId) {
|
||||
try {
|
||||
// Get user from session (Panel context)
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
|
|
@ -237,10 +213,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Note: CSRF verification skipped for Panel session requests
|
||||
// The Panel session itself is already authenticated and secure
|
||||
|
||||
// Get the marker page
|
||||
$marker = kirby()->page($markerId);
|
||||
if (!$marker) {
|
||||
return [
|
||||
|
|
@ -250,7 +222,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Check if user can update the page
|
||||
if (!$marker->permissions()->can('update')) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -259,7 +230,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Get position from request body
|
||||
$data = kirby()->request()->data();
|
||||
|
||||
if (!isset($data['position']['lat']) || !isset($data['position']['lon'])) {
|
||||
|
|
@ -273,7 +243,6 @@ return [
|
|||
$lat = (float) $data['position']['lat'];
|
||||
$lon = (float) $data['position']['lon'];
|
||||
|
||||
// Validate coordinates
|
||||
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -282,13 +251,11 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Update the marker position
|
||||
$marker->update([
|
||||
'latitude' => $lat,
|
||||
'longitude' => $lon
|
||||
]);
|
||||
|
||||
// Get custom icon if available
|
||||
$iconFile = $marker->markerIcon()->toFile();
|
||||
$iconUrl = $iconFile ? $iconFile->url() : null;
|
||||
$iconSize = $marker->markerIconSize()->isNotEmpty()
|
||||
|
|
@ -324,13 +291,13 @@ return [
|
|||
}
|
||||
],
|
||||
|
||||
// DELETE marker
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/markers/(:all)',
|
||||
'method' => 'DELETE',
|
||||
'auth' => false, // Allow Panel session auth
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId, string $markerId) {
|
||||
try {
|
||||
// Get user from session (Panel context)
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
|
|
@ -341,10 +308,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Note: CSRF verification skipped for Panel session requests
|
||||
// The Panel session itself is already authenticated and secure
|
||||
|
||||
// Get the marker page
|
||||
$marker = kirby()->page($markerId);
|
||||
if (!$marker) {
|
||||
return [
|
||||
|
|
@ -354,7 +317,6 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Check if user can delete the page
|
||||
if (!$marker->permissions()->can('delete')) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
|
|
@ -363,8 +325,7 @@ return [
|
|||
];
|
||||
}
|
||||
|
||||
// Delete the marker page
|
||||
$marker->delete(true); // true = force delete
|
||||
$marker->delete(true);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
|
|
@ -373,92 +334,6 @@ return [
|
|||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => 500
|
||||
];
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
[
|
||||
'pattern' => 'map-editor/pages/(:all)/position',
|
||||
'method' => 'PATCH',
|
||||
'auth' => false, // Allow Panel session auth
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
// Get user from session (Panel context)
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized',
|
||||
'code' => 401
|
||||
];
|
||||
}
|
||||
|
||||
// Note: CSRF verification skipped for Panel session requests
|
||||
// The Panel session itself is already authenticated and secure
|
||||
|
||||
// Get the page (marker page in single mode)
|
||||
$page = kirby()->page($pageId);
|
||||
if (!$page) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Page not found',
|
||||
'code' => 404
|
||||
];
|
||||
}
|
||||
|
||||
// Check if user can update the page
|
||||
if (!$page->permissions()->can('update')) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Forbidden',
|
||||
'code' => 403
|
||||
];
|
||||
}
|
||||
|
||||
// Get coordinates from request body
|
||||
$data = kirby()->request()->data();
|
||||
|
||||
if (!isset($data['latitude']) || !isset($data['longitude'])) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Latitude and longitude are required',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$lat = (float) $data['latitude'];
|
||||
$lon = (float) $data['longitude'];
|
||||
|
||||
// Validate coordinates
|
||||
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid coordinates',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
// Update the page position
|
||||
$page->update([
|
||||
'latitude' => $lat,
|
||||
'longitude' => $lon
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'latitude' => $lat,
|
||||
'longitude' => $lon
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
83
public/site/plugins/map-editor/routes/position.php
Normal file
83
public/site/plugins/map-editor/routes/position.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Position Update Route
|
||||
* PATCH route for updating page position (single mode)
|
||||
*/
|
||||
|
||||
return [
|
||||
'pattern' => 'map-editor/pages/(:all)/position',
|
||||
'method' => 'PATCH',
|
||||
'auth' => false,
|
||||
'action' => function (string $pageId) {
|
||||
try {
|
||||
$user = kirby()->user();
|
||||
|
||||
if (!$user && !kirby()->option('debug', false)) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized',
|
||||
'code' => 401
|
||||
];
|
||||
}
|
||||
|
||||
$page = kirby()->page($pageId);
|
||||
if (!$page) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Page not found',
|
||||
'code' => 404
|
||||
];
|
||||
}
|
||||
|
||||
if (!$page->permissions()->can('update')) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Forbidden',
|
||||
'code' => 403
|
||||
];
|
||||
}
|
||||
|
||||
$data = kirby()->request()->data();
|
||||
|
||||
if (!isset($data['latitude']) || !isset($data['longitude'])) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Latitude and longitude are required',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$lat = (float) $data['latitude'];
|
||||
$lon = (float) $data['longitude'];
|
||||
|
||||
if ($lat < -90 || $lat > 90 || $lon < -180 || $lon > 180) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Invalid coordinates',
|
||||
'code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$page->update([
|
||||
'latitude' => $lat,
|
||||
'longitude' => $lon
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'latitude' => $lat,
|
||||
'longitude' => $lon
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => 500
|
||||
];
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
@ -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