net.exclaimindustries.geohashdroid.util.KnownLocation.java Source code

Java tutorial

Introduction

Here is the source code for net.exclaimindustries.geohashdroid.util.KnownLocation.java

Source

/*
 * 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;
    }
}