diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000000..8bb1bfb0a7f Binary files /dev/null and b/.DS_Store differ diff --git a/appinventor/appengine/src/com/google/appinventor/client/OdeMessages.java b/appinventor/appengine/src/com/google/appinventor/client/OdeMessages.java index e4180713f0f..bba85d9c927 100755 --- a/appinventor/appengine/src/com/google/appinventor/client/OdeMessages.java +++ b/appinventor/appengine/src/com/google/appinventor/client/OdeMessages.java @@ -5363,6 +5363,14 @@ String newerVersionComponentException(String componentType, int srcCompVersion, @Description("Terrain map type") String mapTypeTerrain(); + @DefaultMessage("Custom") + @Description("Custom map type") + String mapTypeCustom(); + + @DefaultMessage("CustomUrl") + @Description("The URL of the custom tile layer to use as the base of the map") + String mapCustomUrl(); + @DefaultMessage("Metric") @Description("Display name for the metric unit system") String mapScaleUnitsMetric(); @@ -5431,6 +5439,22 @@ String newerVersionComponentException(String componentType, int srcCompVersion, @Description("") String expectedLatLongPair(String property); + @DefaultMessage("The provided URL {0} does not contain placeholders for {1}.") // Can't use {x} here, Java compiler tries to interpret the variable x + @Description("") + String customUrlNoPlaceholders(String property, String placeholders); + + @DefaultMessage("The provided URL {0}, when tested, failed authentication (with HTTP status code {1}).") + @Description("") + String customUrlBadAuthentication(String property, int statusCode); + + @DefaultMessage("The provided URL {0}, when tested, returned a bad HTTP status code ({1}).") + @Description("") + String customUrlBadStatusCode(String property, int statusCode); + + @DefaultMessage("The provided URL {0}, when tested, returned an exception ({1}).") + @Description("") + String customUrlException(String property, String e); + @DefaultMessage("Notice!") @Description("Title for the Warning Dialog Box") String NoticeTitle(); diff --git a/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/MockMap.java b/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/MockMap.java index 16081ff965d..f324657bd6e 100644 --- a/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/MockMap.java +++ b/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/MockMap.java @@ -29,6 +29,7 @@ public final class MockMap extends MockContainer { protected static final String PROPERTY_NAME_LATITUDE = "Latitude"; protected static final String PROPERTY_NAME_LONGITUDE = "Longitude"; protected static final String PROPERTY_NAME_MAP_TYPE = "MapType"; + protected static final String PROPERTY_NAME_CUSTOM_URL = "CustomUrl"; protected static final String PROPERTY_NAME_CENTER_FROM_STRING = "CenterFromString"; protected static final String PROPERTY_NAME_ZOOM_LEVEL = "ZoomLevel"; protected static final String PROPERTY_NAME_SHOW_COMPASS = "ShowCompass"; @@ -181,6 +182,8 @@ public void onPropertyChange(String propertyName, String newValue) { invalidateMap(); } else if (propertyName.equals(PROPERTY_NAME_MAP_TYPE)) { setMapType(newValue); + } else if (propertyName.equals(PROPERTY_NAME_CUSTOM_URL)) { + setCustomUrl(newValue); } else if (propertyName.equals(PROPERTY_NAME_CENTER_FROM_STRING)) { setCenter(newValue); } else if (propertyName.equals(PROPERTY_NAME_ZOOM_LEVEL)) { @@ -222,6 +225,10 @@ private void setMapType(String tileLayerId) { } } + private void setCustomUrl(String newCustomUrl) { + updateCustomUrl(newCustomUrl); + } + private void setCenter(String center) { String[] parts = center.split(","); if (parts.length != 2) { @@ -484,7 +491,10 @@ private native void initPanel()/*-{ attribution: 'Satellite imagery © USGS'}), L.tileLayer('//basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', {minZoom: 0, maxZoom: 15, - attribution: 'Map data © USGS'}) + attribution: 'Map data © USGS'}), + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', + {minZoom: 0, maxZoom: 18, + attribution: 'Custom map'}) ]; this.@com.google.appinventor.client.editor.simple.components.MockMap::tileLayers = tileLayers; this.@com.google.appinventor.client.editor.simple.components.MockMap::baseLayer = @@ -606,6 +616,23 @@ private native void updateMapType(int type)/*-{ } }-*/; + private native void updateCustomUrl(String customUrl)/*-{ + var L = $wnd.top.L; + var map = this.@com.google.appinventor.client.editor.simple.components.MockMap::mapInstance; + var tileLayers = this.@com.google.appinventor.client.editor.simple.components.MockMap::tileLayers; + var baseLayer = this.@com.google.appinventor.client.editor.simple.components.MockMap::baseLayer; + if (map && baseLayer && tileLayers) { + tileLayers[4] = L.tileLayer(customUrl, + {minZoom: 0, maxZoom: 18, + attribution: 'Custom map data'}); + map.removeLayer(baseLayer); + baseLayer = tileLayers[4]; + map.addLayer(baseLayer); + baseLayer.bringToBack(); + this.@com.google.appinventor.client.editor.simple.components.MockMap::tileLayers = tileLayers; + } + }-*/; + native LatLng projectFromXY(int x, int y)/*-{ var map = this.@com.google.appinventor.client.editor.simple.components.MockMap::mapInstance; if (map) { diff --git a/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/utils/PropertiesUtil.java b/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/utils/PropertiesUtil.java index 52f979dd708..b975d056ad8 100644 --- a/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/utils/PropertiesUtil.java +++ b/appinventor/appengine/src/com/google/appinventor/client/editor/simple/components/utils/PropertiesUtil.java @@ -45,6 +45,7 @@ import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidListViewLayoutChoicePropertyEditor; import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidMapScaleUnitsPropertyEditor; import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidMapTypePropertyEditor; +import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidMapCustomUrlPropertyEditor; import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidNavigationMethodChoicePropertyEditor; import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidRecyclerViewOrientationPropertyEditor; import com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidScreenAnimationChoicePropertyEditor; @@ -266,6 +267,8 @@ public static PropertyEditor createPropertyEditor(String editorType, String defa return new YoungAndroidFloatRangePropertyEditor(-180, 180); } else if (editorType.equals(PropertyTypeConstants.PROPERTY_TYPE_MAP_TYPE)) { return new YoungAndroidMapTypePropertyEditor(); + } else if (editorType.equals(PropertyTypeConstants.PROPERTY_TYPE_MAP_CUSTOMURL)) { + return new YoungAndroidMapCustomUrlPropertyEditor(); } else if (editorType.equals(PropertyTypeConstants.PROPERTY_TYPE_MAP_UNIT_SYSTEM)) { return new YoungAndroidMapScaleUnitsPropertyEditor(); } else if (editorType.equals(PropertyTypeConstants.PROPERTY_TYPE_MAP_ZOOM)) { diff --git a/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapCustomUrlPropertyEditor.java b/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapCustomUrlPropertyEditor.java new file mode 100644 index 00000000000..502ee4d64ae --- /dev/null +++ b/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapCustomUrlPropertyEditor.java @@ -0,0 +1,65 @@ +// -*- mode: java; c-basic-offset: 2; -*- +// Copyright 2024 MIT, All rights reserved +// Released under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +package com.google.appinventor.client.editor.youngandroid.properties; + +import com.google.appinventor.client.widgets.properties.TextPropertyEditor; +import static com.google.appinventor.client.Ode.MESSAGES; +import com.google.gwt.http.client.*; +import com.google.gwt.user.client.Window; + +/** + * Property editor for Map custom URL matching a particular format. + */ +public class YoungAndroidMapCustomUrlPropertyEditor extends TextPropertyEditor { + + public YoungAndroidMapCustomUrlPropertyEditor() { + } + + @Override + protected void validate(String text) throws InvalidTextException { + // Check that the custom URL looks vaguely correct + if (!(text.startsWith("https://") || text.startsWith("http://")) + || !text.contains("{x}") + || !text.contains("{y}") + || !text.contains("{z}")) { + throw new InvalidTextException(MESSAGES.customUrlNoPlaceholders(text, "{x}, {y} and {z}")); + } + + // Try to request a single tile from the custom URL source as a final validation, only report errors + String urlString = text.replace("{x}", "0") + .replace("{y}", "0") + .replace("{z}", "0"); + RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, urlString); + try { + builder.sendRequest(null, new RequestCallback() { + @Override + public void onResponseReceived(Request request, Response response) { + handleResponseCode(urlString, response.getStatusCode()); + } + + @Override + public void onError(Request request, Throwable exception) { + handleRequestError(urlString, exception); + } + }); + } catch (RequestException e) { + throw new InvalidTextException(MESSAGES.customUrlException(urlString, e.getMessage())); + } + } + + // Window.alert is used here, rather than throw InvalidTextException, due to RequestBuilder Override signatures + private void handleResponseCode(String urlString, int responseCode) { + if (responseCode == 401 || responseCode == 403) { + Window.alert(MESSAGES.customUrlBadAuthentication(urlString, responseCode)); + } else if (responseCode >= 400) { + Window.alert(MESSAGES.customUrlBadStatusCode(urlString, responseCode)); + } + } + + private void handleRequestError(String urlString, Throwable exception) { + Window.alert(MESSAGES.customUrlException(urlString, exception.getMessage())); + } +} \ No newline at end of file diff --git a/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapTypePropertyEditor.java b/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapTypePropertyEditor.java index d5ad7042aa7..371df9f6d49 100644 --- a/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapTypePropertyEditor.java +++ b/appinventor/appengine/src/com/google/appinventor/client/editor/youngandroid/properties/YoungAndroidMapTypePropertyEditor.java @@ -19,7 +19,8 @@ public class YoungAndroidMapTypePropertyEditor extends ChoicePropertyEditor { private static final Choice[] mapTypes = new Choice[] { new Choice(MESSAGES.mapTypeRoads(), "1"), new Choice(MESSAGES.mapTypeAerial(), "2"), - new Choice(MESSAGES.mapTypeTerrain(), "3") + new Choice(MESSAGES.mapTypeTerrain(), "3"), + new Choice(MESSAGES.mapTypeCustom(), "4") }; public YoungAndroidMapTypePropertyEditor() { diff --git a/appinventor/appengine/src/com/google/appinventor/client/youngandroid/YoungAndroidFormUpgrader.java b/appinventor/appengine/src/com/google/appinventor/client/youngandroid/YoungAndroidFormUpgrader.java index f916d4d5c0d..1beeab0970f 100644 --- a/appinventor/appengine/src/com/google/appinventor/client/youngandroid/YoungAndroidFormUpgrader.java +++ b/appinventor/appengine/src/com/google/appinventor/client/youngandroid/YoungAndroidFormUpgrader.java @@ -2064,6 +2064,10 @@ private static int upgradeMapProperties(Map componentProperti // Adds ScaleUnits and MapType dropdowns. srcCompVersion = 6; } + if (srcCompVersion < 7) { + // Adds CustomUrl (MapType 4). + srcCompVersion = 7; + } return srcCompVersion; } diff --git a/appinventor/blocklyeditor/src/versioning.js b/appinventor/blocklyeditor/src/versioning.js index b4eeee2c134..4da6ab984ec 100644 --- a/appinventor/blocklyeditor/src/versioning.js +++ b/appinventor/blocklyeditor/src/versioning.js @@ -2506,9 +2506,13 @@ Blockly.Versioning.AllUpgradeMaps = // AI2: // - Adds Units and MapType dropdowns. 6: [Blockly.Versioning.makeSetterUseDropdown( - 'Map', 'ScaleUnits', 'ScaleUnits'), - Blockly.Versioning.makeSetterUseDropdown( - 'Map', 'MapType', 'MapType')] + 'Map', 'ScaleUnits', 'ScaleUnits'), + Blockly.Versioning.makeSetterUseDropdown( + 'Map', 'MapType', 'MapType')], + + // AI2: + // - Adds CustomUrl (MapType 4). + 7: "noUpgrade" }, // End Map upgraders diff --git a/appinventor/components-ios/src/ErrorMessages.swift b/appinventor/components-ios/src/ErrorMessages.swift index 3c8ff95f9f3..2976d3c4c24 100644 --- a/appinventor/components-ios/src/ErrorMessages.swift +++ b/appinventor/components-ios/src/ErrorMessages.swift @@ -320,7 +320,7 @@ import Foundation case .ERROR_INVALID_ANCHOR_HORIZONTAL: return "Invalid value %d given for AnchorHorizontal. Valid settings are 1, 2, or 3." case .ERROR_INVALID_MAP_TYPE: - return "The MapType must be 1, 2, or 3" + return "The MapType must be 1, 2, 3, or 4" // File Errors case .ERROR_CANNOT_FIND_FILE: diff --git a/appinventor/components-ios/src/Map.swift b/appinventor/components-ios/src/Map.swift index 81f3afe79ea..0def2de2893 100644 --- a/appinventor/components-ios/src/Map.swift +++ b/appinventor/components-ios/src/Map.swift @@ -28,6 +28,7 @@ enum AIMapType: Int32 { case roads = 1 case aerial = 2 case terrain = 3 + case custom = 4 } typealias CLLocationDirection = Double @@ -76,7 +77,8 @@ open class Map: ViewComponent, MKMapViewDelegate, UIGestureRecognizerDelegate, M private var _featuresState = 0 private var _boundsChangeReady: Bool = false private var _terrainOverlay: MKTileOverlay? - + private var _customUrlOverlay: MKTileOverlay? + private var _customURL = "" private var _activeOverlay: MapOverlayShape? = nil private var _lastPoint: CLLocationCoordinate2D? = nil private var _activeMarker: Marker? = nil @@ -158,6 +160,7 @@ open class Map: ViewComponent, MKMapViewDelegate, UIGestureRecognizerDelegate, M EnableZoom = true EnablePan = true MapType = 1 + CustomUrl = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" Rotation = 0.0 ScaleUnits = 1 ShowZoom = false @@ -347,7 +350,7 @@ open class Map: ViewComponent, MKMapViewDelegate, UIGestureRecognizerDelegate, M return _mapType.rawValue } set(type) { - if !(1...3 ~= type) { + if !(1...4 ~= type) { form?.dispatchErrorOccurredEvent(self, "MapType", ErrorMessage.ERROR_INVALID_MAP_TYPE.code, ErrorMessage.ERROR_INVALID_MAP_TYPE.message) return @@ -357,17 +360,38 @@ open class Map: ViewComponent, MKMapViewDelegate, UIGestureRecognizerDelegate, M switch _mapType { case .roads: removeTerrainTileRenderer() + removeCustomUrlTileRenderer() mapView.mapType = .standard case .aerial: removeTerrainTileRenderer() + removeCustomUrlTileRenderer() mapView.mapType = .satellite case .terrain: + removeCustomUrlTileRenderer() mapView.mapType = .standard // set that way zooming in too far displays a visible grid setupTerrainTileRenderer() + case .custom: + removeTerrainTileRenderer() + mapView.mapType = .standard + setupCustomUrlTileRenderer() } } } + @objc open var CustomUrl: String? { + get { + return _customURL + } + set(newUrl) { + guard let newUrl = newUrl, newUrl != CustomUrl else { + return + } + _customURL = newUrl + removeCustomUrlTileRenderer() + setupCustomUrlTileRenderer() + } + } + @objc open var ScaleUnits: Int32 { get { return _scaleUnits @@ -887,9 +911,24 @@ open class Map: ViewComponent, MKMapViewDelegate, UIGestureRecognizerDelegate, M } } + /** + * Adds a custom tile overlay that matches the CustomUrl overlay on Android + */ + private func setupCustomUrlTileRenderer() { + _customUrlOverlay = MKTileOverlay(urlTemplate: CustomUrl) + _customUrlOverlay!.canReplaceMapContent = true + mapView.insertOverlay(_customUrlOverlay!, at: 0, level: .aboveLabels) + } + + private func removeCustomUrlTileRenderer() { + if let overlay = _customUrlOverlay { + mapView.removeOverlay(overlay) + } + } + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let tileOverlay = overlay as? MKTileOverlay { - return _mapType == .terrain ? MKTileOverlayRenderer(tileOverlay: tileOverlay) : MKOverlayRenderer() + return (_mapType == .terrain || _mapType == .custom) ? MKTileOverlayRenderer(tileOverlay: tileOverlay) : MKOverlayRenderer() } else if let shape = overlay as? MapCircleOverlay { let renderer = MKCircleRenderer(circle: shape) shape.renderer = renderer diff --git a/appinventor/components-ios/src/MapFactory.swift b/appinventor/components-ios/src/MapFactory.swift index c16032bbc14..7c265cb133b 100644 --- a/appinventor/components-ios/src/MapFactory.swift +++ b/appinventor/components-ios/src/MapFactory.swift @@ -142,4 +142,5 @@ enum MapType: Int32 { case ROADS = 1 case AERIAL = 2 case TERRAIN = 3 + case CUSTOM = 4 } diff --git a/appinventor/components/src/com/google/appinventor/components/common/MapType.java b/appinventor/components/src/com/google/appinventor/components/common/MapType.java index 592a40c5f32..82efc4fd83b 100644 --- a/appinventor/components/src/com/google/appinventor/components/common/MapType.java +++ b/appinventor/components/src/com/google/appinventor/components/common/MapType.java @@ -14,7 +14,8 @@ public enum MapType implements OptionList { Road(1), Aerial(2), - Terrain(3); + Terrain(3), + Custom(4); private final Integer value; diff --git a/appinventor/components/src/com/google/appinventor/components/common/PropertyTypeConstants.java b/appinventor/components/src/com/google/appinventor/components/common/PropertyTypeConstants.java index 549d1fc93e5..ea4ba1663fd 100644 --- a/appinventor/components/src/com/google/appinventor/components/common/PropertyTypeConstants.java +++ b/appinventor/components/src/com/google/appinventor/components/common/PropertyTypeConstants.java @@ -194,6 +194,13 @@ private PropertyTypeConstants() {} */ public static final String PROPERTY_TYPE_MAP_TYPE = "map_type"; + /** + * Map custom URL template required by the Map component. + * @see + * com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidMapCustomUrlPropertyEditor + */ + public static final String PROPERTY_TYPE_MAP_CUSTOMURL = "map_customurl"; + /** * Integer values limited to the range of valid map zoom levels [1, 18]. * @see com.google.appinventor.client.editor.youngandroid.properties.YoungAndroidMapZoomPropertyEditor diff --git a/appinventor/components/src/com/google/appinventor/components/common/YaVersion.java b/appinventor/components/src/com/google/appinventor/components/common/YaVersion.java index 042efca5eb2..6f2e387cf71 100644 --- a/appinventor/components/src/com/google/appinventor/components/common/YaVersion.java +++ b/appinventor/components/src/com/google/appinventor/components/common/YaVersion.java @@ -1212,7 +1212,9 @@ private YaVersion() { // - Added ScaleUnits property // For MAP_COMPONENT_VERSION 6: // - Adds ScaleUnits and MapType dropdowns. - public static final int MAP_COMPONENT_VERSION = 6; + // For MAP_COMPONENT_VERSION 7: + // - Adds CustomUrl (MapType 4). + public static final int MAP_COMPONENT_VERSION = 7; // For MARKER_COMPONENT_VERSION 1: // - Initial Marker implementation using OpenStreetMap diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/Map.java b/appinventor/components/src/com/google/appinventor/components/runtime/Map.java index 607e8c859fd..00f3d157c97 100644 --- a/appinventor/components/src/com/google/appinventor/components/runtime/Map.java +++ b/appinventor/components/src/com/google/appinventor/components/runtime/Map.java @@ -62,7 +62,7 @@ /** * A two-dimensional container that renders map tiles in the background and allows for multiple * {@link Marker} elements to identify points on the map. Map tiles are supplied by OpenStreetMap - * contributors and the the United States Geological Survey. + * contributors and the the United States Geological Survey, or a custom basemap URL can be provided. * * The `Map` component provides three utilities for manipulating its boundaries with App Inventor. * First, a locking mechanism is provided to allow the map to be moved relative to other components @@ -80,7 +80,7 @@ androidMinSdk = 8, description = "

A two-dimensional container that renders map tiles in the background and " + "allows for multiple Marker elements to identify points on the map. Map tiles are supplied " + - "by OpenStreetMap contributors and the United States Geological Survey.

" + + "by OpenStreetMap contributors and the United States Geological Survey, or a custom basemap URL can be provided.

" + "

The Map component provides three utilities for manipulating its boundaries within App " + "Inventor. First, a locking mechanism is provided to allow the map to be moved relative to " + "other components on the Screen. Second, when unlocked, the user can pan the Map to any " + @@ -123,6 +123,7 @@ public Map(final ComponentContainer container) { EnableZoom(true); EnablePan(true); MapTypeAbstract(MapType.Road); + CustomUrl("https://tile.openstreetmap.org/{z}/{x}/{y}.png"); ShowCompass(false); LocationSensor(new LocationSensor(container.$form(), false)); ShowUser(false); @@ -301,6 +302,7 @@ public float Rotation() { * 1. Roads * 2. Aerial * 3. Terrain + * 4. Custom * * @param type Integer identifying the tile set to use for the map's base layer. */ @@ -321,6 +323,7 @@ public void MapType(@Options(MapType.class) int type) { * 1. Roads * 2. Aerial * 3. Terrain + * 4. Custom * * **Note:** Road layers are provided by OpenStreetMap and aerial and terrain layers are * provided by the U.S. Geological Survey. @@ -329,7 +332,7 @@ public void MapType(@Options(MapType.class) int type) { */ @SimpleProperty(category = PropertyCategory.APPEARANCE, description = "The type of tile layer to use as the base of the map. Valid values " + - "are: 1 (Roads), 2 (Aerial), 3 (Terrain)") + "are: 1 (Roads), 2 (Aerial), 3 (Terrain), 4 (Custom)") public @Options(MapType.class) int MapType() { return MapTypeAbstract().toUnderlyingValue(); } @@ -350,6 +353,30 @@ public void MapTypeAbstract(MapType type) { mapController.setMapTypeAbstract(type); } + /** + * @return Returns the custom URL of the base tile layer in use by the map. + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_MAP_CUSTOMURL, + defaultValue = "https://tile.openstreetmap.org/{z}/{x}/{y}.png") + @SimpleProperty(category = PropertyCategory.ADVANCED, + description = "The URL of the custom tile layer to use as the base of the map. Valid URLs " + + "should include {z}, {x} and {y} placeholders and any authentication required.

" + + "e.g. https://tile.openstreetmap.org/{z}/{x}/{y}.png

" + + "or https://example.com/geoserver/gwc/service/tms/1.0.0/workspace:layername@EPSG:3857@jpeg/{z}/{x}/{y}.jpeg?flipY=true&authkey=123") + public String CustomUrl() { + return mapController.getCustomUrl(); + } + + /** + * Update the custom URL of the base tile layer in use by the map. + * e.g. https://tile.openstreetmap.org/{z}/{x}/{y}.png + * e.g. https://example.com/geoserver/gwc/service/tms/1.0.0/workspace:layername@EPSG:3857@jpeg/{z}/{x}/{y}.jpeg?flipY=true&authkey=123 + */ + @SimpleProperty(category = PropertyCategory.ADVANCED) + public void CustomUrl(String url) { + mapController.setCustomUrl(url); + } + /** * Show a compass on the map. If the device provides a digital compass, orientation changes will * be used to rotate the compass icon. diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/util/DummyMapController.java b/appinventor/components/src/com/google/appinventor/components/runtime/util/DummyMapController.java index 5fe04b24e2e..b1f2becad13 100644 --- a/appinventor/components/src/com/google/appinventor/components/runtime/util/DummyMapController.java +++ b/appinventor/components/src/com/google/appinventor/components/runtime/util/DummyMapController.java @@ -65,6 +65,14 @@ public MapFactory.MapType getMapType() { throw new UnsupportedOperationException(); } + public void setCustomUrl(String url) { + throw new UnsupportedOperationException(); + } + + public String getCustomUrl() { + throw new UnsupportedOperationException(); + } + public void setMapTypeAbstract(MapType type) { throw new UnsupportedOperationException(); } diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/util/MapFactory.java b/appinventor/components/src/com/google/appinventor/components/runtime/util/MapFactory.java index db63fc8ff57..1dc170c71cb 100644 --- a/appinventor/components/src/com/google/appinventor/components/runtime/util/MapFactory.java +++ b/appinventor/components/src/com/google/appinventor/components/runtime/util/MapFactory.java @@ -201,6 +201,20 @@ public interface MapController { */ void setMapTypeAbstract(com.google.appinventor.components.common.MapType type); + /** + * Get the customUrl of the map being used. + * + * @return the customUrl of the map's active tile layer + */ + String getCustomUrl(); + + /** + * Set the customUrl of the map being used. + * + * @param Set the new map customUrl for the map + */ + void setCustomUrl(String url); + /** * Set whether the compass is displayed on the map. * @@ -1313,6 +1327,7 @@ public interface MapMarker extends MapFeature, HasFill, HasStroke { * prior to official release. Some apps developed using earlier versions of Maps on test servers * may reference this property, but it may be removed in a future version of App Inventor. */ + @Deprecated @SuppressWarnings("squid:S00100") void ShowShadow(boolean show); @@ -1321,6 +1336,7 @@ public interface MapMarker extends MapFeature, HasFill, HasStroke { * @return true if the marker should have a shadow, otherwise false. * @deprecated See the deprecation message for {@link #ShowShadow(boolean)}. */ + @Deprecated @SuppressWarnings("squid:S00100") boolean ShowShadow(); @@ -1577,7 +1593,12 @@ public enum MapType { /** * Terrain tile layer. */ - TERRAIN + TERRAIN, + + /** + * Custom tile layer. + */ + CUSTOM } /** diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/util/NativeOpenStreetMapController.java b/appinventor/components/src/com/google/appinventor/components/runtime/util/NativeOpenStreetMapController.java index 404dc4751f1..51bb091f6ea 100644 --- a/appinventor/components/src/com/google/appinventor/components/runtime/util/NativeOpenStreetMapController.java +++ b/appinventor/components/src/com/google/appinventor/components/runtime/util/NativeOpenStreetMapController.java @@ -68,9 +68,11 @@ import org.osmdroid.tileprovider.modules.IFilesystemCache; import org.osmdroid.tileprovider.modules.MapTileFilesystemProvider; import org.osmdroid.tileprovider.modules.MapTileSqlCacheProvider; +import org.osmdroid.tileprovider.modules.SqlTileWriter; import org.osmdroid.tileprovider.modules.TileWriter; import org.osmdroid.tileprovider.tilesource.ITileSource; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.tileprovider.tilesource.XYTileSource; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; @@ -109,6 +111,7 @@ class NativeOpenStreetMapController implements MapController, MapListener { private RelativeLayout containerView; private MapView view; private MapType tileType; + private String customUrl; private boolean zoomEnabled; private boolean zoomControlEnabled; private CompassOverlay compass = null; @@ -416,6 +419,39 @@ public MapFactory.MapType getMapType() { return MapFactory.MapType.values()[tileType.toUnderlyingValue()]; } + @Override + public String getCustomUrl() { + return customUrl; + } + + @Override + public void setCustomUrl(String url) { + if (!url.equals(this.customUrl)) { + // Clear any cached tiles if the custom URL changes + // TODO: even though isCleared returns true, the cache is still not empty. Debug further. + // Only solution is to use Android's Storage > Clear cache, which does work. + SqlTileWriter sqlTileWriter = new SqlTileWriter(); + boolean isCleared = sqlTileWriter.purgeCache(); //view.getTileProvider().getTileSource().name()); //"Custom"); + // setRotationEnabled(isCleared); + view.getTileProvider().clearTileCache(); + } + this.customUrl = url; + view.setTileSource(getCustomTileSource()); + } + + private ITileSource getCustomTileSource() { + final ITileSource tileSource = new XYTileSource("Custom", 1, 20, 256, "", + new String[] { customUrl }) { + @Override + public String getTileURLString(MapTile aTile) { + return getBaseUrl().replace("{z}", String.valueOf(aTile.getZoomLevel())) + .replace("{x}", String.valueOf(aTile.getX())) + .replace("{y}", String.valueOf(aTile.getY())); + } + }; + return tileSource; + } + @Override public void setMapTypeAbstract(MapType type) { tileType = type; @@ -429,6 +465,9 @@ public void setMapTypeAbstract(MapType type) { case Terrain: view.setTileSource(TileSourceFactory.USGS_TOPO); break; + case Custom: + view.setTileSource(getCustomTileSource()); + break; } } diff --git a/appinventor/docs/markdown/reference/components/maps.md b/appinventor/docs/markdown/reference/components/maps.md index e8c24fd0fa0..11d52df825f 100644 --- a/appinventor/docs/markdown/reference/components/maps.md +++ b/appinventor/docs/markdown/reference/components/maps.md @@ -363,7 +363,7 @@ A `FeatureCollection` groups one or more map features together. Any events that A two-dimensional container that renders map tiles in the background and allows for multiple [`Marker`](#Marker) elements to identify points on the map. Map tiles are supplied by OpenStreetMap - contributors and the the United States Geological Survey. + contributors and the the United States Geological Survey, or a custom basemap URL can be provided. The `Map` component provides three utilities for manipulating its boundaries with App Inventor. First, a locking mechanism is provided to allow the map to be moved relative to other components @@ -393,6 +393,11 @@ A two-dimensional container that renders map tiles in the background and allows [`PanTo`](#Map.PanTo) with numerical latitude and longitude rather than convert to the string representation for use with this property. +{:id="Map.CustomUrl" .text} *CustomUrl* +: Update the custom URL of the base tile layer in use by the map. + e.g. https://tile.openstreetmap.org/{z}/{x}/{y}.png + e.g. https://example.com/geoserver/gwc/service/tms/1.0.0/workspace:layername + {:id="Map.EnablePan" .boolean} *EnablePan* : Enables or disables the ability of the user to move the Map. @@ -433,6 +438,7 @@ A two-dimensional container that renders map tiles in the background and allows 1. Roads 2. Aerial 3. Terrain + 4. Custom **Note:** Road layers are provided by OpenStreetMap and aerial and terrain layers are provided by the U.S. Geological Survey.