Add dynamic image generation for department lanes

- Implement `/department/lanes/dynamic-image` endpoint for machine UI dynamic image rendering.
- Add OpenAPI specification for the new endpoint with query parameters for buttons, current step, and vehicle type.
- Include `DepartmentLanesImageTest` for lightweight testing of image behavior and input normalization.
- Update `departmentLanesRoute.php` with logic for parameter handling and image composition based on lane configuration.
This commit is contained in:
Jeppe Bundgaard
2026-02-18 16:44:18 +01:00
parent 73ac77a2f5
commit 54160659fc
3 changed files with 258 additions and 0 deletions
+71
View File
@@ -1958,6 +1958,77 @@ paths:
'404':
$ref: '#/components/responses/NotFound'
/department/lanes/dynamic-image:
get:
tags:
- Departments
summary: Generate dynamic image for a department lane
description: |
Returns a composed machine UI image for the specified department lane.
You can optionally highlight button indices, set the current step indicator, and toggle only-current-step mode.
operationId: getDepartmentLaneDynamicImage
parameters:
- name: department
in: query
required: true
description: Department ID
schema:
type: integer
minimum: 1
- name: lane
in: query
required: true
description: Lane ID
schema:
type: integer
minimum: 1
- name: buttons
in: query
required: false
description: Highlighted button IDs (0-indexed). Accepts CSV, JSON array, or repeated query params.
schema:
oneOf:
- type: string
- type: array
items:
type: integer
- name: current_step
in: query
required: false
description: Current step indicator (non-negative integer)
schema:
type: integer
minimum: 0
- name: only_current_step
in: query
required: false
description: If true, only draw the current step highlight
schema:
type: boolean
- name: vehicle_type
in: query
required: false
description: Vehicle type selection override (nullable non-negative integer)
schema:
type: integer
minimum: 0
responses:
'200':
description: Dynamic image rendered successfully
content:
image/png:
schema:
type: string
format: binary
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
/guest/validation/customer-number:
post:
tags:
@@ -3,8 +3,11 @@
namespace routes;
use classes\authentication;
use classes\response;
use dynamicimages\images\machine_1;
use objects\categories_o;
use objects\department_lanes_o;
use objects\department_selfserve_tasks_o;
use objects\logs_o;
use traits\route_t;
@@ -75,6 +78,127 @@ class departmentLanesRoute
]
);
/**
* Generate dynamic image for a department lane (machine UI)
*
* Query parameters:
* - department (int, required)
* - lane (int, required)
* - buttons (array|json|csv, optional) → highlighted button IDs (0-indexed)
* - current_step (int >= 0, optional) → current click/step indicator
* - only_current_step (bool/int, optional) → if true, only draw current step highlight
* - vehicle_type (int|null, optional) → normalized but currently not used by machine_1
*
* Response: image/png
*/
$this->get('/department/lanes/dynamic-image', function () {
global $response;
// Reuse listing permission; viewing image is tied to lane visibility
$this->requirePermission('list_department_lanes');
// Authenticated user and department access validation
$user = (new authentication())->get_user();
if (!$user) {
$response->error('Invalid session', 400);
}
// Required params
self::requireParameters(['department', 'lane']);
$department_id = (int) self::getParameter('department');
$lane_id = (int) self::getParameter('lane');
self::requireType($department_id, self::type_int());
self::requireType($lane_id, self::type_int());
self::requireMinValue($department_id, 1);
self::requireMinValue($lane_id, 1);
// Department access control (if group limits apply)
if (method_exists($user, 'getGroup') && $user->getGroup()) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (is_array($authorized_department_ids) && !in_array($department_id, $authorized_department_ids, true)) {
$response->error('You do not have access to this department', 403);
}
}
// Load lane and verify it belongs to department
$lane = (new department_lanes_o())->select($lane_id);
if (!$lane->exists()) {
$response->error('Department lane not found', 404);
}
if ((int)$lane->department->value() !== $department_id) {
$response->error('Lane does not belong to the specified department', 400);
}
// Resolve dynamic image id → class (support id=1 for now)
$dynamic_image_id = $lane->dynamic_image_id->value();
if ($dynamic_image_id === null) {
$response->error('No dynamic image configured for this lane', 404);
}
$dynamic_image_id = (int)$dynamic_image_id;
// Parse optional params
$buttons = null;
if ($response->isRequestParameterSet('buttons')) {
$buttons = $response->getRequestParameter('buttons');
try {
$buttons = department_selfserve_tasks_o::normalizeButtonsInput($buttons);
} catch (\Exception $e) {
$response->error('Invalid buttons parameter: ' . $e->getMessage(), 400);
}
}
$current_step = 0;
if ($response->isRequestParameterSet('current_step')) {
$cs = (int) $response->getRequestParameter('current_step');
self::requireMinValue($cs, 0);
$current_step = $cs;
}
$only_current_step = false;
if ($response->isRequestParameterSet('only_current_step')) {
$val = $response->getRequestParameter('only_current_step');
// Accept true/false, 1/0, '1'/'0'
$only_current_step = in_array(strtolower((string)$val), ['1', 'true', 'yes', 'on'], true);
}
// Vehicle type (normalized; may be unused by concrete image class at present)
$vehicle_type = null;
if ($response->isRequestParameterSet('vehicle_type')) {
try {
$vehicle_type = department_selfserve_tasks_o::normalizeVehicleTypeInput($response->getRequestParameter('vehicle_type'));
} catch (\Exception $e) {
$response->error('Invalid vehicle_type parameter: ' . $e->getMessage(), 400);
}
}
// Instantiate and configure the image class based on dynamic_image_id
switch ($dynamic_image_id) {
case 1:
$image = new machine_1();
break;
default:
$response->error('Unsupported dynamic image id: ' . $dynamic_image_id, 400);
}
// Apply parameters
if (is_array($buttons)) {
$image->highlighted_buttons = $buttons;
}
$image->current_step = $current_step;
$image->only_generate_current_step = (bool)$only_current_step;
// Note: $vehicle_type currently not used by machine_1; reserved for future images.
// Compose and serve the image
try {
$image->setup();
$image->servePicture('png');
exit; // Ensure no extra output is appended
} catch (\Exception $e) {
$response->error('Failed to generate image: ' . $e->getMessage(), 500);
}
}, [
'list_department_lanes' => 'View dynamic image for a department lane',
]);
$this->post('/department/lanes', function () {
// Require the user to be logged in
@@ -0,0 +1,63 @@
<?php
// Lightweight, env-agnostic test for dynamic images construction
// This test avoids requiring full app bootstrap and Imagick composition by
// leveraging exportAsBase64() fallback when no canvas is initialized.
// Minimal stubs to satisfy dependencies when running without full bootstrap
namespace classes { class db {} class object_property { public function __construct(...$a){} public function value(){return null;} public function set($v){} } }
namespace traits { trait db_object_t {} }
namespace {
if (!defined('WD')) { define('WD', dirname(__DIR__, 2)); }
require_once WD . '/modules/dynamicimages/traits/dynamicimages_asset_t.php';
require_once WD . '/modules/dynamicimages/interfaces/dynamicimages_asset_i.php';
require_once WD . '/modules/dynamicimages/classes/dynamicimages_asset.php';
require_once WD . '/modules/dynamicimages/interfaces/dynamicimages_image_i.php';
require_once WD . '/modules/dynamicimages/traits/dynamicimages_image_t.php';
require_once WD . '/modules/dynamicimages/classes/dynamicimages_image.php';
require_once WD . '/modules/dynamicimages/images/machine_1.php';
require_once WD . '/objects/department_selfserve_tasks_o.php';
use dynamicimages\images\machine_1;
use objects\department_selfserve_tasks_o;
function ok($m){ echo "\n\033[32m✔ $m\033[0m\n"; }
function fail($m){ echo "\n\033[31m✖ $m\033[0m\n"; }
echo "\nDepartmentLanesImageTest starting...\n";
// 1) Buttons normalization examples
try {
$arr = department_selfserve_tasks_o::normalizeButtonsInput('0, 2,3 , 5');
if ($arr === [0,2,3,5]) { ok('CSV buttons normalization works'); } else { fail('CSV normalization mismatch: '.json_encode($arr)); }
} catch (\Exception $e) { fail('CSV normalization threw: '.$e->getMessage()); }
try {
$arr = department_selfserve_tasks_o::normalizeButtonsInput('[1, 1, "2", 4]');
if ($arr === [1,2,4]) { ok('JSON buttons normalization with de-dup works'); } else { fail('JSON normalization mismatch: '.json_encode($arr)); }
} catch (\Exception $e) { fail('JSON normalization threw: '.$e->getMessage()); }
// 2) Vehicle type normalization examples
try { $v = department_selfserve_tasks_o::normalizeVehicleTypeInput('3'); if ($v===3) ok('Vehicle type numeric string ok'); else fail('Vehicle type mismatch'); } catch (\Exception $e) { fail($e->getMessage()); }
try { $v = department_selfserve_tasks_o::normalizeVehicleTypeInput(null); if ($v===null) ok('Vehicle type null ok'); else fail('Vehicle type null mismatch'); } catch (\Exception $e) { fail($e->getMessage()); }
// 3) Sanity: instantiate machine_1 and export base64 from first asset (no canvas)
try {
$img = new machine_1();
// Set some sample parameters (not used until setup(), which we skip to avoid Imagick requirement)
$img->highlighted_buttons = [0,2,5];
$img->current_step = 1;
$dataUri = $img->exportAsBase64();
if (is_string($dataUri) && str_starts_with($dataUri, 'data:image/')) {
ok('machine_1 exportAsBase64 fallback returns a data URI');
} else {
fail('machine_1 exportAsBase64 returned unexpected value');
}
} catch (\Throwable $e) {
fail('machine_1 instantiation/export threw: '.$e->getMessage());
}
echo "\nDepartmentLanesImageTest completed.\n";
}