Using getTileURL in Android Application with GeoServer - android

We are just starting to work with Google Maps on Android and have a GeoServer set up to provide tiles which we would like to add as overlay on the map. So far, I have followed a few tutorials and references to get started.
For getting MyLocation
Setting up WMS on Android
WMS Reference
The problem: While the url that I am generating in the getTileUrl function in the TileProviderFactory does indeed return a png image when I set a breakpoint and copy and paste the url into a browser, it does not load onto the map as an overlay on the Android device. There are no errors being thrown from what I can see and after reading this I am not sure if there will be any as they have indicated that the errors are being muted.
What I am wondering is if you can see any immediate issues in my code or have any suggestions for debugging where I will be able to tell if the application is actually communicating with my GeoServer to retrieve the image or not. I've looked at the log on the GeoServer and it seems as though only my browser requests are going through and it's not receiving any requests from Android (it's a bit difficult to tell because we have other applications using the server as well). The Android phone is connected by both wifi and cell and has gps enabled. As a last resort I have tried changing the tile overlay zIndex and setting it to visible but this didn't seem to make any difference.
EDIT: Android device is definitely NOT communicating with GeoServer at this point.
EDIT 2: Able to load static images from websites
(like this) as overlays and found that I am getting the following exception on testing out an HTTP Request to the formed URL:
W/System.err(10601): java.net.SocketException: The operation timed out
W/System.err(10601): at org.apache.harmony.luni.platform.OSNetworkSystem
.connectStreamWithTimeoutSocketImpl(Native Method)
W/System.err(10601): at org.apache.harmony.luni.net.PlainSocketImpl
.connect(PlainSocketImpl.java:244)
W/System.err(10601): at org.apache.harmony.luni.net.PlainSocketImpl
.connect(PlainSocketImpl.java:533)
W/System.err(10601): at java.net.Socket
.connect(Socket.java:1074)
W/System.err(10601): at org.apache.http.conn.scheme.PlainSocketFactory
.connectSocket(PlainSocketFactory.java:119)
Thanks.
MapTestActivity
public class MapTestActivity extends FragmentActivity
implements LocationListener, LocationSource{
private GoogleMap mMap;
private OnLocationChangedListener mListener;
private LocationManager locationManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map_test);
setupLocationManager();
setupMapIfNeeded();
}
private void setupLocationManager() {
this.locationManager =
(LocationManager) getSystemService(LOCATION_SERVICE);
if (locationManager != null) {
boolean gpsIsEnabled = locationManager.isProviderEnabled(
LocationManager.GPS_PROVIDER);
boolean networkIsEnabled = locationManager.isProviderEnabled(
LocationManager.NETWORK_PROVIDER);
if(gpsIsEnabled) {
this.locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 5000L, 10F, this);
}
else if(networkIsEnabled) {
this.locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, 5000L, 10F, this);
}
else {
//Show an error dialog that GPS is disabled...
}
}
else {
// Show some generic error dialog because
// something must have gone wrong with location manager.
}
}
private void setupMapIfNeeded() {
// Do a null check to confirm that we have not already instantiated the
// map.
if (mMap == null) {
// Try to obtain the map from the SupportMapFragment.
mMap = ((SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map)).getMap();
// Check if we were successful in obtaining the map.
if (mMap != null) {
setUpMap();
}
mMap.setLocationSource(this);
}
}
private void setUpMap() {
// TODO Auto-generated method stub
mMap.setMyLocationEnabled(true);
TileProvider geoServerTileProvider = TileProviderFactory
.getGeoServerTileProvider();
TileOverlay geoServerTileOverlay = mMap.addTileOverlay(
new TileOverlayOptions()
.tileProvider(geoServerTileProvider)
.zIndex(10000)
.visible(true));
}
// Non-relevant listener methods removed
}
TileProviderFactory
public class TileProviderFactory {
public static GeoServerTileProvider getGeoServerTileProvider() {
String baseURL = "mytileserver.com";
String version = "1.3.0";
String request = "GetMap";
String format = "image/png";
String srs = "EPSG:900913";
String service = "WMS";
String width = "256";
String height = "256";
String styles = "";
String layers = "wtx:road_hazards";
final String URL_STRING = baseURL +
"&LAYERS=" + layers +
"&VERSION=" + version +
"&SERVICE=" + service +
"&REQUEST=" + request +
"&TRANSPARENT=TRUE&STYLES=" + styles +
"&FORMAT=" + format +
"&SRS=" + srs +
"&BBOX=%f,%f,%f,%f" +
"&WIDTH=" + width +
"&HEIGHT=" + height;
GeoServerTileProvider tileProvider =
new GeoServerTileProvider(256,256) {
#Override
public synchronized URL getTileUrl(int x, int y, int zoom) {
try {
double[] bbox = getBoundingBox(x, y, zoom);
String s = String.format(Locale.US, URL_STRING, bbox[MINX],
bbox[MINY], bbox[MAXX], bbox[MAXY]);
Log.d("GeoServerTileURL", s);
URL url = null;
try {
url = new URL(s);
}
catch (MalformedURLException e) {
throw new AssertionError(e);
}
return url;
}
catch (RuntimeException e) {
Log.d("GeoServerTileException",
"getTile x=" + x + ", y=" + y +
", zoomLevel=" + zoom +
" raised an exception", e);
throw e;
}
}
};
return tileProvider;
}
}
GeoServerTileProvider
public abstract class GeoServerTileProvider extends UrlTileProvider{
// Web Mercator n/w corner of the map.
private static final double[] TILE_ORIGIN =
{-20037508.34789244, 20037508.34789244};
//array indexes for that data
private static final int ORIG_X = 0;
private static final int ORIG_Y = 1; // "
// Size of square world map in meters, using WebMerc projection.
private static final double MAP_SIZE = 20037508.34789244 * 2;
// array indexes for array to hold bounding boxes.
protected static final int MINX = 0;
protected static final int MINY = 1;
protected static final int MAXX = 2;
protected static final int MAXY = 3;
public GeoServerTileProvider(int width, int height) {
super(width, height);
}
// Return a web Mercator bounding box given tile x/y indexes and a zoom
// level.
protected double[] getBoundingBox(int x, int y, int zoom) {
double tileSize = MAP_SIZE / Math.pow(2, zoom);
double minx = TILE_ORIGIN[ORIG_X] + x * tileSize;
double maxx = TILE_ORIGIN[ORIG_X] + (x+1) * tileSize;
double miny = TILE_ORIGIN[ORIG_Y] - (y+1) * tileSize;
double maxy = TILE_ORIGIN[ORIG_Y] - y * tileSize;
double[] bbox = new double[4];
bbox[MINX] = minx;
bbox[MINY] = miny;
bbox[MAXX] = maxx;
bbox[MAXY] = maxy;
return bbox;
}
}

This turned out to be a network issue and completely unrelated to my implementation which is "correct". I guess this question will serve well as an example for others who are getting started with Android + GeoServer implementation so I will leave it up.

Related

Android change google map tile with custom tiles

Is it possible to change google map API v3 standart map to my own custom map coming from url? I know that OSMdroid provide it but i want work with google map API. Is it possible?
it is indeed possible by using WMS services (if you don't know what they are, please google it).
Here is some code you can use:
The WMSTile provider is used by GoogleMapsAPI to set the map provider:
public abstract class WMSTileProvider extends UrlTileProvider {
// Web Mercator n/w corner of the map.
private static final double[] TILE_ORIGIN = { -20037508.34789244, 20037508.34789244 };
// array indexes for that data
private static final int ORIG_X = 0;
private static final int ORIG_Y = 1; // "
// Size of square world map in meters, using WebMerc projection.
private static final double MAP_SIZE = 20037508.34789244 * 2;
// array indexes for array to hold bounding boxes.
protected static final int MINX = 0;
protected static final int MAXX = 1;
protected static final int MINY = 2;
protected static final int MAXY = 3;
// cql filters
private String cqlString = "";
// Construct with tile size in pixels, normally 256, see parent class.
public WMSTileProvider(int x, int y) {
super(x, y);
}
#SuppressWarnings("deprecation")
protected String getCql() {
try {
return URLEncoder.encode(cqlString, Charset.defaultCharset().name());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return URLEncoder.encode(cqlString);
}
}
public void setCql(String c) {
cqlString = c;
}
// Return a web Mercator bounding box given tile x/y indexes and a zoom
// level.
protected double[] getBoundingBox(int x, int y, int zoom) {
double tileSize = MAP_SIZE / Math.pow(2, zoom);
double minx = TILE_ORIGIN[ORIG_X] + x * tileSize;
double maxx = TILE_ORIGIN[ORIG_X] + (x + 1) * tileSize;
double miny = TILE_ORIGIN[ORIG_Y] - (y + 1) * tileSize;
double maxy = TILE_ORIGIN[ORIG_Y] - y * tileSize;
double[] bbox = new double[4];
bbox[MINX] = minx;
bbox[MINY] = miny;
bbox[MAXX] = maxx;
bbox[MAXY] = maxy;
return bbox;
}
}
And you can instantiate a custom one from your URL in such a way:
public static WMSTileProvider getWMSTileProviderByName(String layerName) {
final String OSGEO_WMS = "http://YOURWMSSERVERURL?"
+ "LAYERS=" + layerName
+ "&FORMAT=image/png8&"
+ "PROJECTION=EPSG:3857&"
+ "TILEORIGIN=lon=-20037508.34,lat=-20037508.34&"
+ "TILESIZE=w=256,h=256"
+ "&MAXEXTENT=-20037508.34,-20037508.34,20037508.34,20037508.34&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&SRS=EPSG:3857"
+ "&BBOX=%f,%f,%f,%f&WIDTH=256&HEIGHT=256";
return new WMSTileProvider(256, 256) {
#Override
public synchronized URL getTileUrl(int x, int y, int zoom) {
final double[] bbox = getBoundingBox(x, y, zoom);
String s = String.format(Locale.US, OSGEO_WMS, bbox[MINX], bbox[MINY], bbox[MAXX], bbox[MAXY]);
try {
return new URL(s);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
};
}
Add to your map:
TileProvider tileProvider = getWMSTileProviderByName("MYLAYERNAME");
TileOverlay tileOverlay = myMap.addTileOverlay(new TileOverlayOptions()
.tileProvider(tileProvider));
You should also set the map type to MAP_NONE when using a custom tile provider (if it is not transparent), so you avoid to load gmaps tiles that are hidden behind your custom map.

WMS On Android tile loading issue

I'm using Google Maps v2 to display tiles from a wms. I referred this
site. Problem in loading tiles, they are loading multiple times i dont konw y? Can any1 help me.
Here is my code
package com.example.testgooglemaps;
import android.app.Activity;
import android.os.Bundle;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.android.gms.maps.model.TileProvider;
public class Lanch extends Activity {
// Google Map
private GoogleMap googleMap;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lanch);
try {
// Loading map
initilizeMap();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* function to load map. If map is not created it will create it for you
* */
private void initilizeMap() {
if (googleMap == null) {
googleMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.map)).getMap();
// check if map is created successfully or not
if (googleMap != null) {
setUpMap();
}
}
}
#Override
protected void onResume() {
super.onResume();
initilizeMap();
}
private void setUpMap() {
TileProvider wmsTileProvider = TileProviderFactory.getOsgeoWmsTileProvider();
googleMap.addTileOverlay(new TileOverlayOptions().tileProvider(wmsTileProvider));
// to satellite so we can see the WMS overlay.
googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
}
}
TileProvider class...
package com.example.testgooglemaps;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import android.util.Log;
public class TileProviderFactory {
public static WMSTileProvider getOsgeoWmsTileProvider() {
final String OSGEO_WMS = "http://localhost/geoserver/magnamaps/wms?service=WMS&version=1.1.0&request=GetMap&layers=magnamaps:bang_apartments&styles=&bbox=%f,%f,%f,%f&width=256&height=256&crs=EPSG:4326&format=image/png&transparent=true";
WMSTileProvider tileProvider = new WMSTileProvider(256, 256) {
#Override
public synchronized URL getTileUrl(int x, int y, int zoom) {
double[] bbox = getBoundingBox(x, y, zoom);
String s = String.format(Locale.US, OSGEO_WMS, bbox[MINX], bbox[MINY], bbox[MAXX], bbox[MAXY]);
Log.d("WMSDEMO", s);
URL url = null;
try {
url = new URL(s);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return url;
}
};
return tileProvider;
}
}
WMSTileProvider class...
package com.example.testgooglemaps;
import java.net.URLEncoder;
import com.google.android.gms.maps.model.UrlTileProvider;
public abstract class WMSTileProvider extends UrlTileProvider {
// Web Mercator n/w corner of the map.
private static final double[] TILE_ORIGIN = { -20037508.34789244, 20037508.34789244 };
// array indexes for that data
private static final int ORIG_X = 0;
private static final int ORIG_Y = 1; // "
// Size of square world map in meters, using WebMerc projection.
private static final double MAP_SIZE = 20037508.34789244 * 2;
// array indexes for array to hold bounding boxes.
protected static final int MINX = 0;
protected static final int MAXX = 1;
protected static final int MINY = 2;
protected static final int MAXY = 3;
// cql filters
private String cqlString = "";
// Construct with tile size in pixels, normally 256, see parent class.
public WMSTileProvider(int x, int y) {
super(x, y);
}
protected String getCql() {
return URLEncoder.encode(cqlString);
}
public void setCql(String c) {
cqlString = c;
}
// Return a web Mercator bounding box given tile x/y indexes and a zoom
// level.
protected double[] getBoundingBox(int x, int y, int zoom) {
double tileSize = MAP_SIZE / Math.pow(2, zoom);
double minx = TILE_ORIGIN[ORIG_X] + x * tileSize;
double maxx = TILE_ORIGIN[ORIG_X] + (x + 1) * tileSize;
double miny = TILE_ORIGIN[ORIG_Y] - (y + 1) * tileSize;
double maxy = TILE_ORIGIN[ORIG_Y] - y * tileSize;
double[] bbox = new double[4];
bbox[MINX] = minx;
bbox[MINY] = miny;
bbox[MAXX] = maxx;
bbox[MAXY] = maxy;
return bbox;
}
}
EDIT : While initializing map itself, zoom level is set to 3, Inside this method getTileUrl(int x, int y, int zoom)
In WMSTileProvider.getBoundingBox you are computing the bounding box in units of the Web Mercator projection, which are meters. In your OSGEO_WMS URL string, you are specifying that the bbox units are in EPSG:4326 (degrees). It's likely that the query for each tile is incorrect as a result.
See the WMS reference for bbox and srs:
bbox: Bounding box for map extent. Value is minx,miny,maxx,maxy in
units of the SRS.
Try replacing your srs value with EPSG:3857 (WebMercator)

How to calculate distance from different markers in a map and then pick up the least one

I have to get distance from different markers on the map to the current location of the device and the pick up the shortest one. I have the lat and long for the markers and the current location lat and long can be fetched dynamically.
Suppose I have 5 markers on the map, Bangalore (Lat : 12.971599, Long : 77.594563), Delhi (Lat : 28.635308, Long : 77.224960), Mumbai (Lat : 19.075984, Long : 72.877656), Chennai (Lat : 13.052414, Long : 80.250825), Kolkata (Lat : 22.572646, Long : 88.363895).
Now suppose the user is standing somewhere near Hyderabad (Lat : 17.385044, Long : 78.486671). When the user clicks the button, the app should calculate distance from each marker and pick up and return the shortest one, that will be Bangalore here.
There is a way possible to do it with help of local databases. Can anyone help on that please.?
Can anyone suggest me a nice way to do this, or come up with a good code if you please can. Thanx in advance.
from your comment I see that you expect a maximum of 70-80 locations.
This is not much.
You can simply do a brute force search over all markers and take the minimum.
Iterate over all markers, and search min distance:
List<Marker> markers = createMarkers(); // returns an ArrayList<Markers> from your data source
int minIndex = -1;
double minDist = 1E38; // initialize with a huge value that will be overwritten
int size = markers.size();
for (int i = 0; i < size; i++) {
Marker marker = markers.get(i);
double curDistance = calcDistance(curLatitude, curLongitude, marker.latitude, marker.longitude);
if (curDistance < minDist) {
minDist = curDistance; // update neares
minIndex = i; // store index of nearest marker in minIndex
}
}
if (minIndex >= 0) {
// now nearest maker found:
Marker nearestMarker = markers.get(minIndex);
// TODO do something with nearesr marker
} else {
// list of markers was empty
}
For calcDistance, use the distance calculation method provided by android. (e.g Location.distanceTo() )
For 70-80 markers there is no need to make it faster and much more complex.
If you have some thousands points then it is worth to invest in a faster solution (using a spatial index, and an own distance calculation which avoids the sqrt calc).
Just print out the current time in milli seconds at the begin and at the end of the nearest maker search, and you will see, that it is fast enough.
If you want to find the shortest one not list the closest and you want the process to scale to a large amount of locations, you can do some filtering before you calculate distances and you can simplify the formula to speed it up as you don't care about actual distances (i.e. remove the multiplication by the radius of the earth).
Filtering algorithm, looping through each location :
Calculate the difference in lat and long.
If both differences are larger then a previously processed pair, discard it.
Calculate distance, keep smallest.
You can further help the algorithm by feeding it with what might be close locations first. For example if you know one of the points is in the same country or state.
Here is some Python code to do that, use it as pseudocode for your solution :
locations = {
'Bangalore' : (12.971599, 77.594563),
'Delhi' : (28.635308, 77.224960),
'Mumbai' : (19.075984, 72.877656),
'Chennai' : (13.052414, 80.250825),
'Kolkata' : (22.572646, 88.363895)
}
from math import sin, cos, atan2, sqrt
EARTH_RADIUS = 6373 # km
def distance(a, b): # pass tuples
(lat1, lon1) = a
(lat2, lon2) = b
dlon = lon2 - lon1
dlat = lat2 - lat1
a = (sin(dlat/2))**2 + cos(lat1) * cos(lat2) * (sin(dlon/2))**2
c = 2 * atan2( sqrt(a), sqrt(1-a) )
return EARTH_RADIUS * c
current = (17.385044, 78.486671) # current lat & lng
closest = None
closest_name = None
for name, cordinates in locations.iteritems():
d = distance(current, cordinates)
if closest is None or d < closest:
closest = d
closest_name = name
print "~%dkm (%s)" % (distance(current, cordinates), name)
print "\nClosest location is %s, %d km away." % (closest_name, closest)
Output :
~5700km (Kolkata)
~13219km (Chennai)
~12159km (Bangalore)
~7928km (Delhi)
~10921km (Mumbai)
Closest location is Kolkata, 5700 km away.
How about looping over all markers and checking the distance using Location.distanceBetween? There is no magic involved ;)
List<Marker> markers;
LatLng currentPosition;
float minDistance = Float.MAX_VALUE;
Marker closest = null;
float[] currentDistance = new float[1];
for (Marker marker : markers) {
LatLng markerPosition = marker.getPosition();
Location.distanceBetween(currentPosition.latitude, currentPosition.longitude, markerPosition.latitude, markerPosition.longitude, currentDistance);
if (minDistance > currentDistance[0]) {
minDistance = currentDistance[0];
closest = marker;
}
}
Although there has already been posted some answer, I thought I would present my implementation in java. This has been used with 4000+ markers wrapped in an AsyncTask and has been working with no problems.
First, the logic to calculate distance (assuming you only have the markers and not Location objects, as those gives the possibility to do loc1.distanceTo(loc2)):
private float distBetween(LatLng pos1, LatLng pos2) {
return distBetween(pos1.latitude, pos1.longitude, pos2.latitude,
pos2.longitude);
}
/** distance in meters **/
private float distBetween(double lat1, double lng1, double lat2, double lng2) {
double earthRadius = 3958.75;
double dLat = Math.toRadians(lat2 - lat1);
double dLng = Math.toRadians(lng2 - lng1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ Math.cos(Math.toRadians(lat1))
* Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2)
* Math.sin(dLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
double dist = earthRadius * c;
int meterConversion = 1609;
return (float) (dist * meterConversion);
}
Next, the code for selecting the nearest marker:
private Marker getNearestMarker(List<Marker> markers,
LatLng origin) {
Marker nearestMarker = null;
double lowestDistance = Double.MAX_VALUE;
if (markers != null) {
for (Marker marker : markers) {
double dist = distBetween(origin, marker.getPosition());
if (dist < lowestDistance) {
nearestMarker = marker;
lowestDistance = dist;
}
}
}
return nearestMarker;
}
Perhaps not relevant for your use case but I use the algorithm to select the nearest markers based on a predefined distance. This way I weed out a lot of unnecessary markers:
private List<Marker> getSurroundingMarkers(List<Marker> markers,
LatLng origin, int maxDistanceMeters) {
List<Marker> surroundingMarkers = null;
if (markers != null) {
surroundingMarkers = new ArrayList<Marker>();
for (Marker marker : markers) {
double dist = distBetween(origin, marker.getPosition());
if (dist < maxDistanceMeters) {
surroundingMarkers.add(marker);
}
}
}
return surroundingMarkers;
}
Hope this helps you
This code could help you getting the distances: https://github.com/BeyondAR/beyondar/blob/master/android/BeyondAR_Framework/src/com/beyondar/android/util/math/Distance.java
Here is my implementation of a so called KDTree, consisting of 3 classes: KDTree, KDTNode and KDTResult.
What you finally need is to create the KDTree using KDTree.createTree(), which returns the rootNode of the tree and gets all your fixed points passed in.
Then use KDTree.findNearestWp() to find the nearest Waypoint to the given location.
KDTree:
public class KDTree {
private Comparator<LatLng> latComparator = new LatLonComparator(true);
private Comparator<LatLng> lonComparator = new LatLonComparator(false);;
/**
* Create a KDTree from a list of Destinations. Returns the root-node of the
* tree.
*/
public KDTNode createTree(List<LatLng> recList) {
return createTreeRecursive(0, recList);
}
/**
* Traverse the tree and find the nearest WP.
*
* #param root
* #param wp
* #return
*/
static public LatLng findNearestWp(KDTNode root, LatLng wp) {
KDTResult result = new KDTResult();
findNearestWpRecursive(root, wp, result);
return result.nearestDest;
}
private static void findNearestWpRecursive(KDTNode node, LatLng wp,
KDTResult result) {
double lat = wp.latitude;
double lon = wp.longitude;
/* If a leaf node, calculate distance and return. */
if (node.isLeaf) {
LatLng dest = node.wp;
double latDiff = dest.latitude - lat;
double lonDiff = dest.longitude - lon;
double squareDist = latDiff * latDiff + lonDiff * lonDiff;
// Replace a previously found nearestDest only if the new one is
// nearer.
if (result.nearestDest == null
|| result.squareDistance > squareDist) {
result.nearestDest = dest;
result.squareDistance = squareDist;
}
return;
}
boolean devidedByLat = node.depth % 2 == 0;
boolean goLeft;
/* Check whether left or right is more promising. */
if (devidedByLat) {
goLeft = lat < node.splitValue;
} else {
goLeft = lon < node.splitValue;
}
KDTNode child = goLeft ? node.left : node.right;
findNearestWpRecursive(child, wp, result);
/*
* Check whether result needs to be checked also against the less
* promising side.
*/
if (result.squareDistance > node.minSquareDistance) {
KDTNode otherChild = goLeft ? node.right : node.left;
findNearestWpRecursive(otherChild, wp, result);
}
}
private KDTNode createTreeRecursive(int depth, List<LatLng> recList) {
KDTNode node = new KDTNode();
node.depth = depth;
if (recList.size() == 1) {
// Leafnode found
node.isLeaf = true;
node.wp = recList.get(0);
return node;
}
boolean divideByLat = node.depth % 2 == 0;
sortRecListByDimension(recList, divideByLat);
List<LatLng> leftList = getHalfOf(recList, true);
List<LatLng> rightList = getHalfOf(recList, false);
// Get split point and distance to last left and first right point.
LatLng lastLeft = leftList.get(leftList.size() - 1);
LatLng firstRight = rightList.get(0);
double minDistanceToSplitValue;
double splitValue;
if (divideByLat) {
minDistanceToSplitValue = (firstRight.latitude - lastLeft.latitude) / 2;
splitValue = lastLeft.latitude + Math.abs(minDistanceToSplitValue);
} else {
minDistanceToSplitValue = (firstRight.longitude - lastLeft.longitude) / 2;
splitValue = lastLeft.longitude + Math.abs(minDistanceToSplitValue);
}
node.splitValue = splitValue;
node.minSquareDistance = minDistanceToSplitValue
* minDistanceToSplitValue;
/** Call next level */
depth++;
node.left = createTreeRecursive(depth, leftList);
node.right = createTreeRecursive(depth, rightList);
return node;
}
/**
* Return a sublist representing the left or right half of a List. Size of
* recList must be at least 2 !
*
* IMPORTANT !!!!! Note: The original list must not be modified after
* extracting this sublist, as the returned subList is still backed by the
* original list.
*/
List<LatLng> getHalfOf(List<LatLng> recList, boolean leftHalf) {
int mid = recList.size() / 2;
if (leftHalf) {
return recList.subList(0, mid);
} else {
return recList.subList(mid, recList.size());
}
}
private void sortRecListByDimension(List<LatLng> recList, boolean sortByLat) {
Comparator<LatLng> comparator = sortByLat ? latComparator
: lonComparator;
Collections.sort(recList, comparator);
}
class LatLonComparator implements Comparator<LatLng> {
private boolean byLat;
public LatLonComparator(boolean sortByLat) {
this.byLat = sortByLat;
}
#Override
public int compare(LatLng lhs, LatLng rhs) {
double diff;
if (byLat) {
diff = lhs.latitude - rhs.latitude;
} else {
diff = lhs.longitude - rhs.longitude;
}
if (diff > 0) {
return 1;
} else if (diff < 0) {
return -1;
} else {
return 0;
}
}
}
}
KDTNode:
/** Node of the KDTree */
public class KDTNode {
KDTNode left;
KDTNode right;
boolean isLeaf;
/** latitude or longitude of the nodes division line. */
double splitValue;
/** Distance between division line and first point. */
double minSquareDistance;
/**
* Depth of the node in the tree. An even depth devides the tree in the
* latitude-axis, an odd depth devides the tree in the longitude-axis.
*/
int depth;
/** The Waypoint in case the node is a leaf node. */
LatLng wp;
}
KDTResult:
/** Holds the result of a tree traversal. */
public class KDTResult {
LatLng nearestDest;
// I use the square of the distance to avoid square-root operations.
double squareDistance;
}
Please note, that I am using a simplified distance calculation, which works in my case, as I am only interested in very nearby waypoints. For points further apart, this may result in getting not exactly the nearest point. The absolute difference of two longitudes expressed as east-west distance in meters, depends on the latitude, where this difference is measured. This is not taken into account in my algorithm and I am not sure about the relevance of this effect in your case.
An efficient way to search for the smallest distance between a single point (that may change frequently), and a large set of points, in two dimensions is to use a QuadTree. There is a cost to initially build the QuadTree (i.e., add your marker locations to the data structure), so you only want to do this once (or as infrequently as possible). But, once constructed, searches for the closest point will typically be faster than a brute force comparison against all points in the large set.
BBN's OpenMap project has an open-source QuadTree Java implementation that I believe should work on Android that has a get(float lat, float lon) method to return the closest point.
Google's android-maps-utils library also has an open-source implementation of a QuadTree intended to run on Android, but as it is currently written it only supports a search(Bounds bounds) operation to return a set of points in a given bounding box, and not the point closest to an input point. But, it could be modified to perform the closest point search.
If you have a relatively small number of points (70-80 may be sufficiently small), then in real-world performance a brute-force comparison may execute in a similar amount of time to the QuadTree solution. But, it also depends on how frequently you intended on re-calculating the closest point - if frequent, a QuadTree may be a better choice.
I thought it should not be too difficult to extend my KDTree (see my other answer) also to a 3 dimensional version, and here is the result.
But as I do not use this version myself so far, take it with care. I added a unit-test, which shows that it works at least for your example.
/** 3 dimensional implementation of a KDTree for LatLng coordinates. */
public class KDTree {
private XYZComparator xComparator = new XYZComparator(0);
private XYZComparator yComparator = new XYZComparator(1);
private XYZComparator zComparator = new XYZComparator(2);
private XYZComparator[] comparators = { xComparator, yComparator,
zComparator };
/**
* Create a KDTree from a list of lat/lon coordinates. Returns the root-node
* of the tree.
*/
public KDTNode createTree(List<LatLng> recList) {
List<XYZ> xyzList = convertTo3Dimensions(recList);
return createTreeRecursive(0, xyzList);
}
/**
* Traverse the tree and find the point nearest to wp.
*/
static public LatLng findNearestWp(KDTNode root, LatLng wp) {
KDTResult result = new KDTResult();
XYZ xyz = convertTo3Dimensions(wp);
findNearestWpRecursive(root, xyz, result);
return result.nearestWp;
}
/** Convert lat/lon coordinates into a 3 dimensional xyz system. */
private static XYZ convertTo3Dimensions(LatLng wp) {
// See e.g.
// http://stackoverflow.com/questions/8981943/lat-long-to-x-y-z-position-in-js-not-working
double cosLat = Math.cos(wp.latitude * Math.PI / 180.0);
double sinLat = Math.sin(wp.latitude * Math.PI / 180.0);
double cosLon = Math.cos(wp.longitude * Math.PI / 180.0);
double sinLon = Math.sin(wp.longitude * Math.PI / 180.0);
double rad = 6378137.0;
double f = 1.0 / 298.257224;
double C = 1.0 / Math.sqrt(cosLat * cosLat + (1 - f) * (1 - f) * sinLat
* sinLat);
double S = (1.0 - f) * (1.0 - f) * C;
XYZ result = new XYZ();
result.x = (rad * C) * cosLat * cosLon;
result.y = (rad * C) * cosLat * sinLon;
result.z = (rad * S) * sinLat;
result.wp = wp;
return result;
}
private List<XYZ> convertTo3Dimensions(List<LatLng> recList) {
List<XYZ> result = new ArrayList<KDTree.XYZ>();
for (LatLng latLng : recList) {
XYZ xyz = convertTo3Dimensions(latLng);
result.add(xyz);
}
return result;
}
private static void findNearestWpRecursive(KDTNode node, XYZ wp,
KDTResult result) {
/* If a leaf node, calculate distance and return. */
if (node.isLeaf) {
double xDiff = node.xyz.x - wp.x;
double yDiff = node.xyz.y - wp.y;
double zDiff = node.xyz.z - wp.z;
double squareDist = xDiff * xDiff + yDiff * yDiff + zDiff * zDiff;
// Replace a previously found nearestDest only if the new one is
// nearer.
if (result.nearestWp == null || result.squareDistance > squareDist) {
result.nearestWp = node.xyz.wp;
result.squareDistance = squareDist;
}
return;
}
int devidedByDimension = node.depth % 3;
boolean goLeft;
/* Check whether left or right is more promising. */
if (devidedByDimension == 0) {
goLeft = wp.x < node.splitValue;
} else if (devidedByDimension == 1) {
goLeft = wp.y < node.splitValue;
} else {
goLeft = wp.z < node.splitValue;
}
KDTNode child = goLeft ? node.left : node.right;
findNearestWpRecursive(child, wp, result);
/*
* Check whether result needs to be checked also against the less
* promising side.
*/
if (result.squareDistance > node.minSquareDistance) {
KDTNode otherChild = goLeft ? node.right : node.left;
findNearestWpRecursive(otherChild, wp, result);
}
}
private KDTNode createTreeRecursive(int depth, List<XYZ> recList) {
KDTNode node = new KDTNode();
node.depth = depth;
if (recList.size() == 1) {
// Leafnode found
node.isLeaf = true;
node.xyz = recList.get(0);
return node;
}
int dimension = node.depth % 3;
sortWayPointListByDimension(recList, dimension);
List<XYZ> leftList = getHalfOf(recList, true);
List<XYZ> rightList = getHalfOf(recList, false);
// Get split point and distance to last left and first right point.
XYZ lastLeft = leftList.get(leftList.size() - 1);
XYZ firstRight = rightList.get(0);
double minDistanceToSplitValue;
double splitValue;
if (dimension == 0) {
minDistanceToSplitValue = (firstRight.x - lastLeft.x) / 2;
splitValue = lastLeft.x + Math.abs(minDistanceToSplitValue);
} else if (dimension == 1) {
minDistanceToSplitValue = (firstRight.y - lastLeft.y) / 2;
splitValue = lastLeft.y + Math.abs(minDistanceToSplitValue);
} else {
minDistanceToSplitValue = (firstRight.z - lastLeft.z) / 2;
splitValue = lastLeft.z + Math.abs(minDistanceToSplitValue);
}
node.splitValue = splitValue;
node.minSquareDistance = minDistanceToSplitValue
* minDistanceToSplitValue;
/** Call next level */
depth++;
node.left = createTreeRecursive(depth, leftList);
node.right = createTreeRecursive(depth, rightList);
return node;
}
/**
* Return a sublist representing the left or right half of a List. Size of
* recList must be at least 2 !
*
* IMPORTANT !!!!! Note: The original list must not be modified after
* extracting this sublist, as the returned subList is still backed by the
* original list.
*/
List<XYZ> getHalfOf(List<XYZ> xyzList, boolean leftHalf) {
int mid = xyzList.size() / 2;
if (leftHalf) {
return xyzList.subList(0, mid);
} else {
return xyzList.subList(mid, xyzList.size());
}
}
private void sortWayPointListByDimension(List<XYZ> wayPointList, int sortBy) {
XYZComparator comparator = comparators[sortBy];
Collections.sort(wayPointList, comparator);
}
class XYZComparator implements Comparator<XYZ> {
private int sortBy;
public XYZComparator(int sortBy) {
this.sortBy = sortBy;
}
#Override
public int compare(XYZ lhs, XYZ rhs) {
double diff;
if (sortBy == 0) {
diff = lhs.x - rhs.x;
} else if (sortBy == 1) {
diff = lhs.y - rhs.y;
} else {
diff = lhs.z - rhs.z;
}
if (diff > 0) {
return 1;
} else if (diff < 0) {
return -1;
} else {
return 0;
}
}
}
/** 3 Dimensional coordinates of a waypoint. */
static class XYZ {
double x;
double y;
double z;
// Keep also the original waypoint
LatLng wp;
}
/** Node of the KDTree */
public static class KDTNode {
KDTNode left;
KDTNode right;
boolean isLeaf;
/** latitude or longitude of the nodes division line. */
double splitValue;
/** Distance between division line and first point. */
double minSquareDistance;
/**
* Depth of the node in the tree. Depth 0,3,6.. devides the tree in the
* x-axis, depth 1,4,7,.. devides the tree in the y-axis and depth
* 2,5,8... devides the tree in the z axis.
*/
int depth;
/** The Waypoint in case the node is a leaf node. */
XYZ xyz;
}
/** Holds the result of a tree traversal. */
static class KDTResult {
LatLng nearestWp;
// We use the square of the distance to avoid square-root operations.
double squareDistance;
}
}
And here is the unit test:
public void testSOExample() {
KDTree tree = new KDTree();
LatLng Bangalore = new LatLng(12.971599, 77.594563);
LatLng Delhi = new LatLng(28.635308, 77.224960);
LatLng Mumbai = new LatLng(19.075984, 72.877656);
LatLng Chennai = new LatLng(13.052414, 80.250825);
LatLng Kolkata = new LatLng(22.572646, 88.363895);
List<LatLng> cities = Arrays.asList(new LatLng[] { Bangalore, Delhi,
Mumbai, Chennai, Kolkata });
KDTree.KDTNode root = tree.createTree(cities);
LatLng Hyderabad = new LatLng(17.385044, 78.486671);
LatLng nearestWp = tree.findNearestWp(root, Hyderabad);
assertEquals(nearestWp, Bangalore);
}
Here, I got a way to do that Using databases.
This is a calculate distance function:
public void calculateDistance() {
if (latitude != 0.0 && longitude != 0.0) {
for(int i=0;i<97;i++) {
Location myTargetLocation=new Location("");
myTargetLocation.setLatitude(targetLatitude[i]);
myTargetLocation.setLongitude(targetLongitude[i]);
distance[i]=myCurrentLocation.distanceTo(myTargetLocation);
distance[i]=distance[i]/1000;
mdb.insertDetails(name[i],targetLatitude[i], targetLongitude[i], distance[i]);
}
Cursor c1= mdb.getallDetail();
while (c1.moveToNext()) {
String station_name=c1.getString(1);
double latitude=c1.getDouble(2);
double longitude=c1.getDouble(3);
double dis=c1.getDouble(4);
//Toast.makeText(getApplicationContext(),station_name+" & "+latitude+" & "+longitude+" & "+dis,1).show();
}
Arrays.sort(distance);
double nearest_distance=distance[0];
Cursor c2=mdb.getNearestStationName();
{
while (c2.moveToNext()) {
double min_dis=c2.getDouble(4);
if(min_dis==nearest_distance)
{
String nearest_stationName=c2.getString(1);
if(btn_clicked.equals("source"))
{
source.setText(nearest_stationName);
break;
}
else if(btn_clicked.equals("dest"))
{
destination.setText(nearest_stationName);
break;
}
else
{
}
}
}
}
}
else
{
Toast.makeText(this, "GPS is Not Working Properly,, please check Gps and Wait for few second", 1).show();
}
}
All we have to do is Create an array named targetLatitude[i] and targetLongitude[i] containing Lats and Longs of all the places you want to calculate distance from.
Then create a database as shown below:
public class MyDataBase {
SQLiteDatabase sdb;
MyHelper mh;
MyDataBase(Context con)
{
mh = new MyHelper(con, "Metro",null, 1);
}
public void open() {
try
{
sdb=mh.getWritableDatabase();
}
catch(Exception e)
{
}
}
public void insertDetails(String name,double latitude,double longitude,double distance) {
ContentValues cv=new ContentValues();
cv.put("name", name);
cv.put("latitude", latitude);
cv.put("longitude",longitude);
cv.put("distance", distance);
sdb.insert("stations", null, cv);
}
public void insertStops(String stop,double latitude,double logitude)
{
ContentValues cv=new ContentValues();
cv.put("stop", stop);
cv.put("latitude", latitude);
cv.put("logitude", logitude);
sdb.insert("stops", null, cv);
}
public Cursor getallDetail()
{
Cursor c=sdb.query("stations",null,null,null,null,null,null);
return c;
}
public Cursor getNearestStationName() {
Cursor c=sdb.query("stations",null,null,null,null,null,null);
return c;
}
public Cursor getStops(String stop)
{
Cursor c;
c=sdb.query("stops",null,"stop=?",new String[]{stop},null, null, null);
return c;
}
class MyHelper extends SQLiteOpenHelper
{
public MyHelper(Context context, String name, CursorFactory factory,
int version) {
super(context, name, factory, version);
// TODO Auto-generated constructor stub
}
#Override
public void onCreate(SQLiteDatabase db) {
// TODO Auto-generated method stub
db.execSQL("Create table stations(_id integer primary key,name text," +
" latitude double, longitude double, distance double );");
db.execSQL("Create table stops(_id integer primary key,stop text," +
"latitude double,logitude double);");
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub
}
}
public void deleteDetail() {
sdb.delete("stations",null,null);
sdb.delete("stops",null,null);
}
public void close() {
sdb.close();
}
}
Then execute the CalculateDistance function wherever you want and you can get the nearest station name.

TileProvider using local tiles

I would like to use the new TileProvider functionality of the latest Android Maps API (v2) to overlay some custom tiles on the GoogleMap. However as my users will not have internet a lot of the time, I want to keep the tiles stored in a zipfile/folder structure on the device. I will be generating my tiles using Maptiler with geotiffs. My questions are:
What would be the best way to store the tiles on the device?
How would I go about creating a TileProvider that returns local tiles?
You can put tiles into assets folder (if it is acceptable for the app size) or download them all on first start and put them into device storage (SD card).
You can implement TileProvider like this:
public class CustomMapTileProvider implements TileProvider {
private static final int TILE_WIDTH = 256;
private static final int TILE_HEIGHT = 256;
private static final int BUFFER_SIZE = 16 * 1024;
private AssetManager mAssets;
public CustomMapTileProvider(AssetManager assets) {
mAssets = assets;
}
#Override
public Tile getTile(int x, int y, int zoom) {
byte[] image = readTileImage(x, y, zoom);
return image == null ? null : new Tile(TILE_WIDTH, TILE_HEIGHT, image);
}
private byte[] readTileImage(int x, int y, int zoom) {
InputStream in = null;
ByteArrayOutputStream buffer = null;
try {
in = mAssets.open(getTileFilename(x, y, zoom));
buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[BUFFER_SIZE];
while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
} catch (OutOfMemoryError e) {
e.printStackTrace();
return null;
} finally {
if (in != null) try { in.close(); } catch (Exception ignored) {}
if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
}
}
private String getTileFilename(int x, int y, int zoom) {
return "map/" + zoom + '/' + x + '/' + y + ".png";
}
}
And now you can use it with your GoogleMap instance:
private void setUpMap() {
mMap.setMapType(GoogleMap.MAP_TYPE_NONE);
mMap.addTileOverlay(new TileOverlayOptions().tileProvider(new CustomMapTileProvider(getResources().getAssets())));
CameraUpdate upd = CameraUpdateFactory.newLatLngZoom(new LatLng(LAT, LON), ZOOM);
mMap.moveCamera(upd);
}
In my case I also had a problem with y coordinate of tiles generated by MapTiler, but I managed it by adding this method into CustomMapTileProvider:
/**
* Fixing tile's y index (reversing order)
*/
private int fixYCoordinate(int y, int zoom) {
int size = 1 << zoom; // size = 2^zoom
return size - 1 - y;
}
and callig it from getTile() method like this:
#Override
public Tile getTile(int x, int y, int zoom) {
y = fixYCoordinate(y, zoom);
...
}
[Upd]
If you know exac area of your custom map, you should return NO_TILE for missing tiles from getTile(...) method.
This is how I did it:
private static final SparseArray<Rect> TILE_ZOOMS = new SparseArray<Rect>() {{
put(8, new Rect(135, 180, 135, 181 ));
put(9, new Rect(270, 361, 271, 363 ));
put(10, new Rect(541, 723, 543, 726 ));
put(11, new Rect(1082, 1447, 1086, 1452));
put(12, new Rect(2165, 2894, 2172, 2905));
put(13, new Rect(4330, 5789, 4345, 5810));
put(14, new Rect(8661, 11578, 8691, 11621));
}};
#Override
public Tile getTile(int x, int y, int zoom) {
y = fixYCoordinate(y, zoom);
if (hasTile(x, y, zoom)) {
byte[] image = readTileImage(x, y, zoom);
return image == null ? null : new Tile(TILE_WIDTH, TILE_HEIGHT, image);
} else {
return NO_TILE;
}
}
private boolean hasTile(int x, int y, int zoom) {
Rect b = TILE_ZOOMS.get(zoom);
return b == null ? false : (b.left <= x && x <= b.right && b.top <= y && y <= b.bottom);
}
The possibility of adding custom tileproviders in the new API (v2) is great, however you mention that your users are mostly offline. If a user is offline when first launching the application you cannot use the new API as it requires the user to be online (at least once to build a cache it seems) - otherwise it will only display a black screen.
EDIT 2/22-14:
I recently came across the same issue again - having custom tiles for an app which had to work offline. Solved it by adding an invisible (w/h 0/0) mapview to an initial view where the client had to download some content. This seems to work, and allows me to use a mapview in offline mode later on.
This is how I implemented this in Kotlin:
class LocalTileProvider : TileProvider
{
override fun getTile(x: Int, y: Int, zoom: Int): Tile?
{
// This is for my case only
if (zoom > 11)
return TileProvider.NO_TILE
val path = "${getImagesFolder()}/tiles/$zoom/$x/$y/filled.png"
val file = File(path)
if (!file.exists())
return TileProvider.NO_TILE
return try {
Tile(TILE_SIZE, TILE_SIZE, file.readBytes())
}
catch (e: Exception)
{
TileProvider.NO_TILE
}
}
companion object {
const val TILE_SIZE = 512
}
}

AsyncTask not executing properly according to logic

I'm having problems trying to download specific tiles for OSM in different zoom levels, based on a given set of places with their latitude and longitude values.
What I am trying to do is to determine the MapTile number for the top/bottom left and top/bottom right corners, and loop the numbers to download the tiles. For now I am trying to download zoom levels 1 above and 1 below the given zoom level in the constructor.
public class MapDownload extends AsyncTask<String, Void, String>{
int zoom;
private ArrayList<GeoPoint> places;
private Coordinates topRight = new Coordinates(); // a java class I did for myself
private Coordinates bottomRight = new Coordinates();
private Coordinates topLeft = new Coordinates();
private Coordinates bottomLeft = new Coordinates();
public MapDownload(ArrayList<GeoPoint> placeList, int zoom){
this.places = placeList;
this.zoom = zoom;
}
#Override
protected String doInBackground(String... params) {
// TODO Auto-generated method stub
for (int w = zoom -1 ; w <= zoom +1; w++){
double maxLat = 0.0;
double maxLon = 0.0;
double minLat = 0.0;
double minLon = 0.0;
for(GeoPoint point: places) {
double lon = (double) ( point.getLongitudeE6() / 1E6 * 1.0);
double lat = (double) (point.getLatitudeE6() / 1E6 * 1.0);
if(lat > maxLat) {
maxLat = lat;
}
if(lat < minLat || minLat == 0.0) {
minLat = lat;
}
if(lon> maxLon) {
maxLon = lon;
}
if(lon < minLon || lon == 0.0) {
minLon = lon;
}
}
topRight = topRight.gpsToMaptile(maxLon, maxLat, w); //top right
bottomRight = bottomRight.gpsToMaptile(maxLon, minLat, w); //bottom right
topLeft = topLeft.gpsToMaptile(minLon, maxLat, w); //top left
bottomLeft = bottomLeft.gpsToMaptile(minLon, minLat, w); //bottom left
for (int x = topLeft.getYTile(); x < bottomLeft.getYTile(); x++){
for(int y = topLeft.getXTile(); y < bottomRight.getXTile(); y++){
try {
String urlStr = "http://a.tile.openstreetmap.org/"+ w +"/"+y+"/"+x+".png";
URL url = new URL(urlStr);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
File newFileDir = new File(Environment.getExternalStorageDirectory().toString()
+ "/downloadMap/test/"+w+"/"+y);
newFileDir.mkdirs();
File newFile = new File(newFileDir, x+".png");
OutputStream output = new FileOutputStream(newFile);
int read;
while ((read = in.read()) != -1) {
output.write(read);
output.flush();
}
urlConnection.disconnect();
} catch (Exception e) {
Log.e("URL::: ERROR", e.getMessage());
e.printStackTrace();
}
}
}
}
return null;
}
In my MainActivity class, this is what I did to call this AsyncTask:
public class MainActivity extends Activity implements LocationListener, MapViewConstants {
public void onCreate(Bundle savedInstanceState) {
MapDownload mapDownload = new MapDownload(placeList, 12);
mapDownload.execute("");
}
}
Things were fine when I did the loops for int x and int y (for a single zoom layer). However once I placed the 3rd loop with int w (to loop for different zoom levels), things started going haywire and it started downloading every single tile into the phone.
I have tested for the code logic separately (by printing the urlStr) and it does indeed work to determine the specific MapTiles needed for download. However the same codes wouldn't work when placed in this AsyncTask class, which led me to believe that such codes might have a problem with the implementation of AsyncTask.
Hope there is someone out there whom could point out my mistake. Thank you!

Categories

Resources