diff --git a/composer.json b/composer.json index 6930c8b20..f3321ec0a 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "drupal/hdbt_admin": "^3.0", "drupal/helfi_azure_fs": "^2.0", "drupal/helfi_drupal_tools": "dev-main", + "drupal/helfi_hakuvahti": "^1.0", "drupal/helfi_navigation": "^2.0", "drupal/helfi_platform_config": "^5.0", "drupal/helfi_proxy": "^3.0", diff --git a/composer.lock b/composer.lock index d0c5c5775..bfcedae5c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f910f52c9b4f4e0f916c8274235f7df3", + "content-hash": "f6bcc49cd08b6ba11e872af7ce818e9d", "packages": [ { "name": "asm89/stack-cors", @@ -4330,16 +4330,16 @@ }, { "name": "drupal/helfi_api_base", - "version": "2.8.7", + "version": "2.8.9", "source": { "type": "git", "url": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base.git", - "reference": "23ceff793eb0b7ef283bb5a13340bd5cf58ec9cf" + "reference": "ae4d8c126cc42fd4cac3ce7ad9ac6eeea154b248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-api-base/zipball/23ceff793eb0b7ef283bb5a13340bd5cf58ec9cf", - "reference": "23ceff793eb0b7ef283bb5a13340bd5cf58ec9cf", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-api-base/zipball/ae4d8c126cc42fd4cac3ce7ad9ac6eeea154b248", + "reference": "ae4d8c126cc42fd4cac3ce7ad9ac6eeea154b248", "shasum": "" }, "require": { @@ -4350,12 +4350,12 @@ "drupal/monolog": "^3.0", "drupal/raven": "^5.0 || ^6.0", "ext-curl": "*", - "firebase/php-jwt": "^6.5", + "firebase/php-jwt": "^7.0", "php": "^8.1", + "phrity/websocket": "^3.6", "symfony/polyfill-php84": "^1.0", "symfony/polyfill-php85": "^1.0", "t4web/composer-lock-parser": "^1.0", - "textalk/websocket": "^1.6", "webmozart/assert": "^1.0" }, "conflict": { @@ -4381,10 +4381,10 @@ ], "description": "Helfi - API Base", "support": { - "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base/tree/2.8.7", + "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base/tree/2.8.9", "issues": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base/issues" }, - "time": "2026-02-13T11:09:08+00:00" + "time": "2026-02-19T05:22:34+00:00" }, { "name": "drupal/helfi_azure_fs", @@ -4517,6 +4517,38 @@ }, "time": "2025-12-08T13:55:35+00:00" }, + { + "name": "drupal/helfi_hakuvahti", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/City-of-Helsinki/drupal-module-helfi-hakuvahti.git", + "reference": "d04ffa2b098173315742b3ba2a44f448ad745d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-hakuvahti/zipball/d04ffa2b098173315742b3ba2a44f448ad745d88", + "reference": "d04ffa2b098173315742b3ba2a44f448ad745d88", + "shasum": "" + }, + "require": { + "drupal/helfi_api_base": "*" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "drupal/coder": "^8.3" + }, + "type": "drupal-module", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Helfi - Hakuvahti", + "support": { + "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-hakuvahti/tree/1.1.0", + "issues": "https://github.com/City-of-Helsinki/drupal-module-helfi-hakuvahti/issues" + }, + "time": "2026-02-16T15:01:04+00:00" + }, { "name": "drupal/helfi_navigation", "version": "2.3.1", @@ -8170,16 +8202,16 @@ }, { "name": "firebase/php-jwt", - "version": "v6.11.1", + "version": "v7.0.2", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", - "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", "shasum": "" }, "require": { @@ -8227,9 +8259,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + "source": "https://github.com/firebase/php-jwt/tree/v7.0.2" }, - "time": "2025-04-09T20:32:01+00:00" + "time": "2025-12-16T22:17:28+00:00" }, { "name": "galbar/jsonpath", @@ -10608,29 +10640,211 @@ }, "time": "2021-09-22T16:57:06+00:00" }, + { + "name": "phrity/comparison", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-comparison.git", + "reference": "cf80abb822537eeaaeb4142157cd667ca6372a13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-comparison/zipball/cf80abb822537eeaaeb4142157cd667ca6372a13", + "reference": "cf80abb822537eeaaeb4142157cd667ca6372a13", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Comparison\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.", + "homepage": "https://phrity.sirn.se/comparison", + "keywords": [ + "comparable", + "comparator", + "comparison", + "equalable", + "filter", + "sort" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-comparison/issues", + "source": "https://github.com/sirn-se/phrity-comparison/tree/1.4.1" + }, + "time": "2025-12-05T07:38:30+00:00" + }, + { + "name": "phrity/http", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-http.git", + "reference": "1e7eee67359287b94aae2b7d40b730d5f5394943" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-http/zipball/1e7eee67359287b94aae2b7d40b730d5f5394943", + "reference": "1e7eee67359287b94aae2b7d40b730d5f5394943", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Utilities and interfaces for handling HTTP.", + "homepage": "https://phrity.sirn.se/http", + "keywords": [ + "HTTP Factories", + "HTTP Serializer", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-http/issues", + "source": "https://github.com/sirn-se/phrity-http/tree/1.1.0" + }, + "time": "2025-12-22T20:22:29+00:00" + }, + { + "name": "phrity/net-stream", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-stream.git", + "reference": "f46694e1b721867ec3c19731a7fcbbead3c6ac89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/f46694e1b721867ec3c19731a7fcbbead3c6ac89", + "reference": "f46694e1b721867ec3c19731a7fcbbead3c6ac89", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/util-errorhandler": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phrity/net-uri": "^2.0", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory", + "homepage": "https://phrity.sirn.se/net-stream", + "keywords": [ + "Socket", + "client", + "psr-17", + "psr-7", + "server", + "stream", + "stream factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-stream/issues", + "source": "https://github.com/sirn-se/phrity-net-stream/tree/2.3.3" + }, + "time": "2025-12-24T12:07:07+00:00" + }, { "name": "phrity/net-uri", - "version": "1.3.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/sirn-se/phrity-net-uri.git", - "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43" + "reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43", - "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/0737de026b75177ae302ac9fdbbd0ffc2610f3b8", + "reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8", "shasum": "" }, "require": { - "php": "^7.4 | ^8.0", + "ext-mbstring": "*", + "php": "^8.1", + "phrity/comparison": "^1.0", "psr/http-factory": "^1.0", - "psr/http-message": "^1.0 | ^2.0" + "psr/http-message": "^1.1 | ^2.0" }, "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpunit/phpunit": "^9.0 | ^10.0", - "squizlabs/php_codesniffer": "^3.0" + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phrity/util-errorhandler": "^1.1", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "suggest": { + "ext-intl": "Enables IDN conversion for non-ASCII domains" }, "type": "library", "autoload": { @@ -10659,9 +10873,9 @@ ], "support": { "issues": "https://github.com/sirn-se/phrity-net-uri/issues", - "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0" + "source": "https://github.com/sirn-se/phrity-net-uri/tree/2.2.1" }, - "time": "2023-08-21T10:33:06+00:00" + "time": "2025-12-05T10:39:22+00:00" }, { "name": "phrity/util-errorhandler", @@ -10715,6 +10929,71 @@ }, "time": "2025-12-05T21:25:36+00:00" }, + { + "name": "phrity/websocket", + "version": "3.6.2", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/websocket-php.git", + "reference": "b9816ed2b4a10c8c42bd0b6398044ab506869756" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/b9816ed2b4a10c8c42bd0b6398044ab506869756", + "reference": "b9816ed2b4a10c8c42bd0b6398044ab506869756", + "shasum": "" + }, + "require": { + "php": "^8.1", + "phrity/http": "^1.0", + "phrity/net-stream": "^2.3", + "phrity/net-uri": "^2.1", + "psr/http-message": "^1.1 | ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "phrity/logger-console": "^1.0", + "phrity/net-mock": "^2.3", + "phrity/util-errorhandler": "^1.1", + "robiningelbrecht/phpunit-coverage-tools": "^1.9", + "squizlabs/php_codesniffer": "^3.5 || ^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "WebSocket client and server", + "homepage": "https://phrity.sirn.se/websocket", + "keywords": [ + "client", + "server", + "websocket" + ], + "support": { + "issues": "https://github.com/sirn-se/websocket-php/issues", + "source": "https://github.com/sirn-se/websocket-php/tree/3.6.2" + }, + "time": "2025-12-21T09:58:16+00:00" + }, { "name": "proj4php/proj4php", "version": "v2.0.19", @@ -14872,57 +15151,6 @@ }, "time": "2022-02-23T14:59:32+00:00" }, - { - "name": "textalk/websocket", - "version": "1.6.3", - "source": { - "type": "git", - "url": "https://github.com/Textalk/websocket-php.git", - "reference": "67de79745b1a357caf812bfc44e0abf481cee012" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/67de79745b1a357caf812bfc44e0abf481cee012", - "reference": "67de79745b1a357caf812bfc44e0abf481cee012", - "shasum": "" - }, - "require": { - "php": "^7.4 | ^8.0", - "phrity/net-uri": "^1.0", - "phrity/util-errorhandler": "^1.0", - "psr/http-message": "^1.0", - "psr/log": "^1.0 | ^2.0 | ^3.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpunit/phpunit": "^9.0", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "WebSocket\\": "lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Fredrik Liljegren" - }, - { - "name": "Sören Jensen" - } - ], - "description": "WebSocket client and server", - "support": { - "issues": "https://github.com/Textalk/websocket-php/issues", - "source": "https://github.com/Textalk/websocket-php/tree/1.6.3" - }, - "time": "2022-11-07T18:59:33+00:00" - }, { "name": "twig/twig", "version": "v3.23.0", @@ -18248,16 +18476,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.53", + "version": "11.5.55", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607" + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a997a653a82845f1240d73ee73a8a4e97e4b0607", - "reference": "a997a653a82845f1240d73ee73a8a4e97e4b0607", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", "shasum": "" }, "require": { @@ -18330,7 +18558,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.53" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" }, "funding": [ { @@ -18354,7 +18582,7 @@ "type": "tidelift" } ], - "time": "2026-02-10T12:28:25+00:00" + "time": "2026-02-18T12:37:06+00:00" }, { "name": "ramsey/collection", diff --git a/conf/cmi/block.block.hdbt_subtheme_hakuvahti.yml b/conf/cmi/block.block.hdbt_subtheme_hakuvahti.yml new file mode 100644 index 000000000..8d00722c7 --- /dev/null +++ b/conf/cmi/block.block.hdbt_subtheme_hakuvahti.yml @@ -0,0 +1,25 @@ +uuid: f73e721d-5865-48b3-a24a-051fa2f42961 +langcode: en +status: true +dependencies: + module: + - helfi_kymp_content + - system + theme: + - hdbt_subtheme +id: hdbt_subtheme_hakuvahti +theme: hdbt_subtheme +region: content +weight: 5 +provider: null +plugin: kymp_vehicle_removal +settings: + id: kymp_vehicle_removal + label: Hakuvahti + label_display: '0' + provider: helfi_kymp_content +visibility: + request_path: + id: request_path + negate: false + pages: "/pysakointi/ajoneuvojen-siirrot/ajoneuvojen-siirtokehotukset\r\n/parking/vehicle-removal/vehicle-removal-requests\r\n/parkering/forflyttning-av-fordon/flyttningsuppmaningar" diff --git a/conf/cmi/core.extension.yml b/conf/cmi/core.extension.yml index 88ee25252..605139f7f 100644 --- a/conf/cmi/core.extension.yml +++ b/conf/cmi/core.extension.yml @@ -51,6 +51,7 @@ module: helfi_ckeditor: 0 helfi_csp: 0 helfi_etusivu_entities: 0 + helfi_hakuvahti: 0 helfi_hyte_search: 0 helfi_image_styles: 0 helfi_kymp_content: 0 diff --git a/conf/cmi/helfi_hakuvahti.config.default.yml b/conf/cmi/helfi_hakuvahti.config.default.yml new file mode 100644 index 000000000..ced5d4b45 --- /dev/null +++ b/conf/cmi/helfi_hakuvahti.config.default.yml @@ -0,0 +1,9 @@ +uuid: 10b0636e-5846-4ce6-a01f-ae4eaf8967be +langcode: en +status: true +dependencies: { } +_core: + default_config_hash: VOToVi_aKYE5IrQ7kOYaB0Ki5hOC4kh2f_KF5FJqKnY +id: default +label: Default +site_id: 'kymp' diff --git a/conf/cmi/helfi_hakuvahti.settings.yml b/conf/cmi/helfi_hakuvahti.settings.yml new file mode 100644 index 000000000..4c900f620 --- /dev/null +++ b/conf/cmi/helfi_hakuvahti.settings.yml @@ -0,0 +1 @@ +langcode: en diff --git a/conf/cmi/search_api.index.mobilenote_data.yml b/conf/cmi/search_api.index.mobilenote_data.yml index e83eee9da..1afdda509 100644 --- a/conf/cmi/search_api.index.mobilenote_data.yml +++ b/conf/cmi/search_api.index.mobilenote_data.yml @@ -95,7 +95,7 @@ tracker_settings: default: indexing_order: fifo options: - cron_limit: 20 + cron_limit: 1 delete_on_fail: true index_directly: false track_changes_in_references: true diff --git a/public/modules/custom/helfi_kymp_content/helfi_kymp_content.libraries.yml b/public/modules/custom/helfi_kymp_content/helfi_kymp_content.libraries.yml new file mode 100644 index 000000000..6963b6caa --- /dev/null +++ b/public/modules/custom/helfi_kymp_content/helfi_kymp_content.libraries.yml @@ -0,0 +1,4 @@ +vehicle-removal-search: + version: HELFI_DEPLOYMENT_IDENTIFIER + js: {} + # Theme will extend this library. diff --git a/public/modules/custom/helfi_kymp_content/helfi_kymp_content.module b/public/modules/custom/helfi_kymp_content/helfi_kymp_content.module index be77a2558..0801ad846 100644 --- a/public/modules/custom/helfi_kymp_content/helfi_kymp_content.module +++ b/public/modules/custom/helfi_kymp_content/helfi_kymp_content.module @@ -18,8 +18,11 @@ use Drupal\node\NodeInterface; /** * Implements hook_theme(). */ -function helfi_kymp_content_theme() { +function helfi_kymp_content_theme(): array { return [ + 'vehicle_removal' => [ + 'variables' => [], + ], 'subdistricts_navigation' => [ 'variables' => [ 'navigation' => NULL, diff --git a/public/modules/custom/helfi_kymp_content/helfi_kymp_content.services.yml b/public/modules/custom/helfi_kymp_content/helfi_kymp_content.services.yml index 6150d5842..990c84f0a 100644 --- a/public/modules/custom/helfi_kymp_content/helfi_kymp_content.services.yml +++ b/public/modules/custom/helfi_kymp_content/helfi_kymp_content.services.yml @@ -16,5 +16,6 @@ services: Drupal\helfi_kymp_content\StreetDataService: ~ + Drupal\helfi_kymp_content\Paikkatieto\PaikkatietoClient: ~ Drupal\helfi_kymp_content\MobileNoteDataService: ~ diff --git a/public/modules/custom/helfi_kymp_content/src/MobileNoteDataService.php b/public/modules/custom/helfi_kymp_content/src/MobileNoteDataService.php index 2d21567c4..93b7556c3 100644 --- a/public/modules/custom/helfi_kymp_content/src/MobileNoteDataService.php +++ b/public/modules/custom/helfi_kymp_content/src/MobileNoteDataService.php @@ -7,6 +7,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Site\Settings; use Drupal\Core\TypedData\TypedDataManagerInterface; +use Drupal\helfi_kymp_content\Paikkatieto\PaikkatietoClient; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; use proj4php\Point; @@ -35,24 +36,17 @@ class MobileNoteDataService { */ protected Proj $projTarget; - public const METHOD_BBOX = 'BBOX'; - public const METHOD_POINT = 'POINT'; - - /** - * The street query mode to use. - */ - private const STREET_QUERY_MODE = self::METHOD_POINT; - /** * Constructs a new MobileNoteDataService instance. */ public function __construct( - protected readonly ClientInterface $client, - protected readonly TypedDataManagerInterface $typedDataManager, - protected readonly TimeInterface $time, - protected readonly Settings $settings, + protected ClientInterface $client, + protected TypedDataManagerInterface $typedDataManager, + protected TimeInterface $time, + protected Settings $settings, #[Autowire(service: 'logger.channel.helfi_kymp_content')] - protected readonly LoggerInterface $logger, + protected LoggerInterface $logger, + protected PaikkatietoClient $paikkatietoClient, ) { // EPSG:3879 is Helsinki local CRS (ETRS-GK25FIN). $this->proj4 = new Proj4php(); @@ -123,26 +117,29 @@ public function getMobileNoteData(): array { * The items to fetch street names for. */ public function fetchNearbyStreets(array $items): void { - $apiSettings = $this->settings->get('helfi_kymp_mobilenote', []); - $apiKey = $apiSettings['address_api_key'] ?? NULL; - - if (empty($apiKey)) { - $this->logger->warning('Paikkatietoapi: Missing API key.'); - return; - } - foreach ($items as $item) { $geo = $item->get('geometry')->getValue(); - if ($geo) { - if (self::STREET_QUERY_MODE === self::METHOD_POINT) { - $result = $this->fetchStreetsByPoint($geo, $apiKey); - } - else { - $result = $this->fetchStreetsByBbox($geo, $apiKey); - } - $item->set('street_names', $result); + if (!$geo) { + continue; } + + if (empty($geo->coordinates)) { + continue; + } + + if (!isset($geo->type) || $geo->type !== 'linestring') { + $this->logger->warning('Skipping item with unknown geometry type @type.', [ + '@type' => $geo->type ?? '', + ]); + } + + // This can fail with an exception when the API is + // unable to handle too many requests. If that happens, + // the processing should fail and be re-tried automatically. + $result = $this->paikkatietoClient->fetchStreetsForLineString($geo->coordinates); + + $item->set('street_names', $result); } } @@ -303,111 +300,4 @@ protected function convertGeometry(array $geometry): object { ]; } - /** - * Fetches street names using the bounding box method. - * - * @param object $geometry - * The GeoJSON geometry object. - * @param string $apiKey - * The Address API key. - * - * @return array - * A list of unique street names found within the bounding box. - */ - protected function fetchStreetsByBbox(object $geometry, string $apiKey): array { - if (empty($geometry->coordinates)) { - return []; - } - - // Calculate Bounding Box (minX, minY, maxX, maxY). - $lons = array_column($geometry->coordinates, 0); - $lats = array_column($geometry->coordinates, 1); - - // Add buffer (approx 20m = 0.0002 deg) to ensure results for lines. - $buffer = 0.0002; - $minX = min($lons) - $buffer; - $maxX = max($lons) + $buffer; - $minY = min($lats) - $buffer; - $maxY = max($lats) + $buffer; - - return $this->fetchStreetsFromApi([ - 'bbox' => implode(',', [$minX, $minY, $maxX, $maxY]), - 'limit' => 20, - ], $apiKey); - } - - /** - * Fetches street names using the point-radius method. - * - * @param object $geometry - * The GeoJSON geometry object. - * @param string $apiKey - * The Address API key. - * - * @return array - * A list of unique street names found within radius. - */ - protected function fetchStreetsByPoint(object $geometry, string $apiKey): array { - if (empty($geometry->coordinates)) { - return []; - } - - // Calculate Centroid. - $lons = array_column($geometry->coordinates, 0); - $lats = array_column($geometry->coordinates, 1); - $count = count($geometry->coordinates); - - if ($count === 0) { - return []; - } - - return $this->fetchStreetsFromApi([ - 'lat' => array_sum($lats) / $count, - 'lon' => array_sum($lons) / $count, - 'distance' => 20, - 'limit' => 20, - ], $apiKey); - } - - /** - * Fetches street names from the Paikkatietohaku API. - * - * @param array $queryParams - * Query parameters for the API request. - * @param string $apiKey - * The Address API key. - * - * @return array - * A list of unique street names. - */ - protected function fetchStreetsFromApi(array $queryParams, string $apiKey): array { - try { - $response = $this->client->request('GET', 'https://paikkatietohaku.api.hel.fi/v1/address/', [ - 'headers' => ['Api-Key' => $apiKey], - 'query' => $queryParams, - 'timeout' => 60, - ]); - - $data = json_decode($response->getBody()->getContents(), TRUE); - $streets = []; - - foreach ($data['results'] ?? [] as $result) { - if (!empty($result['street']['name']['fi'])) { - $streets[] = $result['street']['name']['fi']; - } - if (!empty($result['street']['name']['sv'])) { - $streets[] = $result['street']['name']['sv']; - } - } - - return array_values(array_unique($streets)); - } - catch (\Exception $e) { - $this->logger->error('Paikkatietoapi failed: @message', [ - '@message' => $e->getMessage(), - ]); - return []; - } - } - } diff --git a/public/modules/custom/helfi_kymp_content/src/Paikkatieto/Exception.php b/public/modules/custom/helfi_kymp_content/src/Paikkatieto/Exception.php new file mode 100644 index 000000000..13f3ac1bb --- /dev/null +++ b/public/modules/custom/helfi_kymp_content/src/Paikkatieto/Exception.php @@ -0,0 +1,11 @@ +fetchStreetsByPoint($midLat, $midLon, $distance); + array_push($streets, ...$segmentStreets); + } + + return array_values(array_unique($streets)); + } + + /** + * Fetches street names using the point-radius method. + * + * @param float $lat + * Latitude. + * @param float $lon + * Longitude. + * @param int $distance + * Distance. We look at street names within this + * distance and pick the closest result. + * + * @return array + * A list of unique street names found within radius. + * + * @throws \Drupal\helfi_kymp_content\Paikkatieto\Exception + * @throws \InvalidArgumentException + */ + public function fetchStreetsByPoint(float $lat, float $lon, int $distance = 75): array { + $results = $this->makeRequest([ + 'lat' => $lat, + 'lon' => $lon, + 'distance' => $distance, + 'limit' => 20, + ]); + + if (empty($results)) { + return []; + } + + // Find the closest result by haversine distance. + $closest = NULL; + $minDistance = PHP_FLOAT_MAX; + + foreach ($results as $result) { + $coords = $result->location->coordinates ?? NULL; + if (!$coords) { + continue; + } + // API returns [lon, lat] (GeoJSON order). + $d = self::haversineDistance($lat, $lon, $coords[1], $coords[0]); + if ($d < $minDistance) { + $minDistance = $d; + $closest = $result; + } + } + + if (!$closest) { + return []; + } + + $streets = []; + foreach (['fi', 'sv'] as $langcode) { + if (!empty($closest->street->name->{$langcode})) { + $streets[] = $closest->street->name->{$langcode}; + } + } + + return $streets; + } + + /** + * Fetches results from the Paikkatietohaku API. + * + * @param array $queryParams + * Query parameters for the API request. + * @param int $maxRetries + * Maximum number of retries on 502 errors. + * + * @return array + * The results array from the API response. + * + * @throws \Drupal\helfi_kymp_content\Paikkatieto\Exception + * @throws \InvalidArgumentException + */ + private function makeRequest(array $queryParams, int $maxRetries = 3): array { + $apiSettings = $this->settings->get('helfi_kymp_mobilenote', []); + $apiKey = $apiSettings['address_api_key'] ?? NULL; + + if (empty($apiKey)) { + throw new \InvalidArgumentException('Paikkatietoapi: Missing API key.'); + } + + $lastException = NULL; + + for ($attempt = 0; $attempt <= $maxRetries; $attempt++) { + if ($attempt > 0) { + // Exponential backoff: 1s, 2s, 4s. + sleep(2 ** ($attempt - 1)); + } + + try { + $response = $this->httpClient->request('GET', 'https://paikkatietohaku.api.hel.fi/v1/address/', [ + 'headers' => ['Api-Key' => $apiKey], + 'query' => $queryParams, + 'timeout' => 60, + ]); + + $data = Utils::jsonDecode($response->getBody()->getContents()); + + return $data->results ?? []; + } + catch (GuzzleException $e) { + $lastException = $e; + $is502 = $e instanceof RequestException && $e->getResponse()?->getStatusCode() === 502; + + // Only retry on 502 (rate limiting). + if (!$is502) { + throw new Exception($e->getMessage(), previous: $e); + } + + $this->logger?->info('Paikkatietoapi returned 502, retry @attempt of @max.', [ + '@attempt' => $attempt + 1, + '@max' => $maxRetries, + ]); + } + } + + throw new Exception($lastException->getMessage(), previous: $lastException); + } + + /** + * Calculate the distance between two coordinates using the Haversine formula. + */ + private static function haversineDistance(float $lat1, float $lon1, float $lat2, float $lon2): float { + $lat1 = deg2rad($lat1); + $lon1 = deg2rad($lon1); + $lat2 = deg2rad($lat2); + $lon2 = deg2rad($lon2); + + $deltaLat = $lat2 - $lat1; + $deltaLon = $lon2 - $lon1; + + $a = sin($deltaLat / 2) ** 2 + + cos($lat1) * cos($lat2) * sin($deltaLon / 2) ** 2; + + $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); + + return self::EARTH_RADIUS * $c; + } + +} diff --git a/public/modules/custom/helfi_kymp_content/src/Plugin/Block/SubdistrictsNavigationBlock.php b/public/modules/custom/helfi_kymp_content/src/Plugin/Block/SubdistrictsNavigationBlock.php index cb88db70f..f40d170a4 100644 --- a/public/modules/custom/helfi_kymp_content/src/Plugin/Block/SubdistrictsNavigationBlock.php +++ b/public/modules/custom/helfi_kymp_content/src/Plugin/Block/SubdistrictsNavigationBlock.php @@ -4,6 +4,7 @@ namespace Drupal\helfi_kymp_content\Plugin\Block; +use Drupal\Core\Block\Attribute\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -13,6 +14,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Template\Attribute; use Drupal\helfi_kymp_content\DistrictUtility; use Drupal\node\NodeInterface; @@ -22,12 +24,11 @@ /** * Provides a 'SubdistrictsNavigationBlock' block. - * - * @Block( - * id = "subdistricts_navigation", - * admin_label = @Translation("Subdistricts navigation"), - * ) */ +#[Block( + id: 'subdistricts_navigation', + admin_label: new TranslatableMarkup('Subdistricts navigation'), +)] final class SubdistrictsNavigationBlock extends BlockBase implements ContainerFactoryPluginInterface { /** diff --git a/public/modules/custom/helfi_kymp_content/src/Plugin/Block/VehicleRemovalBlock.php b/public/modules/custom/helfi_kymp_content/src/Plugin/Block/VehicleRemovalBlock.php new file mode 100644 index 000000000..f0b152599 --- /dev/null +++ b/public/modules/custom/helfi_kymp_content/src/Plugin/Block/VehicleRemovalBlock.php @@ -0,0 +1,69 @@ +configFactory->get('elastic_proxy.settings'); + $reactSettings = $this->configFactory->get('react_search.settings'); + + $cache = new CacheableMetadata(); + $cache->addCacheableDependency($proxySettings); + $cache->addCacheableDependency($reactSettings); + + $build = [ + '#theme' => 'vehicle_removal', + '#attached' => [ + 'drupalSettings' => [ + 'helfi_react_search' => [ + 'elastic_proxy_url' => $proxySettings->get('elastic_proxy_url'), + 'sentry_dsn_react' => $reactSettings->get('sentry_dsn_react'), + ], + ], + 'library' => [ + 'helfi_kymp_content/vehicle-removal-search', + ], + ], + ]; + + // Apply hakuvahti settings. + $this->drupalSettings->applyTo($build); + + // Cache tags. + $cache->applyTo($build); + + return $build; + } + +} diff --git a/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/HelfiStreetDataSource.php b/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/HelfiStreetDataSource.php index 2d19893c3..65e37fb52 100644 --- a/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/HelfiStreetDataSource.php +++ b/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/HelfiStreetDataSource.php @@ -5,22 +5,23 @@ namespace Drupal\helfi_kymp_content\Plugin\search_api\datasource; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\helfi_kymp_content\Plugin\DataType\StreetData; use Drupal\helfi_kymp_content\StreetDataService; +use Drupal\search_api\Attribute\SearchApiDatasource; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Datasource\DatasourcePluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a datasource for kartta.hel.fi. - * - * @SearchApiDatasource( - * id = "helfi_street_data_source", - * label = @Translation("Helfi street datasource"), - * description = @Translation("Datasource for street data from kartta.hel.fi."), - * ) */ +#[SearchApiDatasource( + id: 'helfi_street_data_source', + label: new TranslatableMarkup('Helfi street datasource'), + description: new TranslatableMarkup('Datasource for street data from kartta.hel.fi.'), +)] class HelfiStreetDataSource extends DatasourcePluginBase implements DatasourceInterface { /** @@ -37,6 +38,18 @@ public static function create(ContainerInterface $container, array $configuratio return $instance; } + /** + * {@inheritdoc} + */ + public function getItemIds($page = NULL) { + // No pagination. + if ($page && $page > 0) { + return NULL; + } + + return array_keys($this->loadMultiple([])); + } + /** * {@inheritdoc} */ diff --git a/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/MobileNoteDataSource.php b/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/MobileNoteDataSource.php index 43341bd57..ae8e8f441 100644 --- a/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/MobileNoteDataSource.php +++ b/public/modules/custom/helfi_kymp_content/src/Plugin/search_api/datasource/MobileNoteDataSource.php @@ -27,38 +27,19 @@ final class MobileNoteDataSource extends DatasourcePluginBase implements Datasou /** * The MobileNote data service. */ - - /** - * Constructs a new MobileNoteDataSource instance. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\helfi_kymp_content\MobileNoteDataService $dataService - * The MobileNote data service. - */ - public function __construct( - array $configuration, - $plugin_id, - $plugin_definition, - protected MobileNoteDataService $dataService, - ) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - } + protected MobileNoteDataService $dataService; /** * {@inheritDoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( + $instance = new static( $configuration, $plugin_id, $plugin_definition, - $container->get(MobileNoteDataService::class) ); + $instance->dataService = $container->get(MobileNoteDataService::class); + return $instance; } /** diff --git a/public/modules/custom/helfi_kymp_content/templates/vehicle-removal.html.twig b/public/modules/custom/helfi_kymp_content/templates/vehicle-removal.html.twig new file mode 100644 index 000000000..f054f8a88 --- /dev/null +++ b/public/modules/custom/helfi_kymp_content/templates/vehicle-removal.html.twig @@ -0,0 +1,4 @@ +