Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom url for map component #3160

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5252,6 +5252,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();
Expand Down Expand Up @@ -5320,6 +5328,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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -484,7 +491,10 @@ private native void initPanel()/*-{
attribution: 'Satellite imagery &copy; <a href="http://mapquest.com">USGS</a>'}),
L.tileLayer('//basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
{minZoom: 0, maxZoom: 15,
attribution: 'Map data &copy; <a href="http://www.usgs.gov">USGS</a>'})
attribution: 'Map data &copy; <a href="http://www.usgs.gov">USGS</a>'}),
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 =
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,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;
Expand Down Expand Up @@ -263,6 +264,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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,10 @@ private static int upgradeMapProperties(Map<String, JSONValue> componentProperti
// Adds ScaleUnits and MapType dropdowns.
srcCompVersion = 6;
}
if (srcCompVersion < 7) {
// Adds CustomUrl (MapType 4).
srcCompVersion = 7;
}
return srcCompVersion;
}

Expand Down
10 changes: 7 additions & 3 deletions appinventor/blocklyeditor/src/versioning.js
Original file line number Diff line number Diff line change
Expand Up @@ -2489,9 +2489,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

Expand Down
2 changes: 1 addition & 1 deletion appinventor/components-ios/src/ErrorMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,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:
Expand Down
43 changes: 41 additions & 2 deletions appinventor/components-ios/src/Map.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum AIMapType: Int32 {
case roads = 1
case aerial = 2
case terrain = 3
case custom = 4
}

typealias CLLocationDirection = Double
Expand Down Expand Up @@ -76,6 +77,7 @@ 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 _activeOverlay: MapOverlayShape? = nil
private var _lastPoint: CLLocationCoordinate2D? = nil
Expand Down Expand Up @@ -158,6 +160,7 @@ open class Map: ViewComponent, MKMapViewDelegate, UIGestureRecognizerDelegate, M
EnableZoom = true
EnablePan = true
MapType = 1
CustomUrl = ""
Rotation = 0.0
ScaleUnits = 1
ShowZoom = false
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions appinventor/components-ios/src/MapFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,5 @@ enum MapType: Int32 {
case ROADS = 1
case AERIAL = 2
case TERRAIN = 3
case CUSTOM = 4
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
public enum MapType implements OptionList<Integer> {
Road(1),
Aerial(2),
Terrain(3);
Terrain(3),
Custom(4);

private final Integer value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1192,7 +1192,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
Expand Down
Loading