diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md index 1e1c7b381e7..f4b4accdb34 100644 --- a/src/Map/CHANGELOG.md +++ b/src/Map/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 2.28 + +- Add `minZoom` and `maxZoom` options to `Map` to set the minimum and maximum zoom levels + ## 2.27 - The `fitBoundsToMarkers` option is not overridden anymore when using the `Map` LiveComponent, but now respects the value you defined. diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index f4b24236857..e4d3f3fd989 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -30,6 +30,8 @@ export type Icon = { export type MapDefinition = { center: Point | null; zoom: number | null; + minZoom: number | null; + maxZoom: number | null; options: MapOptions; bridgeOptions?: BridgeMapOptions; extra: ExtraData; @@ -92,6 +94,8 @@ export default abstract class>; polygonsValue: Array>; @@ -113,6 +119,8 @@ export default abstract class; hasCenterValue: boolean; hasZoomValue: boolean; + hasMinZoomValue: boolean; + hasMaxZoomValue: boolean; hasFitBoundsToMarkersValue: boolean; hasMarkersValue: boolean; hasPolygonsValue: boolean; @@ -142,6 +150,8 @@ export default abstract class = { center: Point | null; zoom: number | null; + minZoom: number | null; + maxZoom: number | null; options: MapOptions; /** * Additional options passed to the Map constructor. @@ -221,6 +223,8 @@ export default abstract class< providerOptions: Object, center: Object, zoom: Number, + minZoom: Number, + maxZoom: Number, fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, @@ -233,6 +237,8 @@ export default abstract class< declare centerValue: Point | null; declare zoomValue: number | null; + declare minZoomValue: number | null; + declare maxZoomValue: number | null; declare fitBoundsToMarkersValue: boolean; declare markersValue: Array>; declare polygonsValue: Array>; @@ -244,6 +250,8 @@ export default abstract class< declare hasCenterValue: boolean; declare hasZoomValue: boolean; + declare hasMinZoomValue: boolean; + declare hasMaxZoomValue: boolean; declare hasFitBoundsToMarkersValue: boolean; declare hasMarkersValue: boolean; declare hasPolygonsValue: boolean; @@ -275,6 +283,8 @@ export default abstract class< const mapDefinition: MapDefinition = { center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, + minZoom: this.hasMinZoomValue ? this.minZoomValue : null, + maxZoom: this.hasMaxZoomValue ? this.maxZoomValue : null, options: this.optionsValue, extra, }; @@ -335,6 +345,10 @@ export default abstract class< public abstract zoomValueChanged(): void; + public abstract minZoomValueChanged(): void; + + public abstract maxZoomValueChanged(): void; + public markersValueChanged(): void { if (!this.isConnected) { return; diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst index 84d812b1638..55101196430 100644 --- a/src/Map/doc/index.rst +++ b/src/Map/doc/index.rst @@ -90,6 +90,30 @@ You can set the center and zoom of the map using the ``center()`` and ``zoom()`` ->fitBoundsToMarkers() ; +Min and max zooms +~~~~~~~~~~~~~~~~~ + +.. versionadded:: 2.28 + + The ability to set min and max zooms was added in UX Map 2.28. + +You can set the minimum and maximum zoom levels of the map using the ``minZoom()`` and ``maxZoom()`` methods:: + + use Symfony\UX\Map\Map; + use Symfony\UX\Map\Point; + + $map + ->center(new Point(46.903354, 1.888334)) + ->zoom(6) + ->minZoom(3) // Set the minimum zoom level + ->maxZoom(10) // Set the maximum zoom level + ; + +.. warning:: + + Ensure ``zoom``, ``minZoom`` and ``maxZoom`` are compatible with each other (``minZoom <= zoom <= maxZoom``), + otherwise an exception will be thrown. + Add markers ~~~~~~~~~~~ diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts index 185f4eb3787..b9996d6be72 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts @@ -8,6 +8,8 @@ export default class extends AbstractMapController; centerValueChanged(): void; zoomValueChanged(): void; + minZoomValueChanged(): void; + maxZoomValueChanged(): void; protected dispatchEvent(name: string, payload?: Record): void; protected doCreateMap({ definition }: { definition: MapDefinition; diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js index 7c4813a9fd2..825fc1f3076 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js @@ -22,6 +22,8 @@ class default_1 extends Controller { const mapDefinition = { center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, + minZoom: this.hasMinZoomValue ? this.minZoomValue : null, + maxZoom: this.hasMaxZoomValue ? this.maxZoomValue : null, options: this.optionsValue, extra, }; @@ -127,6 +129,8 @@ default_1.values = { providerOptions: Object, center: Object, zoom: Number, + minZoom: Number, + maxZoom: Number, fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, @@ -187,6 +191,16 @@ class map_controller extends default_1 { this.map.setZoom(this.zoomValue); } } + minZoomValueChanged() { + if (this.map && this.hasMinZoomValue && this.minZoomValue) { + this.map.setOptions({ minZoom: this.minZoomValue }); + } + } + maxZoomValueChanged() { + if (this.map && this.hasMaxZoomValue && this.maxZoomValue) { + this.map.setOptions({ maxZoom: this.maxZoomValue }); + } + } dispatchEvent(name, payload = {}) { payload.google = _google; this.dispatch(name, { @@ -195,7 +209,7 @@ class map_controller extends default_1 { }); } doCreateMap({ definition }) { - const { center, zoom, options, bridgeOptions = {} } = definition; + const { center, zoom, minZoom, maxZoom, options, bridgeOptions = {} } = definition; options.zoomControl = typeof options.zoomControlOptions !== 'undefined'; options.mapTypeControl = typeof options.mapTypeControlOptions !== 'undefined'; options.streetViewControl = typeof options.streetViewControlOptions !== 'undefined'; @@ -203,6 +217,8 @@ class map_controller extends default_1 { return new _google.maps.Map(this.element, { center, zoom, + minZoom, + maxZoom, ...options, ...bridgeOptions, }); diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts index 4f14961e908..61529fadc2a 100644 --- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts @@ -127,6 +127,18 @@ export default class extends AbstractMapController< } } + public minZoomValueChanged(): void { + if (this.map && this.hasMinZoomValue && this.minZoomValue) { + this.map.setOptions({ minZoom: this.minZoomValue }); + } + } + + public maxZoomValueChanged(): void { + if (this.map && this.hasMaxZoomValue && this.maxZoomValue) { + this.map.setOptions({ maxZoom: this.maxZoomValue }); + } + } + protected dispatchEvent(name: string, payload: Record = {}): void { payload.google = _google; this.dispatch(name, { @@ -136,7 +148,7 @@ export default class extends AbstractMapController< } protected doCreateMap({ definition }: { definition: MapDefinition }): google.maps.Map { - const { center, zoom, options, bridgeOptions = {} } = definition; + const { center, zoom, minZoom, maxZoom, options, bridgeOptions = {} } = definition; // We assume the following control options are enabled if their options are set options.zoomControl = typeof options.zoomControlOptions !== 'undefined'; @@ -147,6 +159,8 @@ export default class extends AbstractMapController< return new _google.maps.Map(this.element, { center, zoom, + minZoom, + maxZoom, ...options, ...bridgeOptions, }); diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts index a8a62f59255..4cb60349dda 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts @@ -26,6 +26,8 @@ export default class extends AbstractMapController): void; protected doCreateMap({ definition }: { definition: MapDefinition; diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js index 3e61359e80c..502438cba71 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -23,6 +23,8 @@ class default_1 extends Controller { const mapDefinition = { center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, + minZoom: this.hasMinZoomValue ? this.minZoomValue : null, + maxZoom: this.hasMaxZoomValue ? this.maxZoomValue : null, options: this.optionsValue, extra, }; @@ -128,6 +130,8 @@ default_1.values = { providerOptions: Object, center: Object, zoom: Number, + minZoom: Number, + maxZoom: Number, fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, @@ -159,6 +163,16 @@ class map_controller extends default_1 { this.map.setZoom(this.zoomValue); } } + minZoomValueChanged() { + if (this.map && this.hasMinZoomValue && this.minZoomValue) { + this.map.setMinZoom(this.minZoomValue); + } + } + maxZoomValueChanged() { + if (this.map && this.hasMaxZoomValue && this.maxZoomValue) { + this.map.setMaxZoom(this.maxZoomValue); + } + } dispatchEvent(name, payload = {}) { payload.L = L; this.dispatch(name, { @@ -167,10 +181,12 @@ class map_controller extends default_1 { }); } doCreateMap({ definition }) { - const { center, zoom, options, bridgeOptions = {} } = definition; + const { center, zoom, minZoom, maxZoom, options, bridgeOptions = {} } = definition; const map = L.map(this.element, { center: center === null ? undefined : center, zoom: zoom === null ? undefined : zoom, + minZoom: minZoom === null ? undefined : minZoom, + maxZoom: maxZoom === null ? undefined : maxZoom, attributionControl: false, zoomControl: false, ...options, diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts index f72429f79b9..b782c1aac9f 100644 --- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts @@ -87,6 +87,18 @@ export default class extends AbstractMapController< } } + public minZoomValueChanged(): void { + if (this.map && this.hasMinZoomValue && this.minZoomValue) { + this.map.setMinZoom(this.minZoomValue); + } + } + + public maxZoomValueChanged(): void { + if (this.map && this.hasMaxZoomValue && this.maxZoomValue) { + this.map.setMaxZoom(this.maxZoomValue); + } + } + protected dispatchEvent(name: string, payload: Record = {}): void { payload.L = L; this.dispatch(name, { @@ -96,11 +108,13 @@ export default class extends AbstractMapController< } protected doCreateMap({ definition }: { definition: MapDefinition }): L.Map { - const { center, zoom, options, bridgeOptions = {} } = definition; + const { center, zoom, minZoom, maxZoom, options, bridgeOptions = {} } = definition; const map = L.map(this.element, { center: center === null ? undefined : center, zoom: zoom === null ? undefined : zoom, + minZoom: minZoom === null ? undefined : minZoom, + maxZoom: maxZoom === null ? undefined : maxZoom, attributionControl: false, zoomControl: false, ...options, diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php index 672369e2800..0df52df1957 100644 --- a/src/Map/src/Map.php +++ b/src/Map/src/Map.php @@ -48,12 +48,15 @@ public function __construct( array $circles = [], array $rectangles = [], private array $extra = [], + private ?float $minZoom = null, + private ?float $maxZoom = null, ) { $this->markers = new Markers($markers); $this->polygons = new Polygons($polygons); $this->polylines = new Polylines($polylines); $this->circles = new Circles($circles); $this->rectangles = new Rectangles($rectangles); + $this->validateZooms(); } public function getRendererName(): ?string @@ -71,6 +74,23 @@ public function center(Point $center): self public function zoom(float $zoom): self { $this->zoom = $zoom; + $this->validateZooms(); + + return $this; + } + + public function minZoom(float $minZoom): self + { + $this->minZoom = $minZoom; + $this->validateZooms(); + + return $this; + } + + public function maxZoom(float $maxZoom): self + { + $this->maxZoom = $maxZoom; + $this->validateZooms(); return $this; } @@ -206,6 +226,8 @@ public function toArray(): array // Send `null` if empty instead of `[]`, because Stimulus Controller values validation expect an Object, // and sending `(object) $this->extra` mess with LiveComponent hydration checksum validation 'extra' => [] === $this->extra ? null : $this->extra, + 'minZoom' => $this->minZoom, + 'maxZoom' => $this->maxZoom, ]; } @@ -221,6 +243,8 @@ public function toArray(): array * fitBoundsToMarkers?: bool, * options?: array, * extra?: array, + * minZoom?: float, + * maxZoom?: float, * } $map * * @internal @@ -269,4 +293,31 @@ public static function fromArray(array $map): self return new self(...$map); } + + private function validateZooms(): void + { + if (null !== $this->zoom && $this->zoom < 0) { + throw new InvalidArgumentException('The "zoom" must be greater than or equal to 0.'); + } + + if (null !== $this->minZoom && $this->minZoom < 0) { + throw new InvalidArgumentException('The "minZoom" must be greater than or equal to 0.'); + } + + if (null !== $this->maxZoom && $this->maxZoom < 0) { + throw new InvalidArgumentException('The "maxZoom" must be greater than or equal to 0.'); + } + + if (null !== $this->minZoom && null !== $this->maxZoom && $this->minZoom > $this->maxZoom) { + throw new InvalidArgumentException('The "minZoom" must be less than or equal to "maxZoom".'); + } + + if (null !== $this->zoom && null !== $this->minZoom && $this->zoom < $this->minZoom) { + throw new InvalidArgumentException('The "zoom" must be greater than or equal to "minZoom".'); + } + + if (null !== $this->zoom && null !== $this->maxZoom && $this->zoom > $this->maxZoom) { + throw new InvalidArgumentException('The "zoom" must be less than or equal to "maxZoom".'); + } + } } diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php index 05690e7b082..cece6919452 100644 --- a/src/Map/src/Twig/MapRuntime.php +++ b/src/Map/src/Twig/MapRuntime.php @@ -51,9 +51,11 @@ public function renderMap( ?array $polylines = null, ?array $circles = null, ?array $rectangles = null, + ?float $minZoom = null, + ?float $maxZoom = null, ): string { if ($map instanceof Map) { - if (null !== $center || null !== $zoom || $markers || $polygons || $polylines || $circles || $rectangles) { + if (null !== $center || null !== $zoom || $markers || $polygons || $polylines || $circles || $rectangles || $minZoom || $maxZoom) { throw new \InvalidArgumentException('It is not allowed to pass both a Map object and other parameters (like "center", "zoom", "markers", etc...) to the "renderMap" method. Please use either a Map object or the individual parameters.'); } @@ -82,13 +84,19 @@ public function renderMap( if (null !== $zoom) { $map->zoom($zoom); } + if (null !== $minZoom) { + $map->minZoom($minZoom); + } + if (null !== $maxZoom) { + $map->maxZoom($maxZoom); + } return $this->renderer->renderMap($map, $attributes); } public function render(array $args = []): string { - $map = array_intersect_key($args, array_flip(['map', 'center', 'zoom', 'markers', 'polygons', 'polylines', 'circles', 'rectangles'])); + $map = array_intersect_key($args, array_flip(['map', 'center', 'zoom', 'markers', 'polygons', 'polylines', 'circles', 'rectangles', 'minZoom', 'maxZoom'])); $attributes = array_diff_key($args, $map); return $this->renderMap(...$map, attributes: $attributes); diff --git a/src/Map/src/Twig/UXMapComponent.php b/src/Map/src/Twig/UXMapComponent.php index 0a20552e601..2dc6c46b59f 100644 --- a/src/Map/src/Twig/UXMapComponent.php +++ b/src/Map/src/Twig/UXMapComponent.php @@ -27,6 +27,10 @@ final class UXMapComponent { public ?float $zoom; + public ?float $minZoom; + + public ?float $maxZoom; + public ?Point $center; /** diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php index a09e5866d6e..c5018d45a4d 100644 --- a/src/Map/tests/MapTest.php +++ b/src/Map/tests/MapTest.php @@ -73,6 +73,8 @@ public function testZoomAndCenterCanBeOmittedIfFitBoundsToMarkers(): void 'circles' => [], 'rectangles' => [], 'extra' => null, + 'minZoom' => null, + 'maxZoom' => null, ], $array); } @@ -96,6 +98,8 @@ public function testWithMinimumConfiguration(): void 'circles' => [], 'rectangles' => [], 'extra' => null, + 'minZoom' => null, + 'maxZoom' => null, ], $array); } @@ -105,6 +109,8 @@ public function testWithMaximumConfiguration(): void $map ->center(new Point(48.8566, 2.3522)) ->zoom(6) + ->minZoom(3) + ->maxZoom(15) ->fitBoundsToMarkers() ->options(new DummyOptions(mapId: '1a2b3c4d5e', mapType: 'roadmap')) ->addMarker(new Marker( @@ -229,6 +235,8 @@ public function testWithMaximumConfiguration(): void self::assertEquals([ 'center' => ['lat' => 48.8566, 'lng' => 2.3522], 'zoom' => 6.0, + 'minZoom' => 3.0, + 'maxZoom' => 15.0, 'fitBoundsToMarkers' => true, 'options' => [ '@provider' => 'dummy', @@ -415,4 +423,20 @@ public function testWithMaximumConfiguration(): void ], ], $map->toArray()); } + + /** + * @testWith [-1, null, null, "The \"minZoom\" must be greater than or equal to 0."] + * [null, -1, null, "The \"zoom\" must be greater than or equal to 0."] + * [null, null, -1, "The \"maxZoom\" must be greater than or equal to 0."] + * [5, 2, null, "The \"zoom\" must be greater than or equal to \"minZoom\"."] + * [null, 5, 2, "The \"zoom\" must be less than or equal to \"maxZoom\"."] + * [2.1, null, 2.0, "The \"minZoom\" must be less than or equal to \"maxZoom\"."] + */ + public function testZoomsValidation(?float $minZoom, ?float $zoom, ?float $maxZoom, string $expectedExceptionMessage): void + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage($expectedExceptionMessage); + + new Map(zoom: $zoom, minZoom: $minZoom, maxZoom: $maxZoom); + } }