Skip to content

Commit cbc0f79

Browse files
authored
feat: zoom to fit the provided features in the viewport (#8082)
* feat: zoom to fit the provides features in the viewport * move new API from MapBase to Map
1 parent dfc5d78 commit cbc0f79

File tree

5 files changed

+218
-0
lines changed
  • vaadin-map-flow-parent
    • vaadin-map-flow-integration-tests/src
    • vaadin-map-flow/src/main
    • vaadin-map-testbench/src/main/java/com/vaadin/flow/component/map/testbench

5 files changed

+218
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* This program is available under Vaadin Commercial License and Service Terms.
5+
*
6+
* See {@literal <https://vaadin.com/commercial-license-and-service-terms>} for the full
7+
* license.
8+
*/
9+
package com.vaadin.flow.component.map;
10+
11+
import java.util.List;
12+
13+
import com.vaadin.flow.component.html.Div;
14+
import com.vaadin.flow.component.html.NativeButton;
15+
import com.vaadin.flow.component.map.configuration.Coordinate;
16+
import com.vaadin.flow.component.map.configuration.feature.MarkerFeature;
17+
import com.vaadin.flow.router.Route;
18+
19+
@Route("vaadin-map/zoom-to-fit")
20+
public class ZoomToFitPage extends Div {
21+
public static final MarkerFeature FEATURE_1 = new MarkerFeature(
22+
new Coordinate(-40, 0));
23+
public static final MarkerFeature FEATURE_2 = new MarkerFeature(
24+
new Coordinate(-41, 0));
25+
public static final MarkerFeature FEATURE_3 = new MarkerFeature(
26+
new Coordinate(-42, 0));
27+
public static final MarkerFeature FEATURE_4 = new MarkerFeature(
28+
new Coordinate(40, 0));
29+
public static final MarkerFeature FEATURE_5 = new MarkerFeature(
30+
new Coordinate(41, 0));
31+
public static final MarkerFeature FEATURE_6 = new MarkerFeature(
32+
new Coordinate(42, 0));
33+
34+
public ZoomToFitPage() {
35+
Map map = new Map();
36+
37+
map.getFeatureLayer().addFeature(FEATURE_1);
38+
map.getFeatureLayer().addFeature(FEATURE_2);
39+
map.getFeatureLayer().addFeature(FEATURE_3);
40+
41+
map.getFeatureLayer().addFeature(FEATURE_4);
42+
map.getFeatureLayer().addFeature(FEATURE_5);
43+
map.getFeatureLayer().addFeature(FEATURE_6);
44+
45+
// Initial zoom to fit
46+
map.zoomToFit(List.of(FEATURE_1, FEATURE_2, FEATURE_3), 50, 0);
47+
48+
// Buttons to fit individual sets
49+
NativeButton zoomToFirstSet = new NativeButton("Zoom to fit first set",
50+
e -> map.zoomToFit(List.of(FEATURE_1, FEATURE_2, FEATURE_3), 50,
51+
0));
52+
zoomToFirstSet.setId("zoom-to-first-set");
53+
54+
NativeButton zoomToSecondSet = new NativeButton(
55+
"Zoom to fit second set",
56+
e -> map.zoomToFit(List.of(FEATURE_4, FEATURE_5, FEATURE_6), 50,
57+
0));
58+
zoomToSecondSet.setId("zoom-to-second-set");
59+
60+
add(map, zoomToFirstSet, zoomToSecondSet);
61+
}
62+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* This program is available under Vaadin Commercial License and Service Terms.
5+
*
6+
* See {@literal <https://vaadin.com/commercial-license-and-service-terms>} for the full
7+
* license.
8+
*/
9+
package com.vaadin.flow.components.map;
10+
11+
import java.util.List;
12+
13+
import org.junit.Assert;
14+
import org.junit.Before;
15+
import org.junit.Test;
16+
17+
import com.vaadin.flow.component.map.ZoomToFitPage;
18+
import com.vaadin.flow.component.map.configuration.Coordinate;
19+
import com.vaadin.flow.component.map.configuration.feature.MarkerFeature;
20+
import com.vaadin.flow.component.map.testbench.MapElement;
21+
import com.vaadin.flow.testutil.TestPath;
22+
import com.vaadin.tests.AbstractComponentIT;
23+
24+
@TestPath("vaadin-map/zoom-to-fit")
25+
public class ZoomToFitIT extends AbstractComponentIT {
26+
27+
private MapElement map;
28+
29+
@Before
30+
public void init() {
31+
open();
32+
map = $(MapElement.class).waitForFirst();
33+
}
34+
35+
@Test
36+
public void initialZoomToFit_showsFirstSet() {
37+
Assert.assertTrue(isFeatureVisible(map, ZoomToFitPage.FEATURE_1));
38+
Assert.assertTrue(isFeatureVisible(map, ZoomToFitPage.FEATURE_2));
39+
Assert.assertTrue(isFeatureVisible(map, ZoomToFitPage.FEATURE_3));
40+
41+
Assert.assertFalse(isFeatureVisible(map, ZoomToFitPage.FEATURE_4));
42+
Assert.assertFalse(isFeatureVisible(map, ZoomToFitPage.FEATURE_5));
43+
Assert.assertFalse(isFeatureVisible(map, ZoomToFitPage.FEATURE_6));
44+
}
45+
46+
@Test
47+
public void zoomToFitSecondSet_showsSecondSet() {
48+
clickElementWithJs("zoom-to-second-set");
49+
50+
Assert.assertFalse(isFeatureVisible(map, ZoomToFitPage.FEATURE_1));
51+
Assert.assertFalse(isFeatureVisible(map, ZoomToFitPage.FEATURE_2));
52+
Assert.assertFalse(isFeatureVisible(map, ZoomToFitPage.FEATURE_3));
53+
54+
Assert.assertTrue(isFeatureVisible(map, ZoomToFitPage.FEATURE_4));
55+
Assert.assertTrue(isFeatureVisible(map, ZoomToFitPage.FEATURE_5));
56+
Assert.assertTrue(isFeatureVisible(map, ZoomToFitPage.FEATURE_6));
57+
}
58+
59+
private boolean isFeatureVisible(MapElement map, MarkerFeature feature) {
60+
Coordinate coordinates = feature.getGeometry().getCoordinates();
61+
List<Double> extent = map.getMapReference().getView().calculateExtent();
62+
return coordinates.getX() >= extent.get(0)
63+
&& coordinates.getX() <= extent.get(2)
64+
&& coordinates.getY() >= extent.get(1)
65+
&& coordinates.getY() <= extent.get(3);
66+
}
67+
}

vaadin-map-flow-parent/vaadin-map-flow/src/main/java/com/vaadin/flow/component/map/Map.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
package com.vaadin.flow.component.map;
1010

11+
import java.util.List;
1112
import java.util.Objects;
1213

1314
import com.vaadin.flow.component.Tag;
@@ -16,6 +17,7 @@
1617
import com.vaadin.flow.component.dependency.NpmPackage;
1718
import com.vaadin.flow.component.map.configuration.Configuration;
1819
import com.vaadin.flow.component.map.configuration.Coordinate;
20+
import com.vaadin.flow.component.map.configuration.Feature;
1921
import com.vaadin.flow.component.map.configuration.View;
2022
import com.vaadin.flow.component.map.configuration.feature.MarkerFeature;
2123
import com.vaadin.flow.component.map.configuration.layer.FeatureLayer;
@@ -27,6 +29,10 @@
2729
import com.vaadin.flow.component.map.configuration.source.Source;
2830
import com.vaadin.flow.component.map.configuration.source.VectorSource;
2931
import com.vaadin.flow.component.map.configuration.source.XYZSource;
32+
import com.vaadin.flow.internal.JacksonUtils;
33+
34+
import tools.jackson.databind.node.ArrayNode;
35+
import tools.jackson.databind.node.ObjectNode;
3036

3137
/**
3238
* Map is a component for displaying geographic maps from various sources. It
@@ -321,4 +327,44 @@ public double getZoom() {
321327
public void setZoom(double zoom) {
322328
getView().setZoom(zoom);
323329
}
330+
331+
/**
332+
* Zooms and pans the map to fit the given features in the viewport. Uses a
333+
* default padding of 50 pixels and animation duration of 400 milliseconds.
334+
*
335+
* @param features
336+
* the features to fit in the viewport
337+
*/
338+
public void zoomToFit(List<Feature> features) {
339+
zoomToFit(features, 50, 400);
340+
}
341+
342+
/**
343+
* Zooms and pans the map to fit the given features in the viewport.
344+
*
345+
* @param features
346+
* the features to fit in the viewport
347+
* @param padding
348+
* padding in pixels to add around the features
349+
* @param duration
350+
* animation duration in milliseconds, 0 for no animation
351+
*/
352+
public void zoomToFit(List<Feature> features, int padding, int duration) {
353+
ArrayNode featureIds = JacksonUtils.createArrayNode();
354+
features.forEach(feature -> featureIds.add(feature.getId()));
355+
356+
ObjectNode options = JacksonUtils.createObjectNode();
357+
options.put("padding", padding);
358+
options.put("duration", duration);
359+
360+
// In order to allow calling this method before the map, or the view
361+
// that contains it, is attached to the UI, this needs to be delayed
362+
// with beforeClientResponse to ensure that the client-side connector
363+
// has been initialized beforehand.
364+
getElement().getNode()
365+
.runWhenAttached(ui -> ui.beforeClientResponse(this,
366+
context -> getElement().executeJs(
367+
"this.$connector.zoomToFit($0, $1)", featureIds,
368+
options)));
369+
}
324370
}

vaadin-map-flow-parent/vaadin-map-flow/src/main/resources/META-INF/resources/frontend/vaadin-map/mapConnector.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* See <https://vaadin.com/commercial-license-and-service-terms> for the full
88
* license.
99
*/
10+
import { extend } from 'ol/extent';
1011
import Translate from 'ol/interaction/Translate';
1112
import { setUserProjection as openLayersSetUserProjection } from 'ol/proj';
1213
import { register as openLayersRegisterProjections } from 'ol/proj/proj4';
@@ -68,6 +69,44 @@ function init(mapElement) {
6869
.getArray()
6970
.forEach((layer) => layer.changed());
7071
});
72+
},
73+
/**
74+
* Adjust the map view to fit the given features.
75+
* @param featureIds array of IDs of the features to fit in the map view
76+
* @param options optional options for the OL `View.fit` method
77+
*/
78+
zoomToFit(featureIds, options) {
79+
// Synchronization call for the referenced features must run beforehand, but may have
80+
// been scheduled after this call. Using a timeout to compensate.
81+
setTimeout(() => {
82+
const features = featureIds.map((id) => this.lookup.get(id)).filter(Boolean);
83+
if (features.length === 0) {
84+
return;
85+
}
86+
87+
let extent;
88+
features.forEach((feature) => {
89+
const featureExtent = feature.getGeometry().getExtent();
90+
if (!extent) {
91+
extent = featureExtent.slice();
92+
} else {
93+
extend(extent, featureExtent);
94+
}
95+
});
96+
97+
const padding = options?.padding ?? 0;
98+
const duration = options?.duration ?? 0;
99+
// Viewport size in the OL View instance may not have updated yet to the actual map size,
100+
// so provide the size explicitly
101+
const bounds = mapElement.getBoundingClientRect();
102+
const size = [bounds.width, bounds.height];
103+
104+
mapElement.configuration.getView().fit(extent, {
105+
padding: [padding, padding, padding, padding],
106+
duration,
107+
size
108+
});
109+
});
71110
}
72111
};
73112

vaadin-map-flow-parent/vaadin-map-testbench/src/main/java/com/vaadin/flow/component/map/testbench/MapElement.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,10 @@ public float getRotation() {
318318
public void setRotation(float rotation) {
319319
executor.executeScript(path("setRotation(%s)", rotation));
320320
}
321+
322+
public List<Double> calculateExtent() {
323+
return (List<Double>) get("calculateExtent()");
324+
}
321325
}
322326

323327
public static class LayerCollectionReference

0 commit comments

Comments
 (0)