Skip to content

Commit f04730c

Browse files
committed
refactor(data): Convert Layer to Kotlin and add generics
This commit refactors the `Layer` class by converting it from Java to idiomatic Kotlin. It also introduces generics to both the `Layer` and `Renderer` classes to improve type safety throughout the data layer. Key changes: - **Convert `Layer` to Kotlin**: The `Layer` class is now `Layer.kt`, utilizing Kotlin features like properties and `when` expressions for more concise and readable code. - **Introduce Generics**: `Layer` and `Renderer` are now generic (`Layer<T : Feature>`, `Renderer<T extends Feature>`). This enforces type constraints at compile time. - **Improve Type Safety**: Subclasses like `GeoJsonLayer`, `KmlLayer`, `GeoJsonRenderer`, and `KmlRenderer` now extend the generic base classes, eliminating the need for unsafe casts when handling features. - **Add Unit Tests**: A new `LayerTest.kt` file has been added with comprehensive unit tests for the `Layer` class, using MockK and Truth to verify its behavior.
1 parent 542fa2f commit f04730c

File tree

7 files changed

+283
-111
lines changed

7 files changed

+283
-111
lines changed
Lines changed: 66 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Google Inc.
2+
* Copyright 2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,102 +13,97 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
package com.google.maps.android.data
1617

17-
package com.google.maps.android.data;
18-
19-
import com.google.android.gms.maps.GoogleMap;
20-
import com.google.maps.android.data.geojson.GeoJsonLineStringStyle;
21-
import com.google.maps.android.data.geojson.GeoJsonPointStyle;
22-
import com.google.maps.android.data.geojson.GeoJsonPolygonStyle;
23-
import com.google.maps.android.data.geojson.GeoJsonRenderer;
24-
import com.google.maps.android.data.kml.KmlContainer;
25-
import com.google.maps.android.data.kml.KmlGroundOverlay;
26-
import com.google.maps.android.data.kml.KmlRenderer;
18+
import com.google.android.gms.maps.GoogleMap
19+
import com.google.maps.android.data.geojson.GeoJsonLineStringStyle
20+
import com.google.maps.android.data.geojson.GeoJsonPointStyle
21+
import com.google.maps.android.data.geojson.GeoJsonPolygonStyle
22+
import com.google.maps.android.data.geojson.GeoJsonRenderer
23+
import com.google.maps.android.data.kml.KmlContainer
24+
import com.google.maps.android.data.kml.KmlGroundOverlay
25+
import com.google.maps.android.data.kml.KmlRenderer
2726

2827
/**
2928
* An abstraction that shares the common properties of
30-
* {@link com.google.maps.android.data.kml.KmlLayer KmlLayer} and
31-
* {@link com.google.maps.android.data.geojson.GeoJsonLayer GeoJsonLayer}
29+
* [com.google.maps.android.data.kml.KmlLayer] and
30+
* [com.google.maps.android.data.geojson.GeoJsonLayer]
3231
*/
33-
public abstract class Layer {
34-
35-
private Renderer mRenderer;
32+
abstract class Layer<T : Feature> {
33+
@JvmField
34+
protected var renderer: Renderer<T>? = null
3635

3736
/**
3837
* Adds the KML data to the map
3938
*/
40-
protected void addKMLToMap() {
41-
if (mRenderer instanceof KmlRenderer) {
42-
((KmlRenderer) mRenderer).addLayerToMap();
43-
} else {
44-
throw new UnsupportedOperationException("Stored renderer is not a KmlRenderer");
45-
}
39+
protected fun addKMLToMap() {
40+
val kmlRenderer = renderer as? KmlRenderer
41+
?: throw UnsupportedOperationException("Stored renderer is not a KmlRenderer")
42+
kmlRenderer.addLayerToMap()
4643
}
4744

4845
/**
4946
* Adds GeoJson data to the map
5047
*/
51-
protected void addGeoJsonToMap() {
52-
if (mRenderer instanceof GeoJsonRenderer) {
53-
((GeoJsonRenderer) mRenderer).addLayerToMap();
54-
} else {
55-
throw new UnsupportedOperationException("Stored renderer is not a GeoJsonRenderer");
56-
}
48+
protected fun addGeoJsonToMap() {
49+
val geoJsonRenderer = renderer as? GeoJsonRenderer
50+
?: throw UnsupportedOperationException("Stored renderer is not a GeoJsonRenderer")
51+
geoJsonRenderer.addLayerToMap()
5752
}
5853

59-
public abstract void addLayerToMap();
54+
abstract fun addLayerToMap()
6055

6156
/**
6257
* Removes all the data from the map and clears all the stored placemarks
6358
*/
64-
public void removeLayerFromMap() {
65-
if (mRenderer instanceof GeoJsonRenderer) {
66-
((GeoJsonRenderer) mRenderer).removeLayerFromMap();
67-
} else if (mRenderer instanceof KmlRenderer) {
68-
((KmlRenderer) mRenderer).removeLayerFromMap();
59+
fun removeLayerFromMap() {
60+
when (val r = renderer) {
61+
is GeoJsonRenderer -> r.removeLayerFromMap()
62+
is KmlRenderer -> r.removeLayerFromMap()
6963
}
7064
}
7165

7266
/**
7367
* Sets a single click listener for the entire GoogleMap object, that will be called
7468
* with the corresponding Feature object when an object on the map (Polygon,
7569
* Marker, Polyline) is clicked.
76-
* <p>
70+
*
7771
* If getFeature() returns null this means that either the object is inside a KMLContainer,
7872
* or the object is a MultiPolygon, MultiLineString or MultiPoint and must
7973
* be handled differently.
8074
*
8175
* @param listener Listener providing the onFeatureClick method to call.
8276
*/
83-
public void setOnFeatureClickListener(final OnFeatureClickListener listener) {
84-
mRenderer.setOnFeatureClickListener(listener);
77+
fun setOnFeatureClickListener(listener: OnFeatureClickListener) {
78+
renderer?.setOnFeatureClickListener(listener)
8579
}
8680

8781
/**
8882
* Callback interface for when a map object is clicked.
8983
*/
90-
public interface OnFeatureClickListener {
91-
void onFeatureClick(Feature feature);
84+
fun interface OnFeatureClickListener {
85+
fun onFeatureClick(feature: Feature)
9286
}
9387

9488
/**
9589
* Stores a new Renderer object into mRenderer
9690
*
9791
* @param renderer the new Renderer object that belongs to this Layer
9892
*/
99-
protected void storeRenderer(Renderer renderer) {
100-
mRenderer = renderer;
93+
protected fun storeRenderer(renderer: Renderer<T>) {
94+
this.renderer = renderer
10195
}
10296

10397
/**
10498
* Gets an iterable of all Feature elements that have been added to the layer
10599
*
106100
* @return iterable of Feature elements
107101
*/
108-
public Iterable<? extends Feature> getFeatures() {
109-
return mRenderer.getFeatures();
102+
open fun getFeatures(): Iterable<T> {
103+
return renderer?.features ?: emptyList()
110104
}
111105

106+
112107
/**
113108
* Retrieves a corresponding Feature instance for the given Object
114109
* Allows maps with multiple layers to determine which layer the Object
@@ -117,103 +112,91 @@ public abstract class Layer {
117112
* @param mapObject Object
118113
* @return Feature for the given object
119114
*/
120-
public Feature getFeature(Object mapObject) {
121-
return mRenderer.getFeature(mapObject);
115+
fun getFeature(mapObject: Any): T? {
116+
return renderer?.getFeature(mapObject) as T?
122117
}
123118

124-
public Feature getContainerFeature(Object mapObject) {
125-
return mRenderer.getContainerFeature(mapObject);
119+
fun getContainerFeature(mapObject: Any): T? {
120+
return renderer?.getContainerFeature(mapObject) as T?
126121
}
127122

128123
/**
129124
* Checks if there are any features currently on the layer
130125
*
131126
* @return true if there are features on the layer, false otherwise
132127
*/
133-
protected boolean hasFeatures() {
134-
return mRenderer.hasFeatures();
135-
}
128+
protected open fun hasFeatures(): Boolean = renderer?.hasFeatures() ?: false
136129

137130
/**
138131
* Checks if the layer contains any KmlContainers
139132
*
140133
* @return true if there is at least 1 container within the KmlLayer, false otherwise
141134
*/
142-
protected boolean hasContainers() {
143-
if (mRenderer instanceof KmlRenderer) {
144-
return ((KmlRenderer) mRenderer).hasNestedContainers();
145-
}
146-
return false;
135+
protected open fun hasContainers(): Boolean {
136+
return (renderer as? KmlRenderer)?.hasNestedContainers() ?: false
147137
}
148138

149139
/**
150140
* Gets an iterable of KmlContainerInterface objects
151141
*
152142
* @return iterable of KmlContainerInterface objects
153143
*/
154-
protected Iterable<KmlContainer> getContainers() {
155-
if (mRenderer instanceof KmlRenderer) {
156-
return ((KmlRenderer) mRenderer).getNestedContainers();
157-
}
158-
return null;
144+
protected open fun getContainers(): Iterable<KmlContainer>? {
145+
return (renderer as? KmlRenderer)?.nestedContainers
159146
}
160147

148+
161149
/**
162150
* Gets an iterable of KmlGroundOverlay objects
163151
*
164152
* @return iterable of KmlGroundOverlay objects
165153
*/
166-
protected Iterable<KmlGroundOverlay> getGroundOverlays() {
167-
if (mRenderer instanceof KmlRenderer) {
168-
return ((KmlRenderer) mRenderer).getGroundOverlays();
169-
}
170-
return null;
154+
protected open fun getGroundOverlays(): Iterable<KmlGroundOverlay>? {
155+
return (renderer as? KmlRenderer)?.groundOverlays
171156
}
172157

173158
/**
174159
* Gets the map on which the layer is rendered
175160
*
176161
* @return map on which the layer is rendered
177162
*/
178-
public GoogleMap getMap() {
179-
return mRenderer.getMap();
180-
}
163+
val map: GoogleMap?
164+
get() = renderer?.map
181165

182166
/**
183167
* Renders the layer on the given map. The layer on the current map is removed and
184168
* added to the given map.
185169
*
186170
* @param map to render the layer on, if null the layer is cleared from the current map
187171
*/
188-
public void setMap(GoogleMap map) {
189-
mRenderer.setMap(map);
172+
fun setMap(map: GoogleMap?) {
173+
renderer?.map = map
190174
}
191175

192176
/**
193177
* Checks if the current layer has been added to the map
194178
*
195179
* @return true if the layer is on the map, false otherwise
196180
*/
197-
public boolean isLayerOnMap() {
198-
return mRenderer.isLayerOnMap();
199-
}
181+
val isLayerOnMap: Boolean
182+
get() = renderer?.isLayerOnMap ?: false
200183

201184
/**
202185
* Adds a provided feature to the map
203186
*
204187
* @param feature feature to add to map
205188
*/
206-
protected void addFeature(Feature feature) {
207-
mRenderer.addFeature(feature);
189+
protected open fun addFeature(feature: T) {
190+
renderer?.addFeature(feature)
208191
}
209192

210193
/**
211194
* Remove a specified feature from the map
212195
*
213196
* @param feature feature to be removed
214197
*/
215-
protected void removeFeature(Feature feature) {
216-
mRenderer.removeFeature(feature);
198+
protected open fun removeFeature(feature: T) {
199+
renderer?.removeFeature(feature)
217200
}
218201

219202
/**
@@ -222,27 +205,24 @@ public abstract class Layer {
222205
*
223206
* @return default style used to render GeoJsonPoints
224207
*/
225-
public GeoJsonPointStyle getDefaultPointStyle() {
226-
return mRenderer.getDefaultPointStyle();
227-
}
208+
val defaultPointStyle: GeoJsonPointStyle?
209+
get() = renderer?.defaultPointStyle
228210

229211
/**
230212
* Gets the default style used to render GeoJsonLineStrings. Any changes to this style will be
231213
* reflected in the features that use it.
232214
*
233215
* @return default style used to render GeoJsonLineStrings
234216
*/
235-
public GeoJsonLineStringStyle getDefaultLineStringStyle() {
236-
return mRenderer.getDefaultLineStringStyle();
237-
}
217+
val defaultLineStringStyle: GeoJsonLineStringStyle?
218+
get() = renderer?.defaultLineStringStyle
238219

239220
/**
240221
* Gets the default style used to render GeoJsonPolygons. Any changes to this style will be
241222
* reflected in the features that use it.
242223
*
243224
* @return default style used to render GeoJsonPolygons
244225
*/
245-
public GeoJsonPolygonStyle getDefaultPolygonStyle() {
246-
return mRenderer.getDefaultPolygonStyle();
247-
}
226+
val defaultPolygonStyle: GeoJsonPolygonStyle?
227+
get() = renderer?.defaultPolygonStyle
248228
}

library/src/main/java/com/google/maps/android/data/Renderer.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
* {@link com.google.maps.android.data.kml.KmlRenderer KmlRenderer} and
7878
* {@link com.google.maps.android.data.geojson.GeoJsonRenderer GeoJsonRenderer}
7979
*/
80-
public class Renderer {
80+
public class Renderer<T extends Feature> {
8181

8282
private static final int MARKER_ICON_SIZE = 32;
8383

@@ -87,7 +87,7 @@ public class Renderer {
8787

8888
private GoogleMap mMap;
8989

90-
private final BiMultiMap<Feature> mFeatures = new BiMultiMap<>();
90+
private final BiMultiMap<T> mFeatures = new BiMultiMap<>();
9191

9292
private HashMap<String, KmlStyle> mStyles;
9393

@@ -156,7 +156,7 @@ public Renderer(GoogleMap map,
156156
* @param polylineManager polyline manager to create polyline collection from
157157
* @param groundOverlayManager ground overlay manager to create ground overlay collection from
158158
*/
159-
public Renderer(GoogleMap map, HashMap<? extends Feature, Object> features, MarkerManager markerManager, PolygonManager polygonManager, PolylineManager polylineManager, GroundOverlayManager groundOverlayManager) {
159+
public Renderer(GoogleMap map, HashMap<T, Object> features, MarkerManager markerManager, PolygonManager polygonManager, PolylineManager polylineManager, GroundOverlayManager groundOverlayManager) {
160160
this(map, null, new GeoJsonPointStyle(), new GeoJsonLineStringStyle(), new GeoJsonPolygonStyle(), null, markerManager, polygonManager, polylineManager, groundOverlayManager);
161161
mFeatures.putAll(features);
162162
mImagesCache = null;
@@ -274,7 +274,7 @@ protected void putContainerFeature(Object mapObject, Feature placemark) {
274274
*
275275
* @return set containing Features
276276
*/
277-
public Set<Feature> getFeatures() {
277+
public Set<T> getFeatures() {
278278
return mFeatures.keySet();
279279
}
280280

@@ -284,7 +284,7 @@ public Set<Feature> getFeatures() {
284284
* @param mapObject Marker, Polyline or Polygon
285285
* @return Feature for the given map object
286286
*/
287-
Feature getFeature(Object mapObject) {
287+
T getFeature(Object mapObject) {
288288
return mFeatures.getKey(mapObject);
289289
}
290290

@@ -310,7 +310,7 @@ public Collection<Object> getValues() {
310310
*
311311
* @return mFeatures hashmap
312312
*/
313-
protected HashMap<? extends Feature, Object> getAllFeatures() {
313+
protected HashMap<T, Object> getAllFeatures() {
314314
return mFeatures;
315315
}
316316

@@ -482,7 +482,7 @@ GeoJsonPolygonStyle getDefaultPolygonStyle() {
482482
* @param feature Feature to be added onto the map
483483
* @param object Corresponding map object to this feature
484484
*/
485-
protected void putFeatures(Feature feature, Object object) {
485+
protected void putFeatures(T feature, Object object) {
486486
mFeatures.put(feature, object);
487487
}
488488

@@ -610,7 +610,7 @@ protected void removeGroundOverlays(HashMap<KmlGroundOverlay, GroundOverlay> gro
610610
*
611611
* @param feature feature to remove from map
612612
*/
613-
protected void removeFeature(Feature feature) {
613+
protected void removeFeature(T feature) {
614614
// Check if given feature is stored
615615
if (mFeatures.containsKey(feature)) {
616616
removeFromMap(mFeatures.remove(feature));
@@ -652,7 +652,7 @@ protected void clearStylesRenderer() {
652652
*/
653653
protected void storeData(HashMap<String, KmlStyle> styles,
654654
HashMap<String, String> styleMaps,
655-
HashMap<KmlPlacemark, Object> features,
655+
HashMap<T, Object> features,
656656
ArrayList<KmlContainer> folders,
657657
HashMap<KmlGroundOverlay, GroundOverlay> groundOverlays) {
658658
mStyles = styles;
@@ -667,7 +667,7 @@ protected void storeData(HashMap<String, KmlStyle> styles,
667667
*
668668
* @param feature feature to add to the map
669669
*/
670-
protected void addFeature(Feature feature) {
670+
protected void addFeature(T feature) {
671671
Object mapObject = FEATURE_NOT_ON_MAP;
672672
if (feature instanceof GeoJsonFeature) {
673673
setFeatureDefaultStyles((GeoJsonFeature) feature);

0 commit comments

Comments
 (0)