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

Show scale on map #6379

Merged
merged 7 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -19,9 +19,13 @@
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import com.google.android.gms.location.LocationListener;
import com.google.android.gms.maps.CameraUpdate;
Expand All @@ -46,6 +50,7 @@
import org.odk.collect.androidshared.system.ContextUtils;
import org.odk.collect.androidshared.ui.ToastUtils;
import org.odk.collect.googlemaps.GoogleMapConfigurator.GoogleMapTypeOption;
import org.odk.collect.googlemaps.scaleview.MapScaleView;
import org.odk.collect.location.LocationClient;
import org.odk.collect.maps.LineDescription;
import org.odk.collect.maps.MapConfigurator;
Expand All @@ -72,7 +77,7 @@

import timber.log.Timber;

public class GoogleMapFragment extends SupportMapFragment implements
public class GoogleMapFragment extends Fragment implements
MapFragment, LocationListener, LocationClient.LocationClientListener,
GoogleMap.OnMapClickListener, GoogleMap.OnMapLongClickListener,
GoogleMap.OnMarkerClickListener, GoogleMap.OnMarkerDragListener,
Expand Down Expand Up @@ -100,6 +105,9 @@ public class GoogleMapFragment extends SupportMapFragment implements
);

private GoogleMap map;
private MapScaleView scaleView;
private ReadyListener readyListener;
private ErrorListener errorListener;
private Marker locationCrosshairs;
private Circle accuracyCircle;
private final List<ReadyListener> gpsLocationReadyListeners = new ArrayList<>();
Expand All @@ -121,9 +129,27 @@ public class GoogleMapFragment extends SupportMapFragment implements
private boolean hasCenter;

@Override
@SuppressLint("MissingPermission") // Permission checks for location services handled in widgets
public void init(@Nullable ReadyListener readyListener, @Nullable ErrorListener errorListener) {
getMapAsync((GoogleMap googleMap) -> {
this.readyListener = readyListener;
this.errorListener = errorListener;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mapFragmentDelegate.onCreate(savedInstanceState);
}

@Nullable
@Override
@SuppressLint("MissingPermission") // Permission checks for location services handled in widgets
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.map_layout, container, false);

scaleView = view.findViewById(R.id.scale_view);

SupportMapFragment mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);
mapFragment.getMapAsync((GoogleMap googleMap) -> {
if (googleMap == null) {
ToastUtils.showShortToast(requireContext(), org.odk.collect.strings.R.string.google_play_services_error_occured);
if (errorListener != null) {
Expand All @@ -144,7 +170,9 @@ public void init(@Nullable ReadyListener readyListener, @Nullable ErrorListener
googleMap.setMyLocationEnabled(false);
googleMap.setMinZoomPreference(1);
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
toLatLng(INITIAL_CENTER), INITIAL_ZOOM));
toLatLng(INITIAL_CENTER), INITIAL_ZOOM));
googleMap.setOnCameraMoveListener(() -> scaleView.update(googleMap.getCameraPosition().zoom, googleMap.getCameraPosition().target.latitude));
googleMap.setOnCameraIdleListener(() -> scaleView.update(googleMap.getCameraPosition().zoom, googleMap.getCameraPosition().target.latitude));
loadReferenceOverlay();

// If the screen is rotated before the map is ready, this fragment
Expand All @@ -155,12 +183,8 @@ public void init(@Nullable ReadyListener readyListener, @Nullable ErrorListener
readyListener.onReady(this);
}
});
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mapFragmentDelegate.onCreate(savedInstanceState);
return view;
}

@Override public void onAttach(@NonNull Context context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),
* licensed under the Apache License, Version 2.0.
*/
package org.odk.collect.googlemaps.scaleview;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Typeface;

public class Drawer {

private final Paint textPaint = new Paint();
private final Paint strokePaint = new Paint();
private final Path strokePath = new Path();

private final Paint outlinePaint = new Paint();
private final Path outlineDiffPath = new Path();
private float outlineStrokeWidth = 2; // strokeWidth * 2
private float outlineStrokeDiff = outlineStrokeWidth / 2 / 2; // strokeWidth / 2
private float outlineTextStrokeWidth = 3; // density * 2
private boolean outlineEnabled = true;

private float textHeight;
private float horizontalLineY;

private boolean expandRtlEnabled;
private int viewWidth;

private Scales scales = new Scales(null, null);

Drawer(int color, float textSize, float strokeWidth, float density, boolean outlineEnabled, boolean expandRtlEnabled) {
textPaint.setAntiAlias(true);
textPaint.setColor(color);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextSize(textSize);

strokePaint.setAntiAlias(true);
strokePaint.setColor(color);
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setStrokeWidth(strokeWidth);

outlinePaint.set(strokePaint);
outlinePaint.setARGB(255, 255, 255, 255);
outlineStrokeWidth = strokeWidth * 2;
outlineStrokeDiff = strokeWidth / 2;
outlineTextStrokeWidth = density * 2;
this.outlineEnabled = outlineEnabled;
this.expandRtlEnabled = expandRtlEnabled;

update();
}

private void update() {
outlinePaint.setTextSize(textPaint.getTextSize());
outlinePaint.setTypeface(textPaint.getTypeface());
outlinePaint.setStrokeWidth(outlineTextStrokeWidth);

Rect textRect = new Rect();
Paint highestPaint = outlineEnabled ? outlinePaint : textPaint;
String possibleText = "1234567890kmift";
highestPaint.getTextBounds(possibleText, 0, possibleText.length(), textRect);
textHeight = textRect.height();

horizontalLineY = textHeight + textHeight / 2;
}

int getWidth() {
return (int) (scales.maxLength() + strokePaint.getStrokeWidth());
}

int getHeight() {
if (scales.bottom() != null) {
return (int) (textHeight * 3 + outlineTextStrokeWidth / 2);
} else {
return (int) (horizontalLineY + strokePaint.getStrokeWidth());
}
}

void setScales(Scales scales) {
this.scales = scales;
}

void setColor(int color) {
textPaint.setColor(color);
strokePaint.setColor(color);
}

void setTextSize(float textSize) {
textPaint.setTextSize(textSize);
update();
}

void setTextFont(Typeface font) {
textPaint.setTypeface(font);
update();
}

void setStrokeWidth(float strokeWidth) {
strokePaint.setStrokeWidth(strokeWidth);
outlineStrokeWidth = strokeWidth * 2;
outlineStrokeDiff = strokeWidth / 2;
update();
}

void setOutlineEnabled(boolean enabled) {
outlineEnabled = enabled;
update();
}

void setExpandRtlEnabled(boolean enabled) {
expandRtlEnabled = enabled;
}

void setViewWidth(int width) {
viewWidth = width;
}

void draw(Canvas canvas) {
Scale top = scales.top();
if (top == null) {
return;
}
if (expandRtlEnabled && viewWidth == 0) {
expandRtlEnabled = false;
}

if (expandRtlEnabled) {
outlinePaint.setTextAlign(Paint.Align.RIGHT);
textPaint.setTextAlign(Paint.Align.RIGHT);
} else {
outlinePaint.setTextAlign(Paint.Align.LEFT);
textPaint.setTextAlign(Paint.Align.LEFT);
}

if (outlineEnabled) {
outlinePaint.setStrokeWidth(outlineTextStrokeWidth);
canvas.drawText(top.text(), expandRtlEnabled ? viewWidth : 0, textHeight, outlinePaint);
}
canvas.drawText(top.text(), expandRtlEnabled ? viewWidth : 0, textHeight, textPaint);

strokePath.rewind();
strokePath.moveTo(expandRtlEnabled ? (viewWidth - outlineStrokeDiff) : outlineStrokeDiff, horizontalLineY);
strokePath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), horizontalLineY);
if (outlineEnabled) {
strokePath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight + outlineStrokeDiff);
} else {
strokePath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight);
}

Scale bottom = scales.bottom();
if (bottom != null) {

if (bottom.length() > top.length()) {
strokePath.moveTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), horizontalLineY);
strokePath.lineTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), horizontalLineY);
} else {
strokePath.moveTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), horizontalLineY);
}

strokePath.lineTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), textHeight * 2);

float bottomTextY = horizontalLineY + textHeight + textHeight / 2;
if (outlineEnabled) {
canvas.drawText(bottom.text(), expandRtlEnabled ? viewWidth : 0, bottomTextY, outlinePaint);
}
canvas.drawText(bottom.text(), expandRtlEnabled ? viewWidth : 0, bottomTextY, textPaint);
}

if (outlineEnabled) {
outlinePaint.setStrokeWidth(outlineStrokeWidth);
outlineDiffPath.rewind();
outlineDiffPath.moveTo(expandRtlEnabled ? viewWidth : 0, horizontalLineY);
outlineDiffPath.lineTo(expandRtlEnabled ? (viewWidth - outlineStrokeDiff) : outlineStrokeDiff, horizontalLineY);
outlineDiffPath.moveTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight + outlineStrokeDiff);
outlineDiffPath.lineTo(expandRtlEnabled ? (viewWidth - top.length()) : top.length(), textHeight);
if (bottom != null) {
outlineDiffPath.moveTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), textHeight * 2);
outlineDiffPath.lineTo(expandRtlEnabled ? (viewWidth - bottom.length()) : bottom.length(), textHeight * 2 + outlineStrokeDiff);
}

canvas.drawPath(outlineDiffPath, outlinePaint);
canvas.drawPath(strokePath, outlinePaint);
}

canvas.drawPath(strokePath, strokePaint);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* This file includes code from MapScaleView (https://github.com/pengrad/MapScaleView),
* licensed under the Apache License, Version 2.0.
*/
package org.odk.collect.googlemaps.scaleview;

class MapScaleModel {

private static final double EQUATOR_LENGTH_METERS = 40075016.686;
private static final double EQUATOR_LENGTH_FEET = 131479713.537;

private static final int FT_IN_MILE = 5280;

private static final float[] METERS = {0.2f, 0.5f, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000,
2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000, 2000000};

private static final float[] FT = {1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,
FT_IN_MILE, 2 * FT_IN_MILE, 5 * FT_IN_MILE, 10 * FT_IN_MILE, 20 * FT_IN_MILE, 50 * FT_IN_MILE,
100 * FT_IN_MILE, 200 * FT_IN_MILE, 500 * FT_IN_MILE, 1000 * FT_IN_MILE, 2000 * FT_IN_MILE};

private final float density;
private int maxWidth;

private float lastZoom = -1;
private double lastLatitude = -100;

private double tileSizeMetersAt0Zoom = EQUATOR_LENGTH_METERS / 256;
private double tileSizeFeetAt0Zoom = EQUATOR_LENGTH_FEET / 256;

MapScaleModel(float density) {
this.density = density;
}

// returns true if width changed
boolean updateMaxWidth(int width) {
if (maxWidth != width) {
maxWidth = width;
return true;
} else {
return false;
}
}

void setTileSize(int tileSize) {
tileSizeMetersAt0Zoom = EQUATOR_LENGTH_METERS / tileSize;
tileSizeFeetAt0Zoom = EQUATOR_LENGTH_FEET / tileSize;
}

void setPosition(float zoom, double latitude) {
lastZoom = zoom;
lastLatitude = latitude;
}

/**
* See http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
*/
Scale update(boolean meters) {
float zoom = lastZoom;
double latitude = lastLatitude;
if (zoom < 0 || Math.abs(latitude) > 90) {
return null;
}

double tileSizeAtZoom0 = meters ? tileSizeMetersAt0Zoom : tileSizeFeetAt0Zoom;
float[] distances = meters ? METERS : FT;

final double resolution = tileSizeAtZoom0 / density * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom);

float distance = 0;
int distanceIndex = distances.length;
double screenDistance = maxWidth + 1;

while (screenDistance > maxWidth && distanceIndex > 0) {
distance = distances[--distanceIndex];
screenDistance = Math.abs(distance / resolution);
}

lastZoom = zoom;
lastLatitude = latitude;
return new Scale(text(distance, meters), (float) screenDistance);
}

private String text(float distance, boolean meters) {
if (meters) {
if (distance < 1) {
return (int) (distance * 100) + " cm";
} else if (distance < 1000) {
return (int) distance + " m";
} else {
return (int) distance / 1000 + " km";
}
} else {
if (distance < FT_IN_MILE) {
return (int) distance + " ft";
} else {
return (int) distance / FT_IN_MILE + " mi";
}
}
}
}
Loading