Skip to content

Commit

Permalink
Merge pull request #6379 from grzesiek2010/COLLECT-6233
Browse files Browse the repository at this point in the history
Show scale on map
  • Loading branch information
grzesiek2010 authored Sep 9, 2024
2 parents 35d05ec + b3aa991 commit c7f5b15
Show file tree
Hide file tree
Showing 11 changed files with 640 additions and 11 deletions.
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

0 comments on commit c7f5b15

Please sign in to comment.