Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.83% covered (success)
97.83%
45 / 46
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SmartMapLoadBalancer
97.78% covered (success)
97.78%
44 / 45
87.50% covered (warning)
87.50%
7 / 8
31
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 addProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProviders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProviderForCoordinates
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
6.10
 geocode
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 getFastestProvider
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 exportTileTemplates
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Map\Infrastructure\Provider;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Map\Domain\Model\GeoPoint;
12use App\Modules\Map\Domain\Repository\MapProviderInterface;
13
14/**
15 * Smart Load Balancer and Regional Router evaluating provider latencies and geographical bounds.
16 *
17 * @package App\Modules\Map\Infrastructure\Provider
18 */
19final class SmartMapLoadBalancer
20{
21    /**
22     * @var array<string, MapProviderInterface>
23     */
24    private array $providers = [];
25
26    /**
27     * @param array<int, MapProviderInterface> $providers Initial provider list.
28     */
29    public function __construct(array $providers = [])
30    {
31        foreach ($providers as $provider) {
32            $this->addProvider($provider);
33        }
34    }
35
36    public function addProvider(MapProviderInterface $provider): void
37    {
38        $this->providers[$provider->getName()] = $provider;
39    }
40
41    /**
42     * @return array<string, MapProviderInterface>
43     */
44    public function getProviders(): array
45    {
46        return $this->providers;
47    }
48
49    public function getProvider(string $name): ?MapProviderInterface
50    {
51        return $this->providers[$name] ?? null;
52    }
53
54    /**
55     * Selects best provider for given geographic coordinate.
56     */
57    public function getProviderForCoordinates(float $latitude, float $longitude): MapProviderInterface
58    {
59        // 1. Check local Ammonly provider if within Poland bounds
60        $local = $this->getProvider('ammonly_local');
61        if ($local !== null && $local->isAvailable() && $local->supportsRegion($latitude, $longitude)) {
62            return $local;
63        }
64
65        // 2. Otherwise return MapTiler or fastest global provider
66        $maptiler = $this->getProvider('maptiler');
67        if ($maptiler !== null && $maptiler->isAvailable()) {
68            return $maptiler;
69        }
70
71        return $this->getFastestProvider();
72    }
73
74    /**
75     * Resolves geocoding query routing across regional and global providers.
76     *
77     * @param string      $query       Search query.
78     * @param int         $limit       Max results.
79     * @param string|null $countryCode ISO country code hint if known.
80     * @return array<int, GeoPoint>
81     */
82    public function geocode(string $query, int $limit = 5, ?string $countryCode = null): array
83    {
84        $isPoland = $countryCode === null || strtoupper(trim($countryCode)) === 'PL';
85        $local = $this->getProvider('ammonly_local');
86
87        if ($isPoland && $local !== null && $local->isAvailable()) {
88            $results = $local->geocode($query, $limit);
89            if ($results !== []) {
90                return $results;
91            }
92        }
93
94        $global = $this->getProvider('maptiler');
95        if ($global !== null && $global->isAvailable()) {
96            $results = $global->geocode($query, $limit);
97            if ($results !== []) {
98                return $results;
99            }
100        }
101
102        return [];
103    }
104
105    /**
106     * Returns provider with lowest measured latency.
107     */
108    public function getFastestProvider(): MapProviderInterface
109    {
110        $best = null;
111        $bestLatency = PHP_FLOAT_MAX;
112
113        foreach ($this->providers as $provider) {
114            if (!$provider->isAvailable()) {
115                continue;
116            }
117            $latency = $provider->getLatencyMs();
118            if ($latency < $bestLatency) {
119                $bestLatency = $latency;
120                $best = $provider;
121            }
122        }
123
124        return $best ?? ($this->providers['ammonly_local'] ?? reset($this->providers));
125    }
126
127    /**
128     * Exports all tile URL templates for frontend hybrid rendering and seamless fallback.
129     *
130     * @return array{
131     *     tile_url_template: string,
132     *     tile_dark_url_template: string,
133     *     tile_maptiler_url: string,
134     *     tile_dark_maptiler_url: string
135     * }
136     */
137    public function exportTileTemplates(): array
138    {
139        $local = $this->getProvider('ammonly_local');
140        $maptiler = $this->getProvider('maptiler');
141
142        $lightLocal = $local !== null ? $local->getTileUrlTemplate(false) : '';
143        $darkLocal = $local !== null ? $local->getTileUrlTemplate(true) : $lightLocal;
144
145        $lightMaptiler = $maptiler !== null ? $maptiler->getTileUrlTemplate(false) : '';
146        $darkMaptiler = $maptiler !== null ? $maptiler->getTileUrlTemplate(true) : $lightMaptiler;
147
148        return [
149            'tile_url_template'          => $lightLocal !== '' ? $lightLocal : $lightMaptiler,
150            'tile_dark_url_template'     => $darkLocal !== '' ? $darkLocal : $darkMaptiler,
151            'tile_maptiler_url'          => $lightMaptiler,
152            'tile_dark_maptiler_url'     => $darkMaptiler,
153        ];
154    }
155}