/*
Swisscom Safe Connect
Copyright (C) 2014 Swisscom
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.swisscom.safeconnect.view;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.widget.ImageView;
import com.svg.svgandroid.SVG;
import com.svg.svgandroid.SVGBuilder;
import com.swisscom.safeconnect.R;
import com.swisscom.safeconnect.model.PlumberLastConnectionLogResponse;
import com.swisscom.safeconnect.model.PlumberLastConnectionLogResponseList;
import com.swisscom.safeconnect.utils.Fonts;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Created by vadim on 22.09.14.
*/
public class WorldMapView extends ImageView {
// used to calibrate the map
private static final int offsetX = 0;
private static final int offsetY = 0;
private static final double radian = 0.017453293;
private static final int legendX = 5;
private static final int legendY = 5;
// Robinson's tables
private static final double[] AA = {0.8487,0.84751182,0.84479598,0.840213,0.83359314,0.8257851,
0.814752,0.80006949,0.78216192,0.76060494,0.73658673,0.7086645,0.67777182,0.64475739,
0.60987582,0.57134484,0.52729731,0.48562614,0.45167814};
private static final double[] BB = {0,0.0838426,0.1676852,0.2515278,0.3353704,0.419213,
0.5030556,0.5868982,0.67182264,0.75336633,0.83518048,0.91537187,0.99339958,
1.06872269,1.14066505,1.20841528,1.27035062,1.31998003,1.3523};
private int imgW, imgH;
private int viewW, viewH;
private double earthRadius;
// coordinates of the points on the map
//private Point vpnServerPos = new Point(46.946702, 7.444652);
private Point vpnServerPos = new Point(46.948049, 7.449630);
private Point userPos = vpnServerPos;
private PlumberLastConnectionLogResponse[] connPoints = new PlumberLastConnectionLogResponse[0];
private Path userPosShape;
private float shapeSize, collisionArea, textYShift;
private float legendPadding, legendPadding2, zoomPadding, httpConnSize;
// projected pos points ready to be drawn on the map
private Point projVpnPos;
private PointF drawVpnPos;
private Point projUserPos;
private PointF drawUserPos;
private Point[] projConnPoints;
private PointF[] drawConnPoints;
private RectF bounds;
private float scaleFactor, scaledPadding;
private float transX, transY;
private Matrix matrix = new Matrix();
private Paint userPaintIn, userPaintOut, discUserPaint, discUserPaintFilled;
private Paint vpnPaintIn, vpnPaintOut, vpnPaintNoNetwork;
private Paint blackPaint, whitePaint;
private Paint connPaint, userVpnPaint;
private Paint cityTextPaint;
private boolean disconnected = true;
private boolean noNetwork = false;
private boolean loaded = false;
private boolean tabletLayout = false;
private boolean doNotRecalcBounds = false;
private GestureDetector gestureDetector;
private ScaleGestureDetector scaleDetector;
private OnClickListener userInteractionListener;
private static class Point implements Serializable {
private static final long serialVersionUID = 3941286050907028864L;
public double x, y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
}
private static class SvgHolder {
static Drawable map;
public static void loadGraphics(Resources res) {
if (map == null) {
SVG worldmap = new SVGBuilder()
.readFromResource(res, R.raw.worldmap_robinson_map)
.build();
map = worldmap.getDrawable();
}
}
}
private class SvgLoader extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
SvgHolder.loadGraphics(getResources());
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
loaded = true;
setImageDrawable(SvgHolder.map);
reproject();
calcBounds();
invalidate();
}
}
public WorldMapView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WorldMapView);
tabletLayout = a.getBoolean(R.styleable.WorldMapView_tabletLayout, false);
a.recycle();
shapeSize = 6 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
httpConnSize = 4 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
collisionArea = 13 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
textYShift = 7 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
legendPadding = 3 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
legendPadding2 = 4 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
zoomPadding = 25 * (getContext().getResources().getDisplayMetrics().density + 0.5f);
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
setScaleType(ScaleType.MATRIX);
SvgLoader loader = new SvgLoader();
loader.execute();
gestureDetector = new GestureDetector(getContext(), new GestureListener());
scaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
init();
}
static class SavedState extends BaseSavedState {
boolean disconnected;
boolean noNetwork;
Point vpnServerPos, userPos;
PlumberLastConnectionLogResponse[] connPoints;
SavedState(Parcelable superState) {
super(superState);
}
public SavedState(Parcel source) {
super(source);
disconnected = (Boolean) source.readValue(null);
noNetwork = (Boolean) source.readValue(null);
vpnServerPos = (Point) source.readValue(Point.class.getClassLoader());
userPos = (Point) source.readValue(Point.class.getClassLoader());
Object[] cp = (Object [])source.readValue(PlumberLastConnectionLogResponse.class.getClassLoader());
connPoints = Arrays.copyOf(cp, cp.length, PlumberLastConnectionLogResponse[].class);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeValue(disconnected);
dest.writeValue(noNetwork);
dest.writeValue(vpnServerPos);
dest.writeValue(userPos);
dest.writeValue(connPoints);
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.disconnected = disconnected;
ss.noNetwork = noNetwork;
ss.connPoints = connPoints;
ss.userPos = userPos;
ss.vpnServerPos = vpnServerPos;
return ss;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if(!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
disconnected = ss.disconnected;
noNetwork = ss.noNetwork;
connPoints = ss.connPoints;
vpnServerPos = ss.vpnServerPos;
userPos = ss.userPos;
}
/**
* prepares drawing objects
*/
private void init() {
vpnPaintIn = new Paint(Paint.ANTI_ALIAS_FLAG);
vpnPaintIn.setColor(getResources().getColor(R.color.light_blue));
vpnPaintNoNetwork = new Paint(Paint.ANTI_ALIAS_FLAG);
vpnPaintNoNetwork.setColor(getResources().getColor(R.color.grey));
vpnPaintOut = new Paint(Paint.ANTI_ALIAS_FLAG);
vpnPaintOut.setColor(getResources().getColor(R.color.lighter_blue));
vpnPaintOut.setAlpha(150);
userPaintIn = new Paint(Paint.ANTI_ALIAS_FLAG);
userPaintIn.setColor(getResources().getColor(R.color.green));
discUserPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
discUserPaint.setColor(getResources().getColor(R.color.red));
discUserPaint.setStyle(Paint.Style.FILL_AND_STROKE);
discUserPaint.setPathEffect(new DashPathEffect(new float[] {10, 5}, 0));
discUserPaint.setStrokeWidth(3);
discUserPaintFilled = new Paint(Paint.ANTI_ALIAS_FLAG);
discUserPaintFilled.setColor(getResources().getColor(R.color.red));
userPaintOut = new Paint(Paint.ANTI_ALIAS_FLAG);
userPaintOut.setColor(getResources().getColor(R.color.lighter_green));
userPaintOut.setAlpha(150);
blackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
blackPaint.setColor(getResources().getColor(android.R.color.black));
whitePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
whitePaint.setColor(getResources().getColor(R.color.white));
connPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
connPaint.setColor(getResources().getColor(android.R.color.black));
connPaint.setPathEffect(new DashPathEffect(new float[] {10, 5}, 0));
connPaint.setStyle(Paint.Style.STROKE);
userVpnPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
userVpnPaint.setColor(getResources().getColor(R.color.green));
userVpnPaint.setStyle(Paint.Style.STROKE);
userVpnPaint.setStrokeWidth(3);
cityTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
cityTextPaint.setTypeface(Fonts.FONT_NORMAL);
cityTextPaint.setColor(getResources().getColor(android.R.color.black));
cityTextPaint.setTextSize(12 * (getContext().getResources().getDisplayMetrics().density + 0.5f));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewW = w;
viewH = h;
if (loaded) {
reproject();
}
}
private void reproject() {
imgW = getDrawable().getIntrinsicWidth();
imgH = getDrawable().getIntrinsicHeight();
earthRadius = (imgW / 2.666269758) / 2;
// reproject all points
projVpnPos = projectToScreen(vpnServerPos.x, vpnServerPos.y);
projUserPos = projectToScreen(userPos.x, userPos.y);
projectConnPoints();
calcBounds();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!loaded) return;
if (!noNetwork && isUserPosKnown()) {
canvas.drawLine(drawUserPos.x, drawUserPos.y, drawVpnPos.x, drawVpnPos.y,
disconnected ? discUserPaint : userVpnPaint);
}
// drawing connections
Path connPath;
PlumberLastConnectionLogResponse conn;
List<PointF> drawnConn = new ArrayList<PointF>(5);
for (int i = 0; i < projConnPoints.length; i++) {
conn = connPoints[i];
connPath = new Path();
connPath.moveTo(drawVpnPos.x, drawVpnPos.y);
connPath.lineTo(drawConnPoints[i].x, drawConnPoints[i].y);
// using path, because dashed line does not work!
canvas.drawPath(connPath, connPaint);
canvas.drawCircle(drawConnPoints[i].x, drawConnPoints[i].y, httpConnSize, blackPaint);
// check already drawn points and texts so we don't draw the text that could
// cause overlapping with some other text
boolean drawingAllowed = true;
if (drawConnPoints[i].x < 0 || drawConnPoints[i].y < 0 || drawConnPoints[i].x > viewW ||
drawConnPoints[i].y > viewH) {
drawingAllowed = false;
} else {
for (PointF dp : drawnConn) {
if (Math.abs(drawConnPoints[i].x - dp.x) < collisionArea &&
Math.abs(drawConnPoints[i].y - dp.y) < collisionArea) {
drawingAllowed = false;
break;
}
}
}
if (!drawingAllowed) continue;
// ok to draw
StringBuilder sb = new StringBuilder(conn.getCountryCode());
if (conn.getCity() != null && !conn.getCity().isEmpty()) {
sb.append(", ");
sb.append(conn.getCity());
}
String city = sb.toString();
float textWidth = cityTextPaint.measureText(city);
float textPosX = drawConnPoints[i].x - textWidth / 2;
if (textPosX < collisionArea) {
textPosX = collisionArea;
} else if (textPosX+textWidth > viewW-collisionArea) {
textPosX = viewW-collisionArea-textWidth;
}
canvas.drawText(city, textPosX, drawConnPoints[i].y - textYShift,
cityTextPaint);
drawnConn.add(drawConnPoints[i]);
}
canvas.drawRect(drawVpnPos.x - shapeSize, drawVpnPos.y - shapeSize,
drawVpnPos.x + shapeSize, drawVpnPos.y + shapeSize, noNetwork? vpnPaintNoNetwork : vpnPaintIn);
// not drawing own position when no network
if (!noNetwork && isUserPosKnown()) {
canvas.drawPath(userPosShape, disconnected ? discUserPaintFilled : userPaintIn);
}
drawLegend(canvas);
}
private Path getUserPosLegendShape(float startX, float startY) {
Path userPosLegendShape = new Path();
userPosLegendShape.moveTo(startX, startY+shapeSize*2);
userPosLegendShape.lineTo(startX+shapeSize*2, startY+shapeSize*2);
userPosLegendShape.lineTo(startX+shapeSize, startY);
userPosLegendShape.lineTo(startX, startY+shapeSize*2);
return userPosLegendShape;
}
private boolean isUserPosKnown() {
return userPos != vpnServerPos && !(userPos.x == 0 && userPos.y == 0);
}
private void drawLegend(Canvas canvas) {
Rect rect = new Rect();
float textX = legendX + shapeSize*2 + legendPadding;
String legendUser = getContext().getString(R.string.legend_user);
String legendVpn = getContext().getString(R.string.legend_vpn);
String legendConn = getContext().getString(R.string.legend_conn);
cityTextPaint.getTextBounds(legendUser, 0, legendUser.length()-1, rect);
float textY = legendY + (shapeSize*2-rect.height())/2 + rect.height();
canvas.drawRect(legendX, legendY, legendX + shapeSize*2,
legendX + shapeSize*2, noNetwork? vpnPaintNoNetwork : vpnPaintIn);
float textWidth = cityTextPaint.measureText(legendVpn);
canvas.drawText(legendVpn, textX, textY, cityTextPaint);
if (noNetwork) return;
if (tabletLayout) {
if (isUserPosKnown()) {
canvas.drawPath(getUserPosLegendShape((int) (textX + textWidth + legendPadding2), legendY),
disconnected ? discUserPaintFilled : userPaintIn);
textX += textWidth + textYShift + shapeSize * 2;
canvas.drawText(legendUser, textX, textY, cityTextPaint);
textWidth = cityTextPaint.measureText(legendUser);
} else {
textWidth = 0;
}
if (connPoints.length > 0) {
canvas.drawCircle(textX + textWidth + legendX + legendPadding2*2, legendY + shapeSize, httpConnSize, blackPaint);
textX += textWidth + textYShift + shapeSize * 2;
canvas.drawText(legendConn, textX, textY, cityTextPaint);
}
} else {
float startY = legendY;
if (isUserPosKnown()) {
startY = shapeSize * 2 + legendPadding;
canvas.drawPath(getUserPosLegendShape(legendX, startY),
disconnected ? discUserPaintFilled : userPaintIn);
canvas.drawText(legendUser, textX, rect.height() + startY, cityTextPaint);
}
if (connPoints.length > 0) {
startY += shapeSize * 2 + legendPadding;
canvas.drawCircle(legendX + shapeSize, startY + httpConnSize/2, httpConnSize, blackPaint);
canvas.drawText(legendConn, textX, startY + rect.height()-httpConnSize/2, cityTextPaint);
}
}
}
private double roundToNearest(double roundTo, double value) {
return Math.floor(value/roundTo)*roundTo;
}
private Point project(double lat, double lon) {
double lonSign = Math.signum(lon);
double latSign = Math.signum(lat);
// all calculations are positive
lat = Math.abs(lat);
lon = Math.abs(lon);
double low = roundToNearest(5, lat-0.0000000001);
low = (lat == 0)? 0: low;
double high = low+5;
// interpolation indices
int lowIndex = (int) low/5;
int highIndex = (int) high/5;
double ratio = (lat - low) / 5;
double adjAA = ((this.AA[highIndex]-this.AA[lowIndex])*ratio)+this.AA[lowIndex];
double adjBB = ((this.BB[highIndex]-this.BB[lowIndex])*ratio)+this.BB[lowIndex];
return new Point(adjAA * lon * radian * lonSign * earthRadius + offsetX,
adjBB * latSign * earthRadius + offsetY);
}
private Point projectToScreen(double lat, double lon) {
Point p = project(lat, lon);
p.x = (p.x + imgW /2);
p.y = imgH /2 - p.y;
return p;
}
private void calcBounds() {
if (!loaded) return;
if (doNotRecalcBounds) {
scaleAndTranslate(scaleFactor, transX, transY);
return;
}
ArrayList<Float> allX = new ArrayList<Float>(connPoints.length + 2);
ArrayList<Float> allY = new ArrayList<Float>(connPoints.length + 2);
allX.add((float) projVpnPos.x);
allX.add((float) projUserPos.x);
allY.add((float) projVpnPos.y);
allY.add((float) projUserPos.y);
for (Point p : projConnPoints) {
allX.add((float) p.x);
allY.add((float) p.y);
}
bounds = new RectF(Collections.min(allX) - zoomPadding, Collections.min(allY) - zoomPadding,
Collections.max(allX) + zoomPadding, Collections.max(allY) + zoomPadding);
float scaleFactorX = 1;
if (bounds.right - bounds.left > 0.0001f) {
scaleFactorX = viewW / (bounds.right - bounds.left);
}
float scaleFactorY = 1;
if (bounds.bottom - bounds.top > 0.0001f) {
scaleFactorY = viewH / (bounds.bottom - bounds.top);
}
scaleFactor = Math.min(scaleFactorX, scaleFactorY);
// limit the scale factor
scaleFactor = Math.min(scaleFactor, 5);
float centerX = (viewW - (bounds.right - bounds.left) * scaleFactor) / 2;
float centerY = (viewH - (bounds.bottom - bounds.top) * scaleFactor) / 2;
scaleAndTranslate(scaleFactor, bounds.left - centerX / scaleFactor,
bounds.top - centerY / scaleFactor);
}
private void scaleAndTranslate(float scale, float moveX, float moveY) {
transX = moveX;
transY = moveY;
scaleFactor = scale;
matrix.reset();
matrix.preScale(scale, scale);
matrix.preTranslate(-transX, -transY);
setImageMatrix(matrix);
// calc screen positions after the transformations
drawVpnPos = new PointF((float)(projVpnPos.x-transX)*scaleFactor, (float)(projVpnPos.y-transY)*scaleFactor);
drawUserPos = new PointF((float)(projUserPos.x-transX)*scaleFactor, (float)(projUserPos.y-transY)*scaleFactor);
drawConnPoints = new PointF[projConnPoints.length];
for (int i = 0; i < projConnPoints.length; i++) {
Point p = projConnPoints[i];
drawConnPoints[i] = new PointF((float)(p.x-transX)*scaleFactor, (float)(p.y-transY)*scaleFactor);
}
scaledPadding = zoomPadding*scaleFactor;
userPosShape = new Path();
userPosShape.moveTo(drawUserPos.x-shapeSize, drawUserPos.y+shapeSize);
userPosShape.lineTo(drawUserPos.x+shapeSize, drawUserPos.y+shapeSize);
userPosShape.lineTo(drawUserPos.x, drawUserPos.y-shapeSize);
userPosShape.lineTo(drawUserPos.x-shapeSize, drawUserPos.y+shapeSize);
}
/**
* sets the coordinates of the VPN server and redraws the map
* @param lat
* @param lon
*/
public void setVpnServerPos(double lat, double lon) {
vpnServerPos = new Point(lat, lon);
projVpnPos = projectToScreen(vpnServerPos.x, vpnServerPos.y);
calcBounds();
invalidate();
}
/**
* sets connection positions and draws them on the map
* @param pointsList list from Plumber
*/
public void setConnPoints(PlumberLastConnectionLogResponseList pointsList) {
List<PlumberLastConnectionLogResponse> tmp = new ArrayList<PlumberLastConnectionLogResponse>(5);
// make sure that there is no connection to (0, 0)
for (PlumberLastConnectionLogResponse r: pointsList.getConnectionLogs()) {
if (!(r.getLatitude() == 0 && r.getLongitude() == 0)) {
tmp.add(r);
}
}
connPoints = new PlumberLastConnectionLogResponse[tmp.size()];
connPoints = tmp.toArray(connPoints);
disconnected = false;
noNetwork = false;
projectConnPoints();
setUserPos(pointsList.getLatitude(), pointsList.getLongitude());
calcBounds();
invalidate();
}
private void projectConnPoints() {
projConnPoints = new Point[connPoints.length];
for (int i = 0; i < projConnPoints.length; i++) {
projConnPoints[i] = projectToScreen(connPoints[i].getLatitude(),
connPoints[i].getLongitude());
}
}
private void setUserPos(double lat, double lon) {
userPos = new Point(lat, lon);
projUserPos = projectToScreen(userPos.x, userPos.y);
}
public void showDisconnected() {
projConnPoints = new Point[0];
connPoints = new PlumberLastConnectionLogResponse[0];
disconnected = true;
noNetwork = false;
calcBounds();
invalidate();
}
public void showNoNetwork() {
projConnPoints = new Point[0];
connPoints = new PlumberLastConnectionLogResponse[0];
noNetwork = true;
disconnected = true;
calcBounds();
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
scaleDetector.onTouchEvent(event);
gestureDetector.onTouchEvent(event);
return true;
}
private class GestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
if (userInteractionListener != null) userInteractionListener.onClick(WorldMapView.this);
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
doNotRecalcBounds = true;
float x = transX + distanceX / scaleFactor;
float y = transY + distanceY / scaleFactor;
if (x < -4800 && distanceX < 0 || x > 4800 && distanceX > 0
|| y < -2700 && distanceY < 0 || y > 2700 && distanceY > 0) return true;
scaleAndTranslate(scaleFactor, x, y);
return true;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
private class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener {
private float fx, fy, tx, ty, s;
@Override
public boolean onScale(ScaleGestureDetector detector) {
float s = scaleFactor * detector.getScaleFactor();
if (s < 0.25 && s < scaleFactor || s > 35) return true;
scaleFactor = s;
scaleAndTranslate(scaleFactor, tx + fx - detector.getFocusX() / scaleFactor,
ty + fy - detector.getFocusY() / scaleFactor);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
tx = transX;
ty = transY;
fx = detector.getFocusX()/scaleFactor;
fy = detector.getFocusY()/scaleFactor;
s = scaleFactor;
doNotRecalcBounds = true;
if (userInteractionListener != null) userInteractionListener.onClick(WorldMapView.this);
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
public void setAllowRecalcBounds() {
doNotRecalcBounds = false;
calcBounds();
invalidate();
}
public void setOnUserMapInteractionListener(OnClickListener listener) {
userInteractionListener = listener;
}
}