Java tutorial
/* * KnownLocation.java * Copyright (C) 2016 Nicholas Killewald * * This file is distributed under the terms of the BSD license. * The source package should have a LICENSE file at the toplevel. */ package net.exclaimindustries.geohashdroid.util; import android.app.backup.BackupManager; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.location.Location; import android.os.Parcel; import android.os.Parcelable; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.CircleOptions; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.MarkerOptions; import net.exclaimindustries.geohashdroid.R; import net.exclaimindustries.tools.LocationUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Calendar; import java.util.List; /** * This represents a single known location. It's got a LatLng and a name, as * well as a way to serialize itself out to a preference, mostly by making * itself into a JSON chunk. */ public class KnownLocation implements Parcelable { private String mName; private LatLng mLocation; private double mRange; private boolean mRestrictGraticule = false; private static final String DEBUG_TAG = "KnownLocation"; /** * Private version of the constructor used during {@link #deserialize(JSONObject)}. */ private KnownLocation() { } /** * Builds up a new KnownLocation. * * @param name the name of this mLocation * @param location a LatLng where it can be found * @param range how close it has to be before it triggers a notification, in m * @param restrictGraticule true to only consider the location's native graticule for range purposes, rather than find the closest one */ public KnownLocation(@NonNull String name, @NonNull LatLng location, double range, boolean restrictGraticule) { mName = name; mRange = range; mRestrictGraticule = restrictGraticule; // The marker needs SOME title. if (mName.isEmpty()) mName = "?"; mLocation = location; } private KnownLocation(Parcel in) { readFromParcel(in); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { // Nice how this is easily parcelable. dest.writeString(mName); dest.writeParcelable(mLocation, 0); dest.writeDouble(mRange); dest.writeByte((byte) (mRestrictGraticule ? 0 : 1)); } public void readFromParcel(Parcel in) { // Same way it went out. mName = in.readString(); mLocation = in.readParcelable(KnownLocation.class.getClassLoader()); mRange = in.readDouble(); mRestrictGraticule = in.readByte() != 0; } public static final Parcelable.Creator<KnownLocation> CREATOR = new Parcelable.Creator<KnownLocation>() { @Override public KnownLocation createFromParcel(Parcel source) { return new KnownLocation(source); } @Override public KnownLocation[] newArray(int size) { return new KnownLocation[size]; } }; /** * Deserializes a single JSONObject into a KnownLocation. * * @param obj the object to deserialize * @return a new KnownLocation, or null if something went wrong */ @Nullable public static KnownLocation deserialize(@NonNull JSONObject obj) { KnownLocation toReturn = new KnownLocation(); try { toReturn.mName = obj.getString("name"); toReturn.mLocation = new LatLng(obj.getDouble("lat"), obj.getDouble("lon")); toReturn.mRange = obj.getDouble("range"); // Well, whoops. Turns out I completely forgot to serialize this in // previous versions. We don't want this throwing an exception if // it's not there, so we'll assume false if it doesn't exist. toReturn.mRestrictGraticule = obj.optBoolean("restrictGraticule", false); return toReturn; } catch (JSONException je) { Log.e(DEBUG_TAG, "Couldn't deserialize a KnownLocation for some reason!", je); return null; } } /** * Gets all KnownLocations from Preferences and returns them as a List. * * @param c a Context * @return a List full of KnownLocations (or an empty List) */ @NonNull public static List<KnownLocation> getAllKnownLocations(@NonNull Context c) { List<KnownLocation> toReturn = new ArrayList<>(); // To the preferences! SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); String blob = prefs.getString(GHDConstants.PREF_KNOWN_LOCATIONS, "[]"); // I really hope this is a JSONArray... JSONArray arr; try { arr = new JSONArray(blob); } catch (JSONException je) { Log.e(DEBUG_TAG, "Couldn't parse the known locations JSON blob!", je); return toReturn; } // What's more, I really hope every entry in the JSONArray is a // JSONObject that happens to be a KnownLocation... for (int i = 0; i < arr.length(); i++) { try { JSONObject obj = arr.getJSONObject(i); KnownLocation kl = deserialize(obj); if (kl != null) toReturn.add(kl); } catch (JSONException je) { Log.e(DEBUG_TAG, "Item " + i + " in the known locations JSON blob wasn't a JSONObject!", je); } } return toReturn; } /** * Serializes this out into a single JSONObject. * * @return a JSONObject that can be used to store this data. */ @NonNull public JSONObject serialize() { JSONObject toReturn = new JSONObject(); try { toReturn.put("name", mName); toReturn.put("lat", mLocation.latitude); toReturn.put("lon", mLocation.longitude); toReturn.put("range", mRange); toReturn.put("restrictGraticule", mRestrictGraticule); } catch (JSONException je) { // This really, REALLY shouldn't happen. Really. Log.e("KnownLocation", "JSONException trying to add data into the to-return object? The hell?", je); } return toReturn; } /** * Stores a bunch of KnownLocations to preferences. Note that this <b>replaces</b> * all currently-stored KnownLocations. * * @param c a Context * @param locations a List of KnownLocations */ public static void storeKnownLocations(@NonNull Context c, @NonNull List<KnownLocation> locations) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); SharedPreferences.Editor edit = prefs.edit(); JSONArray arr = new JSONArray(); for (KnownLocation kl : locations) { arr.put(kl.serialize()); } // Man, that's easy. edit.putString(GHDConstants.PREF_KNOWN_LOCATIONS, arr.toString()); edit.apply(); BackupManager bm = new BackupManager(c); bm.dataChanged(); } /** * Gets the name of this KnownLocation. * * @return a name */ @NonNull public String getName() { return mName; } /** * Gets the LatLng this KnownLocation represents. * * @return a LatLng */ @NonNull public LatLng getLatLng() { return mLocation; } /** * Gets the range (in m) required before this KnownLocation will trigger a * notification. * * @return the range */ public double getRange() { return mRange; } /** * <p> * Returns whether or not this KnownLocation is graticule-restricted. That * is, if it should ONLY compare the location's native Graticule when * looking for the closest Info. * </p> * * @return true if this is graticule-restricted, false if not */ public boolean isRestrictedGraticule() { return mRestrictGraticule; } /** * Convenience method to determine the distance from this KnownLocation to * the given Info. * * @param info the Info to check * @return the distance from here to the Info, in meters */ public double getDistanceFrom(@NonNull Info info) { return (double) LocationUtil.latLngToLocation(mLocation).distanceTo(info.getFinalLocation()); } /** * Determines if this KnownLocation is close enough to the given coordinates * to trigger a notification. Note that if the range was specified as zero * or less, this will always return false. * * @param to the LatLng to which this is being compared * @return true if close enough, false if not */ public boolean isCloseEnough(@NonNull LatLng to) { if (mRange <= 0.0) return false; // Stupid LatLngs. I didn't have to deal with these conversions back // when everything just used Locations... float dist[] = new float[1]; Location.distanceBetween(mLocation.latitude, mLocation.longitude, to.latitude, to.longitude, dist); return dist[0] <= mRange; } /** * Determines the closest non-globalhash Info to this KnownLocation for the * given date. That is, it will check all nine graticules around this * KnownLocation and figures out which has the closest hashpoint. Note that * if graticule restriction is on for this KnownLocation, it will ALWAYS * return the Info for the graticule in which this location lives. * * @param con a Context so we can get additional Infos * @param cal a Calendar representing the date to use * @return the closest Info to this KnownLocation * @throws IllegalArgumentException if there isn't any stock data for the given Calendar */ @NonNull public Info getClosestInfo(@NonNull Context con, @NonNull Calendar cal) throws IllegalArgumentException { // Get us a base Graticule. Graticule base = new Graticule(mLocation); // If we're in graticule restriction, short-circuit it to ONLY stick // to the base Graticule. if (mRestrictGraticule) { Info info = HashBuilder.getStoredInfo(con, cal, base); if (info == null) throw new IllegalArgumentException("Info didn't exist in the cache for that date!"); return info; } double bestSoFar = Double.MAX_VALUE; Info bestInfo = null; for (int i = -1; i <= 1; i++) { for (int j = -1; j <= 1; j++) { // Offset the base Graticule, if need be... Graticule check = base; if (i != 0 && j != 0) { check = Graticule.createOffsetFrom(base, i, j); } // Okay, now we can get an Info... Info info = HashBuilder.getStoredInfo(con, cal, check); if (info == null) { // If the info is ever null, we're asking for a date that // doesn't exist yet. Doesn't matter if some of the infos // in this loop succeeded (unless we already returned true); // ALL the infos SHOULD ALWAYS exist if any of them do, so // that's still really really bad. throw new IllegalArgumentException("Info didn't exist in the cache for that date!"); } // Now, how close is it? double dist = getDistanceFrom(info); if (dist < bestSoFar) { bestSoFar = dist; bestInfo = info; } } } // Well, whatever we have, it's the closest! if (bestInfo == null) throw new IllegalArgumentException("Couldn't find any Infos at all to compare! The hell?"); return bestInfo; } /** * <p> * Makes a MarkerOptions out of this KnownLocation (when added to the map, * you get the actual Marker back). This can be directly placed on the map, * but you might want to stick it in something that can build a cluster or * something. * </p> * * <p> * Note that this MarkerOptions won't have a snippet. The caller has to set * that itself. The title, though, will be the KnownLocation's name. * </p> * * @param c a Context * @return a MarkerOptions representing this KnownLocation */ @NonNull public MarkerOptions makeMarker(@NonNull Context c) { MarkerOptions toReturn = new MarkerOptions(); toReturn.flat(false).draggable(false).icon(BitmapDescriptorFactory.fromBitmap(buildMarkerBitmap(c))) .anchor(0.5f, 1.0f).position(mLocation).title(mName); // The snippet should be set by the caller. That'll either be // instructions to tap it again to edit/add it or the distance from it // to the hashpoint. return toReturn; } /** * Makes a CircleOptions out of this KnownLocation (when added to the map, * you get the actual Circle back). This is used in KnownLocationsPicker to * give the user a better idea of what the range looks like. * * @param c a Context * @return a CircleOptions representing this KnownLocation's location and range */ @NonNull public CircleOptions makeCircle(@NonNull Context c) { CircleOptions toReturn = new CircleOptions(); KnownLocationPinData data = new KnownLocationPinData(c, mLocation); int baseColor = data.getColor(); toReturn.center(mLocation).radius(mRange) .strokeWidth(c.getResources().getDimension(R.dimen.known_location_circle_stroke_width)) .strokeColor(Color.argb(c.getResources().getInteger(R.integer.known_location_circle_stroke_alpha), Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor))) .fillColor(Color.argb(c.getResources().getInteger(R.integer.known_location_circle_alpha), Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor))); return toReturn; } @NonNull private Bitmap buildMarkerBitmap(@NonNull Context c) { // Oh, this is going to be FUN. int dim = c.getResources().getDimensionPixelSize(R.dimen.known_location_marker_canvas_size); float radius = c.getResources().getDimension(R.dimen.known_location_pin_head_radius); Bitmap bitmap = Bitmap.createBitmap(dim, dim, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(); paint.setAntiAlias(true); KnownLocationPinData pinData = new KnownLocationPinData(c, mLocation); // Draw the pin line first. That goes from the bottom-center up to // wherever the radius and length take us. float topX = Double.valueOf((dim / 2) + (pinData.getLength() * Math.cos(pinData.getAngle()))).floatValue(); float topY = Double.valueOf(dim - (pinData.getLength() * Math.sin(pinData.getAngle()))).floatValue(); paint.setStrokeWidth(c.getResources().getDimension(R.dimen.known_location_stroke)); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.BLACK); canvas.drawLine(dim / 2, dim, topX, topY, paint); // On the top of that line, fill in a circle. paint.setColor(pinData.getColor()); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(topX, topY, radius, paint); // And outline it. paint.setColor(Color.BLACK); paint.setStyle(Paint.Style.STROKE); canvas.drawCircle(topX, topY, radius, paint); return bitmap; } @Override public String toString() { return "\"" + mName + "\": " + mLocation.latitude + ", " + mLocation.longitude; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof KnownLocation)) return false; // Locations should have identical names, locations, and ranges if // we're expecting them to be identical. final KnownLocation other = (KnownLocation) o; return (mName == null ? other.mName == null : mName.equals(other.mName)) && mRange == other.mRange && (mLocation == null ? other.mLocation == null : mLocation.equals(other.mLocation)); } @Override public int hashCode() { // Good thing there's only three fields to hash up... int toReturn = 17; long convert = Double.doubleToLongBits(mRange); toReturn = 31 * toReturn + (int) (convert & (convert >>> 32)); toReturn = 31 * toReturn + (mLocation == null ? 0 : mLocation.hashCode()); toReturn = 31 * toReturn + (mName == null ? 0 : mName.hashCode()); return toReturn; } }