Skip to content

Commit

Permalink
Automatically choose a new color for additional layers of the same type
Browse files Browse the repository at this point in the history
Fixes #2197
  • Loading branch information
simonpoole committed Sep 16, 2023
1 parent 5c85dad commit 8eb68d8
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 60 deletions.
20 changes: 19 additions & 1 deletion src/main/java/de/blau/android/Map.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public void setUpLayers(@NonNull Context ctx) {
layer = new de.blau.android.layer.tasks.MapOverlay(this);
break;
case GEOJSON:
layer = new de.blau.android.layer.geojson.MapOverlay(this);
layer = new de.blau.android.layer.geojson.MapOverlay(this, contentId);
if (!((de.blau.android.layer.geojson.MapOverlay) layer).loadGeoJsonFile(ctx, Uri.parse(contentId), true)) {
// other error, has already been toasted
layer = null; // this will delete the layer
Expand Down Expand Up @@ -376,6 +376,24 @@ private List<MapViewLayer> getLayers(@NonNull LayerType type, @Nullable String c
return result;
}

/**
* Get a count of layers of a specific type
*
* @param type the LayerType
* @return a count
*/
public int getLayerTypeCount(@NonNull LayerType type) {
int count = 0;
synchronized (mLayers) {
for (MapViewLayer l : mLayers) {
if (l.getType().equals(type)) {
count++;
}
}
}
return count;
}

/**
* Get the list of configured layers
*
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/de/blau/android/layer/StyleableFileLayer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package de.blau.android.layer;

import java.io.InputStream;

import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import de.blau.android.contract.FileExtensions;
import de.blau.android.util.Hash;

/**
* StyleableLayer that is loaded from a file
*
* @author simon
*
*/
public abstract class StyleableFileLayer extends StyleableLayer {

private static final long serialVersionUID = 1L;

/**
* State file file name
*/
protected String stateFileName;

protected String contentId; // could potentially be transient

protected StyleableFileLayer(@NonNull String contentId, String defaultStateFileName) {
this.contentId = contentId;
this.stateFileName = defaultStateFileName;
}

/**
* Check if we have a state file
*
* @param context an Android Context
* @return true if a state file exists
*/
protected boolean hasStateFile(@NonNull Context context) {
setStateFileName(Uri.parse(contentId).getEncodedPath());
try (InputStream stream = context.openFileInput(stateFileName)) {
return true;
} catch (Exception ex) {
return false;
}
}

/**
* Set the name of the state file
*
* This needs to be unique across all instances so best an encoded uri, to avoid filename length issues we use the
* SHA-256 hash
*
* @param baseName the base name for this specific instance
*/
protected void setStateFileName(@NonNull String baseName) {
stateFileName = Hash.sha256(baseName) + "." + FileExtensions.RES;
}
}
47 changes: 20 additions & 27 deletions src/main/java/de/blau/android/layer/geojson/MapOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import de.blau.android.layer.LabelMinZoomInterface;
import de.blau.android.layer.LayerInfoInterface;
import de.blau.android.layer.LayerType;
import de.blau.android.layer.StyleableFileLayer;
import de.blau.android.layer.StyleableLayer;
import de.blau.android.osm.BoundingBox;
import de.blau.android.osm.OsmXml;
Expand All @@ -64,13 +65,13 @@
import de.blau.android.resources.DataStyle;
import de.blau.android.resources.DataStyle.FeatureStyle;
import de.blau.android.resources.symbols.TriangleDown;
import de.blau.android.util.ColorUtil;
import de.blau.android.util.ContentResolverUtil;
import de.blau.android.util.ExecutorTask;
import de.blau.android.util.FileUtil;
import de.blau.android.util.GeoJSONConstants;
import de.blau.android.util.GeoJson;
import de.blau.android.util.GeoMath;
import de.blau.android.util.Hash;
import de.blau.android.util.SavingHelper;
import de.blau.android.util.SerializableTextPaint;
import de.blau.android.util.Snack;
Expand All @@ -79,7 +80,7 @@
import de.blau.android.util.rtree.RTree;
import de.blau.android.views.IMapView;

public class MapOverlay extends StyleableLayer
public class MapOverlay extends StyleableFileLayer
implements Serializable, ExtentInterface, DiscardInterface, ClickableInterface<Feature>, LayerInfoInterface, LabelMinZoomInterface {

private static final long serialVersionUID = 4L;
Expand Down Expand Up @@ -183,20 +184,20 @@ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassN
*/
private String uri;

/**
* State file file name
*/
private String stateFileName = FILENAME;

/**
* Construct this layer
*
* @param map the Map object we are displayed on
* @param contentId the id for the current contents
*/
public MapOverlay(final Map map) {
public MapOverlay(@NonNull final Map map, @NonNull String contentId) {
super(contentId, FILENAME);
this.map = map;
final Preferences prefs = map.getPrefs();
initStyling(prefs.getGeoJsonStrokeWidth(), prefs.getGeoJsonLabelSource(), prefs.getGeoJsonLabelMinZoom(), prefs.getGeoJsonSynbol());
initStyling(!hasStateFile(map.getContext()), prefs.getGeoJsonStrokeWidth(), prefs.getGeoJsonLabelSource(), prefs.getGeoJsonLabelMinZoom(),
prefs.getGeoJsonSynbol());
paint.setColor(
ColorUtil.generateColor(map.getLayerTypeCount(LayerType.GEOJSON), 9, DataStyle.getInternal(DataStyle.GEOJSON_DEFAULT).getPaint().getColor()));
}

@Override
Expand Down Expand Up @@ -420,7 +421,7 @@ protected Boolean doInBackground(Void arg) {
if (name == null) {
name = uri.getLastPathSegment();
}
setFileName(uri.getEncodedPath());
setStateFileName(uri.getEncodedPath());
MapOverlay.this.uri = uri.toString();
return loadGeoJsonFile(ctx, is, fromState);
} catch (SecurityException sex) {
Expand All @@ -444,17 +445,6 @@ protected Boolean doInBackground(Void arg) {
}
}

/**
* Set the name of the state file
*
* This needs to be unique across all instances so best an encoded uri
*
* @param baseName the base name for this specific instance
*/
private void setFileName(@NonNull String baseName) {
stateFileName = Hash.sha256(baseName) + "." + FileExtensions.RES;
}

/**
* Read an InputStream containing GeoJSON data in to the layer, replacing any existing data
*
Expand Down Expand Up @@ -709,25 +699,28 @@ public List<Feature> getFeatures() {

@Override
public void resetStyling() {
initStyling(DataStyle.DEFAULT_GEOJSON_STROKE_WIDTH, "", Map.SHOW_LABEL_LIMIT, TriangleDown.NAME);
initStyling(true, DataStyle.DEFAULT_GEOJSON_STROKE_WIDTH, "", Map.SHOW_LABEL_LIMIT, TriangleDown.NAME);
}

/**
* Init the styling to the provided values
*
* @param style if true set styling
* @param strokeWidth the stroke width
* @param labelKey the source of the label
* @param labelMinZoom min. zoom from on we show the label
* @param symbolName the name of the point symbol
*/
private void initStyling(float strokeWidth, @NonNull String labelKey, int labelMinZoom, String symbolName) {
private void initStyling(boolean style, float strokeWidth, @NonNull String labelKey, int labelMinZoom, String symbolName) {
paint = new SerializableTextPaint(DataStyle.getInternal(DataStyle.GEOJSON_DEFAULT).getPaint());
setStrokeWidth(strokeWidth);
setLabel(labelKey);
setLabelMinZoom(labelMinZoom);
iconRadius = map.getIconRadius();
setPointSymbol(symbolName);
marker = DataStyle.getCurrent().getSymbol(TriangleDown.NAME);
if (style) {
setStrokeWidth(strokeWidth);
setLabel(labelKey);
setLabelMinZoom(labelMinZoom);
setPointSymbol(symbolName);
}
}

@Override
Expand Down
57 changes: 25 additions & 32 deletions src/main/java/de/blau/android/layer/gpx/MapOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import de.blau.android.layer.LabelMinZoomInterface;
import de.blau.android.layer.LayerInfoInterface;
import de.blau.android.layer.LayerType;
import de.blau.android.layer.StyleableFileLayer;
import de.blau.android.layer.StyleableLayer;
import de.blau.android.osm.BoundingBox;
import de.blau.android.osm.Server;
Expand All @@ -54,10 +55,10 @@
import de.blau.android.resources.DataStyle.FeatureStyle;
import de.blau.android.resources.symbols.TriangleDown;
import de.blau.android.services.TrackerService;
import de.blau.android.util.ColorUtil;
import de.blau.android.util.ContentResolverUtil;
import de.blau.android.util.ExecutorTask;
import de.blau.android.util.GeoMath;
import de.blau.android.util.Hash;
import de.blau.android.util.PlaybackTask;
import de.blau.android.util.SavingHelper;
import de.blau.android.util.SerializableTextPaint;
Expand All @@ -66,7 +67,7 @@
import de.blau.android.util.collections.FloatPrimitiveList;
import de.blau.android.views.IMapView;

public class MapOverlay extends StyleableLayer
public class MapOverlay extends StyleableFileLayer
implements Serializable, ExtentInterface, ClickableInterface<WayPoint>, LayerInfoInterface, LabelMinZoomInterface {

private static final long serialVersionUID = 5L; // note that this can't actually be serialized as the transient
Expand All @@ -91,7 +92,6 @@ public class MapOverlay extends StyleableLayer
private SerializableTextPaint wayPointPaint;
private String labelKey;
private int labelMinZoom;
private String contentId; // could potentially be transient
private TrackPoint pausedPoint;

// way point label styling
Expand All @@ -101,29 +101,28 @@ public class MapOverlay extends StyleableLayer
private final transient Paint fontPaint;
private final transient List<String> labelList;

/**
* State file file name
*/
private String stateFileName = FILENAME;

/**
* Construct a new GPX layer
*
* @param map the current Map instance
* @param contentId the id for the current contents
*/
public MapOverlay(@NonNull final Map map, @NonNull String contentId) {
super(contentId, FILENAME);
this.map = map;
this.contentId = contentId;
Context context = map.getContext();
final Preferences prefs = map.getPrefs();
initStyling(prefs.getGpxStrokeWidth(), prefs.getGpxLabelSource(), prefs.getGpxLabelMinZoom(), prefs.getGpxSynbol());
// this is slightly annoying as we need to protect against overwriting already saved state
initStyling(!hasStateFile(context), prefs.getGpxStrokeWidth(), prefs.getGpxLabelSource(), prefs.getGpxLabelMinZoom(), prefs.getGpxSynbol());
paint.setColor(ColorUtil.generateColor(map.getLayerTypeCount(LayerType.GPX), 9, DataStyle.getInternal(DataStyle.GPS_TRACK).getPaint().getColor()));

// the following can only be changed in the DataStyle
FeatureStyle fs = DataStyle.getInternal(DataStyle.LABELTEXT_NORMAL);
fontPaint = fs.getPaint();
fm = fs.getFontMetrics();
labelBackground = DataStyle.getInternal(DataStyle.LABELTEXT_BACKGROUND).getPaint();
yOffset = 2 * fontPaint.getStrokeWidth() + iconRadius;
Context context = map.getContext();

labelList = Arrays.asList(context.getString(R.string.gpx_automatic), context.getString(R.string.gpx_name), context.getString(R.string.gpx_description),
context.getString(R.string.gpx_type));

Expand Down Expand Up @@ -382,32 +381,35 @@ public void setStrokeWidth(float width) {

@Override
public void resetStyling() {
initStyling(DataStyle.DEFAULT_GPX_STROKE_WIDTH, labelList.get(0), Map.SHOW_LABEL_LIMIT, TriangleDown.NAME);
initStyling(true, DataStyle.DEFAULT_GPX_STROKE_WIDTH, labelList.get(0), Map.SHOW_LABEL_LIMIT, TriangleDown.NAME);
}

/**
* Set the styling to the provided values
*
* @param style if true set styling
* @param strokeWidth the stroke width
* @param labelKey the source of the label
* @param labelMinZoom min. zoom from on we show the label
* @param symbolName the name of the point symbol
*/
private void initStyling(float strokeWidth, @NonNull String labelKey, int labelMinZoom, @NonNull String symbolName) {
private void initStyling(boolean style, float strokeWidth, @NonNull String labelKey, int labelMinZoom, @NonNull String symbolName) {
paint = new SerializableTextPaint(DataStyle.getInternal(DataStyle.GPS_TRACK).getPaint());
wayPointPaint = new SerializableTextPaint(DataStyle.getInternal(DataStyle.GPS_POS_FOLLOW).getPaint());

paint.setStrokeWidth(strokeWidth);
// currently styling always sets the waypoint stroke width to the same as the track
wayPointPaint.setStrokeWidth(strokeWidth);
setLabel(labelKey);
setLabelMinZoom(labelMinZoom);
iconRadius = map.getIconRadius();
setPointSymbol(symbolName);
if (style) {
paint.setStrokeWidth(strokeWidth);
// currently styling always sets the waypoint stroke width to the same as the track
wayPointPaint.setStrokeWidth(strokeWidth);
setLabel(labelKey);
setLabelMinZoom(labelMinZoom);
setPointSymbol(symbolName);
}
}

@Override
public void setLabel(String key) {
dirty();
labelKey = key;
map.getPrefs().setGpxLabelSource(key);
}
Expand All @@ -424,6 +426,7 @@ public String getLabel() {

@Override
public void setLabelMinZoom(int minZoom) {
dirty();
labelMinZoom = minZoom;
map.getPrefs().setGpxLabelMinZoom(minZoom);
}
Expand Down Expand Up @@ -487,20 +490,9 @@ protected Integer doInBackground(Void arg) {
}
}

/**
* Set the name of the state file
*
* This needs to be unique across all instances so best an encoded uri, to avoid filename length issues we use the
* SHA-256 hash
*
* @param baseName the base name for this specific instance
*/
private void setStateFileName(@NonNull String baseName) {
stateFileName = Hash.sha256(baseName) + "." + FileExtensions.RES;
}

@Override
public synchronized boolean save(@NonNull Context context) throws IOException {
Log.d(DEBUG_TAG, "Saving state to " + stateFileName);
if (playbackTask != null) {
playbackTask.pause();
pausedPoint = playbackTask.getPausedPoint();
Expand All @@ -511,6 +503,7 @@ public synchronized boolean save(@NonNull Context context) throws IOException {

@Override
public synchronized StyleableLayer load(@NonNull Context context) {
Log.d(DEBUG_TAG, "Loading state from " + stateFileName);
MapOverlay restoredOverlay = savingHelper.load(context, stateFileName, true);
if (restoredOverlay != null) {
Log.d(DEBUG_TAG, "read saved state");
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/de/blau/android/util/ColorUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.blau.android.util;

import androidx.core.graphics.ColorUtils;

public final class ColorUtil {

/**
Expand Down Expand Up @@ -38,4 +40,18 @@ public static int argb(int a, int r, int g, int b) {
public static int argb(float alpha, float r, float g, float b) {
return argb(Math.round(alpha * 255), Math.round(r * 255), Math.round(g * 255), Math.round(b * 255));
}

/**
* Generate a color
*
* @param index index of the color to create
* @param steps the number of steps in HSL space
* @param seed the initial color (index == 0 will return this)
* @return a color value
*/
public static int generateColor(int index, int steps, int seed) {
float[] hsl = new float[3];
ColorUtils.colorToHSL(seed, hsl);
return ColorUtils.HSLToColor(new float[] { (hsl[0] + (index * 360f / steps)) % 360f, hsl[1], hsl[2] });
}
}

0 comments on commit 8eb68d8

Please sign in to comment.