xyz.thepathfinder.android.Cluster Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pathfinder-android Show documentation
Show all versions of pathfinder-android Show documentation
Android/Java client library for the Pathfinder service
package xyz.thepathfinder.android;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* A cluster represents a set of commodities, sub-clusters, and transports.
* A cluster is used to group related models. That relation could be geographic
* type of transportation, or anything else. To sync the cluster with a
* cluster on the Pathfinder server use the {@link Cluster#connect} method.
*
*
*
* Note, that clusters, as are all models, are implemented as singletons.
* If a cluster already exists with the same path it will be returned, not a
* new object. Use {@link Cluster#getInstance} to retrieve an cluster object.
*
*
* @author David Robinson
* @see Commodity
* @see ClusterListener
* @see Transport
*/
public class Cluster extends SubscribableCrudModel {
/**
* String used in the model field of the pathfinder requests.
*/
private static final String MODEL = Pathfinder.CLUSTER;
/**
* Maps a path in the form of a string to the commodities directly under this cluster
*/
private Map commodities;
/**
* Maps a path in the form of a string to the sub-clusters directly under this cluster
*/
private Map subclusters;
/**
* Maps a path in the form of a string to the transports directly under this cluster
*/
private Map transports;
/**
* List of routes for this cluster. The routes object is an immutable list.
* The object should always be reassigned, never mutated.
*/
private List routes;
/**
* Constructor for a cluster object. This should called by {@link #getInstance(String, PathfinderServices)}.
* Each path must only refer to one object.
*
* @param path The path name of the cluster.
* @param services A service object to send messages to the server and keep track of all
* {@link Model} objects.
*/
private Cluster(String path, PathfinderServices services) {
super(path, services);
this.transports = new HashMap();
this.commodities = new HashMap();
this.subclusters = new HashMap();
this.routes = new ArrayList();
boolean isRegistered = this.getServices().getRegistry().isModelRegistered(path);
if (isRegistered) {
throw new IllegalArgumentException("Cluster path already exists: " + path);
} else {
this.getServices().getRegistry().registerModel(this);
}
}
/**
* Returns an instance of cluster object based on the path parameter. If there is
* a cluster with that path already created, it will return that cluster object.
* If there isn't a cluster with that path already created, it will create a new
* cluster object. If the path is associated with a different model type it will
* throw a IllegalArgumentException.
*
* @param path The full path to pathfinder model
* @param services The pathfinder services object.
* @return A cluster with the specified path. If path is an empty string it returns
* null.
* @throws IllegalArgumentException if the requested path is already associated with a
* different {@link Model} type.
*/
protected static Cluster getInstance(String path, PathfinderServices services) {
Cluster cluster = (Cluster) services.getRegistry().getModel(path);
if (cluster == null && !path.equals("")) {
cluster = new Cluster(path, services);
}
return cluster;
}
/**
* Returns an instance of cluster object based on the path in clusterJson. If there is
* a cluster with that path already created, it will return that cluster object with updated fields.
* If there isn't a cluster with that path already created, it will create a new
* cluster object and update the fields. If the path is associated with a different model type or
* the json will not parse to a cluster object's required fields it will throw a
* IllegalArgumentException.
*
* @param clusterJson A json object that parses to a cluster.
* @param services The pathfinder services object.
* @return A cluster with the specified path.
* @throws IllegalArgumentException if the requested path is already associated with a
* different {@link Model} type. Also, if the json object doesn't parse to
* a cluster object.
*/
protected static Cluster getInstance(JsonObject clusterJson, PathfinderServices services) {
boolean canParseToCluster = Cluster.checkClusterFields(clusterJson);
if (!canParseToCluster) {
throw new IllegalArgumentException("JSON could not be parsed to a cluster");
}
String path = Cluster.getPath(clusterJson);
Cluster cluster = Cluster.getInstance(path, services);
cluster.notifyUpdate(null, clusterJson);
return cluster;
}
/**
* Checks the json object for all the required fields of a cluster object.
*
* @param clusterJson JsonObject to check if it will parse to a cluster.
* @return Whether the json can be parsed to a cluster.
*/
private static boolean checkClusterFields(JsonObject clusterJson) {
return clusterJson.has("path") &&
clusterJson.has("transports") &&
clusterJson.get("transports").isJsonArray() &&
clusterJson.has("commodities") &&
clusterJson.get("commodities").isJsonArray() &&
clusterJson.has("subClusters") &&
clusterJson.get("subClusters").isJsonArray();
}
/**
* Returns the path as a string from a JSON object formatted as cluster.
*
* @param clusterJson JSON object that represents a cluster
* @return The path of the cluster
*/
private static String getPath(JsonObject clusterJson) {
return clusterJson.get("path").getAsString();
}
/**
* Creates an unconnected commodity under this cluster.
*
* @param name Name of the commodity, it must form a unique identifier when concatenated with this cluster's path.
* @param startLatitude The pick up latitude of the commodity.
* @param startLongitude The pick up longitude of the commodity.
* @param endLatitude The drop off latitude of the commodity.
* @param endLongitude The drop off longitude of the commodity.
* @param status The current status of the commodity. If null it will default to CommodityStatus.OFFLINE.
* @param metadata A JsonObject representing the metadata field of the commodity. If null it will default to an empty JsonObject.
* @return An unconnected commodity.
* @throws IllegalStateException when this cluster is not connected.
*/
public Commodity createCommodity(String name, double startLatitude, double startLongitude, double endLatitude, double endLongitude, CommodityStatus status, JsonObject metadata) {
if (!this.isConnected()) {
throw new IllegalStateException("Not connected to cluster, cannot create commodity");
}
String path = this.getChildPath(name);
return new Commodity(path, startLatitude, startLongitude, endLatitude, endLongitude, status, metadata, this.getServices());
}
/**
* Creates a commodity with a values specified in the JSON object.
*
* @param commodityJson a JSON object that represents a commodity
* @param services a pathfinder services object
* @return A commodity with the values in the JSON object
*/
private Commodity createCommodity(JsonObject commodityJson, PathfinderServices services) {
return Commodity.getInstance(commodityJson, services);
}
/**
* Returns a commodity directly under this cluster by its name.
*
* @param name The name of the commodity.
* @return A commodity associated with that name if one exists, null if it doesn't exist.
*/
public Commodity getCommodity(String name) {
return this.commodities.get(this.getChildPath(name));
}
/**
* Returns an immutable collection of this cluster's commodities.
*
* @return A collection of commodities.
*/
public Collection getCommodities() {
return Collections.unmodifiableCollection(this.commodities.values());
}
/**
* Returns an immutable map of this cluster's commodities.
*
* @return A map of commodities.
*/
public Map getCommoditiesMap() {
return Collections.unmodifiableMap(this.commodities);
}
/**
* Sets this cluster's commodities.
*
* @param commodities an iterable collection of commodities
*/
private void setCommodities(Iterable commodities) {
for (Commodity commodity : commodities) {
this.commodities.put(commodity.getPath(), commodity);
}
}
/**
* Sets this cluster's commodities.
*
* @param commodities a map of commodities
*/
private void setCommodities(Map commodities) {
this.commodities = commodities;
}
/**
* Creates an unconnected subcluster under this cluster.
*
* @param name Name of the subcluster, it must form a unique identifier when concatenated with this cluster's path.
* @return An unconnected subcluster.
* @throws IllegalStateException when this cluster is not connected.
*/
public Cluster createSubcluster(String name) {
if (!this.isConnected()) {
throw new IllegalStateException("The cluster is not connected on the Pathfinder server");
}
return Cluster.getInstance(this.getChildPath(name), this.getServices());
}
/**
* Returns a direct descendant subcluster by its name.
*
* @param name The name of the subcluster
* @return A subcluster associated with the provided name if exists, null if it doesn't exist.
*/
public Cluster getSubcluster(String name) {
return this.subclusters.get(this.getChildPath(name));
}
/**
* Returns an immutable collection of this cluster's direct subclusters.
*
* @return An immutable collection of clusters.
*/
public Collection getSubclusters() {
return Collections.unmodifiableCollection(this.subclusters.values());
}
/**
* Returns an immutable map of this cluster's direct subclusters.
*
* @return An immutable map of clusters.
*/
public Map getSubclustersMap() {
return Collections.unmodifiableMap(this.subclusters);
}
/**
* Sets this cluster's sub-clusters.
*
* @param subclusters an iterable collection of clusters
*/
private void setSubclusters(Iterable subclusters) {
for (Cluster cluster : subclusters) {
this.subclusters.put(cluster.getPath(), cluster);
}
}
/**
* Sets this cluster's sub-clusters.
*
* @param subclusters a map of clusters
*/
private void setSubclusters(Map subclusters) {
this.subclusters = subclusters;
}
/**
* Creates an unconnected transport under this cluster.
*
* @param name Name of the transport, it must form a unique identifier when concatenated with this cluster's path.
* @param latitude The current latitude of the transport.
* @param longitude The current longitude of the transport.
* @param status The current status of the transport. If null it defaults to TransportStatus.OFFLINE
* @param metadata The transports's metadata field. If null it defaults to an empty JSON object.
* @return An unconnected transport
* @throws IllegalStateException when this cluster has not been connected.
*/
public Transport createTransport(String name, double latitude, double longitude, TransportStatus status, JsonObject metadata) {
if (!this.isConnected()) {
throw new IllegalStateException("Not connected to cluster, cannot create transport");
}
String path = this.getChildPath(name);
return new Transport(path, latitude, longitude, status, metadata, this.getServices());
}
/**
* Creates a connected transport under this cluster with the values in the JSON object provided.
*
* @param transportJson a JSON object that represents a transport.
* @return A transport with the values in the JSON object.
*/
private Transport createTransport(JsonObject transportJson) {
return Transport.getInstance(transportJson, this.getServices());
}
/**
* Returns a transport directly under this cluster by its name.
*
* @param name The name of the transport.
* @return A transport associated with the provided name if exists, null if it doesn't exist.
*/
public Transport getTransport(String name) {
return this.transports.get(this.getChildPath(name));
}
/**
* Returns an immutable collection of this cluster's transports.
*
* @return A collection of transports.
*/
public Collection getTransports() {
return Collections.unmodifiableCollection(this.transports.values());
}
/**
* Returns an immutable map of this cluster's transports.
*
* @return A map of transports.
*/
public Map getTransportsMap() {
return Collections.unmodifiableMap(this.transports);
}
/**
* Sets this cluster's transports.
*
* @param transports an iterable collection of transports
*/
private void setTransports(Iterable transports) {
for (Transport transport : transports) {
this.transports.put(transport.getPath(), transport);
}
}
/**
* Sets this cluster's transports.
*
* @param transports a map of transports
*/
private void setTransports(Map transports) {
this.transports = transports;
}
/**
* Sets this cluster's routes.
*
* @param routes a list of routes for this cluster
*/
private void setRoutes(List routes) {
this.routes = routes;
}
/**
* Returns an immutable collection of this cluster's routes.
*
* @return A collection of routes.
*/
public Collection getRoutes() {
return Collections.unmodifiableCollection(this.routes);
}
/**
* {@inheritDoc}
*/
@Override
protected String getModel() {
return Cluster.MODEL;
}
/**
* {@inheritDoc}
*/
@Override
protected JsonObject createValueJson() {
JsonObject json = new JsonObject();
json.addProperty("path", this.getPath());
json.addProperty("model", this.getModel());
return json;
}
/**
* {@inheritDoc}
*/
protected boolean updateFields(JsonObject json) {
Map prevCommodities;
Map prevSubclusters;
Map prevTransports;
List updatedCommodities = new ArrayList();
List updatedClusters = new ArrayList();
List updatedTransports = new ArrayList();
boolean updated = false;
prevCommodities = this.getCommoditiesMap();
Map commodityMap = new HashMap();
if (json.has("commodities")) {
JsonArray commodities = json.getAsJsonArray("commodities");
for (JsonElement commodityJson : commodities) {
String path = ((JsonObject) commodityJson).get("path").getAsString();
Commodity commodity = Commodity.getInstance(path, this.getServices());
if (commodity.notifyUpdate(null, (JsonObject) commodityJson)) {
updatedCommodities.add(commodity);
}
commodityMap.put(commodity.getPath(), commodity);
}
}
prevSubclusters = this.getSubclustersMap();
Map clusterMap = new HashMap();
if (json.has("subClusters")) {
JsonArray clusters = json.getAsJsonArray("subClusters");
for (JsonElement clusterJson : clusters) {
String path = ((JsonObject) clusterJson).get("path").getAsString();
Cluster cluster = Cluster.getInstance(path, this.getServices());
if (cluster.notifyUpdate(null, (JsonObject) clusterJson)) {
updatedClusters.add(cluster);
}
clusterMap.put(cluster.getPath(), cluster);
}
}
prevTransports = this.getTransportsMap();
Map transportMap = new HashMap();
if (json.has("transports")) {
JsonArray transports = json.getAsJsonArray("transports");
for (JsonElement transportJson : transports) {
String path = ((JsonObject) transportJson).get("path").getAsString();
Transport transport = Transport.getInstance(path, this.getServices());
if (transport.notifyUpdate(null, (JsonObject) transportJson)) {
updatedTransports.add(transport);
}
transportMap.put(transport.getPath(), transport);
}
}
List listeners = this.getListeners();
for (String path : clusterMap.keySet()) {
if (!prevSubclusters.containsKey(path)) {
for (ClusterListener listener : listeners) {
listener.subclusterAdded(clusterMap.get(path));
}
}
updated = true;
}
for (String path : commodityMap.keySet()) {
if (!prevCommodities.containsKey(path)) {
for (ClusterListener listener : listeners) {
listener.commodityAdded(commodityMap.get(path));
}
}
updated = true;
}
for (String path : transportMap.keySet()) {
if (!prevTransports.containsKey(path)) {
for (ClusterListener listener : listeners) {
listener.transportAdded(transportMap.get(path));
}
}
updated = true;
}
for (String path : prevCommodities.keySet()) {
if (!commodityMap.containsKey(path)) {
for (ClusterListener listener : listeners) {
listener.commodityRemoved(prevCommodities.get(path));
}
}
updated = true;
}
for (String path : prevSubclusters.keySet()) {
if (!clusterMap.containsKey(path)) {
for (ClusterListener listener : listeners) {
listener.subclusterRemoved(prevSubclusters.get(path));
}
}
updated = true;
}
for (String path : prevTransports.keySet()) {
if (!transportMap.containsKey(path)) {
for (ClusterListener listener : listeners) {
listener.transportRemoved(prevTransports.get(path));
}
}
updated = true;
}
for (Cluster cluster : updatedClusters) {
for (ClusterListener listener : listeners) {
listener.subclusterUpdated(cluster);
}
updated = true;
}
for (Commodity commodity : updatedCommodities) {
for (ClusterListener listener : listeners) {
listener.commodityUpdated(commodity);
}
updated = true;
}
for (Transport transport : updatedTransports) {
for (ClusterListener listener : listeners) {
listener.transportUpdated(transport);
}
updated = true;
}
this.setCommodities(commodityMap);
this.setSubclusters(clusterMap);
this.setTransports(transportMap);
if (!updatedCommodities.isEmpty()) {
for (ClusterListener listener : listeners) {
listener.commoditiesUpdated(this.getCommodities());
}
}
if (!updatedClusters.isEmpty()) {
for (ClusterListener listener : listeners) {
listener.subclustersUpdated(this.getSubclusters());
}
}
if (!updatedTransports.isEmpty()) {
for (ClusterListener listener : listeners) {
listener.transportsUpdated(this.getTransports());
}
}
String parentPath = this.getParentPath();
Cluster parentCluster = Cluster.getInstance(parentPath, this.getServices());
if (updated && parentCluster != null) {
Collection clusters = parentCluster.getSubclusters();
List clusterListeners = parentCluster.getListeners();
for (ClusterListener listener : clusterListeners) {
listener.subclusterUpdated(this);
listener.subclustersUpdated(clusters);
}
}
return updated;
}
/**
* {@inheritDoc}
*/
protected void route(JsonObject json, PathfinderServices services) {
JsonArray routesJson = json.getAsJsonArray("route");
List routes = new ArrayList();
for (JsonElement route : routesJson) {
routes.add(new Route((JsonObject) route, services));
}
this.setRoutes(routes);
for (ClusterListener listener : this.getListeners()) {
listener.routed(new ArrayList(this.getRoutes()));
}
}
}