package org.azavea.otm.data; import android.location.Address; import android.location.Geocoder; import android.util.Log; import com.google.common.base.Joiner; import com.loopj.android.http.BinaryHttpResponseHandler; import org.azavea.helpers.JSONHelper; import org.azavea.helpers.Logger; import org.azavea.otm.App; import org.azavea.otm.rest.RequestGenerator; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import static com.google.common.collect.Collections2.filter; import static com.google.common.collect.Lists.newArrayList; public class Plot extends Model { public static final String PLOT = "plot"; public static final String ID = "id"; public static final String TITLE = "title"; public static final String ADDRESS_STREET = "address_street"; public static final String ADDRESS_CITY = "address_city"; public static final String ADDRESS_ZIP = "address_zip"; public static final String TREE = "tree"; public static final String HAS_TREE = "has_tree"; public static final String GEOM = "geom"; public static final String PHOTOS = "photos"; public static final String PHOTO_IMAGE = "image"; public static final String PHOTO_THUMBNAIL = "thumbnail"; private PendingStatus hasPending = PendingStatus.Unset; private JSONObject plotDetails = null; private Species species = null; enum PendingStatus { Pending, NoPending, Unset } /** * When Requesting a plot tree photo, these are the valid image types */ public static String[] IMAGE_TYPES = new String[]{ "image/jpeg", "image/png", "image/gif" }; public Plot() { try { // Basic empty plot json structure JSONObject fullPlot = new JSONObject(); plotDetails = new JSONObject(); fullPlot.put(PLOT, plotDetails); this.setData(fullPlot); } catch (JSONException e) { Logger.error("Error creating empty plot", e); } } public Plot(JSONObject data) { setData(data); } @Override public void setData(JSONObject data) { super.setData(data); setupPlotDetails(); } @Override public void setValueForKey(String key, Object value) throws Exception { // Make a tree if this key is for a tree and this plot doesn't have a tree if (key.split("[.]")[0].equals("tree") && !hasTree() && !JSONObject.NULL.equals(value)) { this.createTree(); } super.setValueForKey(key, value); } private void setupPlotDetails() { try { this.plotDetails = this.data.optJSONObject(PLOT); if (!JSONObject.NULL.equals(plotDetails) && this.hasTree()) { Tree tree = this.getTree(); JSONObject speciesData = tree.getSpecies(); if (speciesData != null) { this.species = new Species(); species.setData(speciesData); } } } catch (JSONException e) { Logger.error("Error loading plot information", e); this.plotDetails = null; } } public int getId() throws JSONException { return plotDetails.getInt(ID); } public String getTitle() { return this.data.optString(TITLE, null); } public String getAddress() { final String streetAddress = JSONHelper.safeGetString(plotDetails, ADDRESS_STREET); final String city = JSONHelper.safeGetString(plotDetails, ADDRESS_CITY); final String zip = JSONHelper.safeGetString(plotDetails, ADDRESS_ZIP); Collection<String> addresses = filter(newArrayList(streetAddress, city, zip), s -> s != null); return Joiner.on(", ").join(addresses); } public void setAddressFromGeocoder(Geocoder geocoder) { Geometry geom = getGeometry(); if (geom == null) { Log.e(App.LOG_TAG, "Cannot set Address for a plot with no geometry"); setAddressFields(null, null, null); return; } List<Address> addresses; try { addresses = geocoder.getFromLocation(geom.getY(), geom.getX(), 1); } catch (Exception e) { Logger.warning("Error Geocoding address", e); setAddressFields(null, null, null); return; } if ((addresses != null) && (addresses.size() != 0)) { Address addressData = addresses.get(0); String streetAddress = addressData.getMaxAddressLineIndex() == 0 ? null : addressData.getAddressLine(0); setAddressFields(streetAddress, addressData.getLocality(), addressData.getPostalCode()); } } private void setAddressFields(String streetAddress, String city, String zip) { try { plotDetails.put(ADDRESS_CITY, city); plotDetails.put(ADDRESS_STREET, streetAddress); plotDetails.put(ADDRESS_ZIP, zip); } catch (JSONException e) { Logger.error("Error saving geocoded address", e); } } public String getLastUpdated() throws JSONException { return data.getJSONObject("latest_update").getString("created"); } public void setLastUpdated(String lastUpdated) throws JSONException { data.put("last_updated", lastUpdated); } public String getLastUpdatedBy() throws JSONException { JSONObject lastUser = data.getJSONArray("recent_activity").getJSONObject(0); if (lastUser != null) { return lastUser.getString("username"); } return ""; } public void setLastUpdatedBy(String lastUpdatedBy) throws JSONException { data.put("last_updated_by", lastUpdatedBy); } public Tree getTree() throws JSONException { if (data.isNull(TREE)) { return null; } Tree retTree = new Tree(this); retTree.setData(data.getJSONObject(TREE)); return retTree; } public void setTree(Tree tree) throws JSONException { data.put(TREE, tree.getData()); data.put(HAS_TREE, true); } public Geometry getGeometry() { if (plotDetails.isNull(GEOM)) { return null; } Geometry retGeom = new Geometry(); retGeom.setData(plotDetails.optJSONObject(GEOM)); return retGeom; } public void setGeometry(Geometry geom) throws JSONException { plotDetails.put(GEOM, geom.getData()); } /** * Does this plot have current pending edits? * * @throws JSONException */ public boolean hasPendingEdits() throws JSONException { // Use the cache if available, this might be called a lot if (hasPending != PendingStatus.Unset) { return hasPending == PendingStatus.Pending; } boolean pendings = false; if (!data.isNull("pending_edits")) { if (data.getJSONObject("pending_edits").length() > 0) { pendings = true; } } // Cache for this instance hasPending = pendings ? PendingStatus.Pending : PendingStatus.NoPending; return pendings; } /** * Get a pending edit description for a given field key for a plot or tree * * @param key name of field key * @return An object representing a pending edit description for the field, * or null if there are no pending edits * @throws JSONException */ public PendingEditDescription getPendingEditForKey(String key) throws JSONException { if (this.hasPendingEdits()) { JSONObject edits = data.getJSONObject("pending_edits"); if (!edits.isNull(key)) { return new PendingEditDescription(key, edits.getJSONObject(key)); } } return null; } public boolean hasTree() { return data.optBoolean(HAS_TREE, false); } public void createTree() throws JSONException { this.setTree(new Tree()); } public JSONObject getMostRecentPhoto() { JSONArray photos = data.optJSONArray(PHOTOS); if (photos != null && photos.length() > 0 && this.hasTree()) { List<JSONObject> photoObjects = new ArrayList<>(photos.length()); for (int i = 0; i < photos.length(); i++) { JSONObject photo = photos.optJSONObject(i); // If we start supporting multiple trees, we'll need to check the tree id here if (photo != null && photo.optInt(ID) != 0 && photo.has(PHOTO_IMAGE) && photo.has(PHOTO_THUMBNAIL)) { photoObjects.add(photo); } } return Collections.max(photoObjects, (a, b) -> a.optInt(ID) - b.optInt(ID)); } return null; } /** * Get the most recent tree thumbnail for this plot, by way of an * asynchronous response handler. * * @param handler image handler which will receive callback from async http * request */ public void getTreeThumbnail(BinaryHttpResponseHandler handler) { getTreeImage(PHOTO_THUMBNAIL, handler); } public void getTreePhoto(BinaryHttpResponseHandler handler) { getTreeImage(PHOTO_IMAGE, handler); } private void getTreeImage(String name, BinaryHttpResponseHandler handler) { JSONObject photo = this.getMostRecentPhoto(); if (photo != null) { String url = photo.optString(name); if (url != null) { RequestGenerator rg = new RequestGenerator(); rg.getImage(url, handler); } } } public void assignNewTreePhoto(JSONObject image) throws JSONException { JSONArray photos = data.optJSONArray(PHOTOS); if (photos == null) { photos = new JSONArray(); data.put(PHOTOS, photos); } photos.put(image); } public String getScienticName() { if (this.species != null) { return this.species.getScientificName(); } return null; } public String getCommonName() { if (this.species != null) { return this.species.getCommonName(); } return null; } /** * Get an updated georevhash from a plot update, or the current one if no * new one exists */ public String getUpdatedGeoRev() { return this.data.optString("geoRevHash", App.getAppInstance().getCurrentInstance().getGeoRevId()); } /** * @param pendingKey: the key to get pending edits for */ public String getValueForLatestPendingEdit(String pendingKey) { // get the pending edit description object for this plot PendingEditDescription ped; try { ped = getPendingEditForKey(pendingKey); } catch (JSONException e) { Log.e(App.LOG_TAG, "JSON exception reading pending edit field", e); return ""; } // get a list of pending edits List<PendingEdit> pendingEditList; try { pendingEditList = ped.getPendingEdits(); } catch (JSONException e) { Log.e(App.LOG_TAG, "JSON exception reading pending edit field", e); return ""; } // I assert that the most recent one is the first one. (argh.) PendingEdit mostRecentPendingEdit; if (pendingEditList.size() != 0) { mostRecentPendingEdit = pendingEditList.get(0); } else { return ""; } String value; try { value = mostRecentPendingEdit.getValue(); } catch (JSONException e) { value = null; } return value; } }