/*
* Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.amazonaws.geo;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import com.amazonaws.AmazonClientException;
import com.amazonaws.geo.dynamodb.internal.DynamoDBManager;
import com.amazonaws.geo.dynamodb.internal.DynamoDBUtil;
import com.amazonaws.geo.model.BatchWritePointResult;
import com.amazonaws.geo.model.DeletePointRequest;
import com.amazonaws.geo.model.DeletePointResult;
import com.amazonaws.geo.model.GeoPoint;
import com.amazonaws.geo.model.GeoQueryRequest;
import com.amazonaws.geo.model.GeoQueryResult;
import com.amazonaws.geo.model.GeohashRange;
import com.amazonaws.geo.model.GetPointRequest;
import com.amazonaws.geo.model.GetPointResult;
import com.amazonaws.geo.model.PutPointRequest;
import com.amazonaws.geo.model.PutPointResult;
import com.amazonaws.geo.model.QueryRadiusRequest;
import com.amazonaws.geo.model.QueryRadiusResult;
import com.amazonaws.geo.model.QueryRectangleRequest;
import com.amazonaws.geo.model.QueryRectangleResult;
import com.amazonaws.geo.model.UpdatePointRequest;
import com.amazonaws.geo.model.UpdatePointResult;
import com.amazonaws.geo.s2.internal.S2Manager;
import com.amazonaws.geo.s2.internal.S2Util;
import com.amazonaws.geo.util.GeoJsonMapper;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.QueryResult;
import com.google.common.geometry.S2CellId;
import com.google.common.geometry.S2CellUnion;
import com.google.common.geometry.S2LatLng;
import com.google.common.geometry.S2LatLngRect;
/**
* <p>
* Manager to hangle geo spatial data in Amazon DynamoDB tables. All service calls made using this client are blocking,
* and will not return until the service call completes.
* </p>
* <p>
* This class is designed to be thread safe; however, once constructed GeoDataManagerConfiguration should not be
* modified. Modifying GeoDataManagerConfiguration may cause unspecified behaviors.
* </p>
* */
public class GeoDataManager {
private GeoDataManagerConfiguration config;
private DynamoDBManager dynamoDBManager;
/**
* <p>
* Construct and configure GeoDataManager using GeoDataManagerConfiguration.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* AmazonDynamoDBClient ddb = new AmazonDynamoDBClient(new ClasspathPropertiesFileCredentialsProvider());
* Region usWest2 = Region.getRegion(Regions.US_WEST_2);
* ddb.setRegion(usWest2);
*
* ClientConfiguration clientConfiguration = new ClientConfiguration().withMaxErrorRetry(5);
* ddb.setConfiguration(clientConfiguration);
*
* GeoDataManagerConfiguration config = new GeoDataManagerConfiguration(ddb, "geo-table");
* GeoDataManager geoDataManager = new GeoDataManager(config);
* </pre>
*
* @param config
* Container for the configuration parameters for GeoDataManager.
*/
public GeoDataManager(GeoDataManagerConfiguration config) {
this.config = config;
dynamoDBManager = new DynamoDBManager(this.config);
}
/**
* <p>
* Return GeoDataManagerConfiguration. The returned GeoDataManagerConfiguration should not be modified.
* </p>
*
* @return
* GeoDataManagerConfiguration that is used to configure this GeoDataManager.
*/
public GeoDataManagerConfiguration getGeoDataManagerConfiguration() {
return config;
}
/**
* <p>
* Put a point into the Amazon DynamoDB table. Once put, you cannot update attributes specified in
* GeoDataManagerConfiguration: hash key, range key, geohash and geoJson. If you want to update these columns, you
* need to insert a new record and delete the old record.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint geoPoint = new GeoPoint(47.5, -122.3);
* AttributeValue rangeKeyValue = new AttributeValue().withS("a6feb446-c7f2-4b48-9b3a-0f87744a5047");
* AttributeValue titleValue = new AttributeValue().withS("Original title");
*
* PutPointRequest putPointRequest = new PutPointRequest(geoPoint, rangeKeyValue);
* putPointRequest.getPutItemRequest().getItem().put("title", titleValue);
*
* PutPointResult putPointResult = geoDataManager.putPoint(putPointRequest);
* </pre>
*
* @param putPointRequest
* Container for the necessary parameters to execute put point request.
*
* @return Result of put point request.
*/
public PutPointResult putPoint(PutPointRequest putPointRequest) {
return dynamoDBManager.putPoint(putPointRequest);
}
/**
* <p>
* Put a list of points into the Amazon DynamoDB table. Once put, you cannot update attributes specified in
* GeoDataManagerConfiguration: hash key, range key, geohash and geoJson. If you want to update these columns, you
* need to insert a new record and delete the old record.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint geoPoint = new GeoPoint(47.5, -122.3);
* AttributeValue rangeKeyValue = new AttributeValue().withS("a6feb446-c7f2-4b48-9b3a-0f87744a5047");
* AttributeValue titleValue = new AttributeValue().withS("Original title");
*
* PutPointRequest putPointRequest = new PutPointRequest(geoPoint, rangeKeyValue);
* putPointRequest.getPutItemRequest().getItem().put("title", titleValue);
* List<PutPointRequest> putPointRequests = new ArrayList<PutPointRequest>();
* putPointRequests.add(putPointRequest);
* BatchWritePointResult batchWritePointResult = geoDataManager.batchWritePoints(putPointRequests);
* </pre>
*
* @param putPointRequests
* Container for the necessary parameters to execute put point request.
*
* @return Result of batch put point request.
*/
public BatchWritePointResult batchWritePoints(List<PutPointRequest> putPointRequests) {
return dynamoDBManager.batchWritePoints(putPointRequests);
}
/**
* <p>
* Get a point from the Amazon DynamoDB table.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint geoPoint = new GeoPoint(47.5, -122.3);
* AttributeValue rangeKeyValue = new AttributeValue().withS("a6feb446-c7f2-4b48-9b3a-0f87744a5047");
*
* GetPointRequest getPointRequest = new GetPointRequest(geoPoint, rangeKeyValue);
* GetPointResult getPointResult = geoIndexManager.getPoint(getPointRequest);
*
* System.out.println("item: " + getPointResult.getGetItemResult().getItem());
* </pre>
*
* @param getPointRequest
* Container for the necessary parameters to execute get point request.
*
* @return Result of get point request.
* */
public GetPointResult getPoint(GetPointRequest getPointRequest) {
return dynamoDBManager.getPoint(getPointRequest);
}
/**
* <p>
* Query a rectangular area constructed by two points and return all points within the area. Two points need to
* construct a rectangle from minimum and maximum latitudes and longitudes. If minPoint.getLongitude() >
* maxPoint.getLongitude(), the rectangle spans the 180 degree longitude line.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint minPoint = new GeoPoint(45.5, -124.3);
* GeoPoint maxPoint = new GeoPoint(49.5, -120.3);
*
* QueryRectangleRequest queryRectangleRequest = new QueryRectangleRequest(minPoint, maxPoint);
* QueryRectangleResult queryRectangleResult = geoIndexManager.queryRectangle(queryRectangleRequest);
*
* for (Map<String, AttributeValue> item : queryRectangleResult.getItem()) {
* System.out.println("item: " + item);
* }
* </pre>
*
* @param queryRectangleRequest
* Container for the necessary parameters to execute rectangle query request.
*
* @return Result of rectangle query request.
*/
public QueryRectangleResult queryRectangle(QueryRectangleRequest queryRectangleRequest) {
S2LatLngRect latLngRect = S2Util.getBoundingLatLngRect(queryRectangleRequest);
S2CellUnion cellUnion = S2Manager.findCellIds(latLngRect);
List<GeohashRange> ranges = mergeCells(cellUnion);
cellUnion = null;
return new QueryRectangleResult(dispatchQueries(ranges, queryRectangleRequest));
}
/**
* <p>
* Query a circular area constructed by a center point and its radius.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint centerPoint = new GeoPoint(47.5, -122.3);
*
* QueryRadiusRequest queryRadiusRequest = new QueryRadiusRequest(centerPoint, 100);
* QueryRadiusResult queryRadiusResult = geoIndexManager.queryRadius(queryRadiusRequest);
*
* for (Map<String, AttributeValue> item : queryRadiusResult.getItem()) {
* System.out.println("item: " + item);
* }
* </pre>
*
* @param queryRadiusRequest
* Container for the necessary parameters to execute radius query request.
*
* @return Result of radius query request.
* */
public QueryRadiusResult queryRadius(QueryRadiusRequest queryRadiusRequest) {
S2LatLngRect latLngRect = S2Util.getBoundingLatLngRect(queryRadiusRequest);
S2CellUnion cellUnion = S2Manager.findCellIds(latLngRect);
List<GeohashRange> ranges = mergeCells(cellUnion);
cellUnion = null;
return new QueryRadiusResult(dispatchQueries(ranges, queryRadiusRequest));
}
/**
* <p>
* Update a point data in Amazon DynamoDB table. You cannot update attributes specified in
* GeoDataManagerConfiguration: hash key, range key, geohash and geoJson. If you want to update these columns, you
* need to insert a new record and delete the old record.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint geoPoint = new GeoPoint(47.5, -122.3);
*
* String rangeKey = "a6feb446-c7f2-4b48-9b3a-0f87744a5047";
* AttributeValue rangeKeyValue = new AttributeValue().withS(rangeKey);
*
* UpdatePointRequest updatePointRequest = new UpdatePointRequest(geoPoint, rangeKeyValue);
*
* AttributeValue titleValue = new AttributeValue().withS("Updated title.");
* AttributeValueUpdate titleValueUpdate = new AttributeValueUpdate().withAction(AttributeAction.PUT)
* .withValue(titleValue);
* updatePointRequest.getUpdateItemRequest().getAttributeUpdates().put("title", titleValueUpdate);
*
* UpdatePointResult updatePointResult = geoIndexManager.updatePoint(updatePointRequest);
* </pre>
*
* @param updatePointRequest
* Container for the necessary parameters to execute update point request.
*
* @return Result of update point request.
*/
public UpdatePointResult updatePoint(UpdatePointRequest updatePointRequest) {
return dynamoDBManager.updatePoint(updatePointRequest);
}
/**
* <p>
* Delete a point from the Amazon DynamoDB table.
* </p>
* <b>Sample usage:</b>
*
* <pre>
* GeoPoint geoPoint = new GeoPoint(47.5, -122.3);
*
* String rangeKey = "a6feb446-c7f2-4b48-9b3a-0f87744a5047";
* AttributeValue rangeKeyValue = new AttributeValue().withS(rangeKey);
*
* DeletePointRequest deletePointRequest = new DeletePointRequest(geoPoint, rangeKeyValue);
* DeletePointResult deletePointResult = geoIndexManager.deletePoint(deletePointRequest);
* </pre>
*
* @param deletePointRequest
* Container for the necessary parameters to execute delete point request.
*
* @return Result of delete point request.
*/
public DeletePointResult deletePoint(DeletePointRequest deletePointRequest) {
return dynamoDBManager.deletePoint(deletePointRequest);
}
/**
* Merge continuous cells in cellUnion and return a list of merged GeohashRanges.
*
* @param cellUnion
* Container for multiple cells.
*
* @return A list of merged GeohashRanges.
*/
private List<GeohashRange> mergeCells(S2CellUnion cellUnion) {
List<GeohashRange> ranges = new ArrayList<GeohashRange>();
for (S2CellId c : cellUnion.cellIds()) {
GeohashRange range = new GeohashRange(c.rangeMin().id(), c.rangeMax().id());
boolean wasMerged = false;
for (GeohashRange r : ranges) {
if (r.tryMerge(range)) {
wasMerged = true;
break;
}
}
if (!wasMerged) {
ranges.add(range);
}
}
return ranges;
}
/**
* Query Amazon DynamoDB in parallel and filter the result.
*
* @param ranges
* A list of geohash ranges that will be used to query Amazon DynamoDB.
*
* @param latLngRect
* The rectangle area that will be used as a reference point for precise filtering.
*
* @return Aggregated and filtered items returned from Amazon DynamoDB.
*/
private GeoQueryResult dispatchQueries(List<GeohashRange> ranges, GeoQueryRequest geoQueryRequest) {
GeoQueryResult geoQueryResult = new GeoQueryResult();
ExecutorService executorService = config.getExecutorService();
List<Future<?>> futureList = new ArrayList<Future<?>>();
for (GeohashRange outerRange : ranges) {
for (GeohashRange range : outerRange.trySplit(config.getHashKeyLength())) {
GeoQueryThread geoQueryThread = new GeoQueryThread(geoQueryRequest, geoQueryResult, range);
futureList.add(executorService.submit(geoQueryThread));
}
}
ranges = null;
for (int i = 0; i < futureList.size(); i++) {
try {
futureList.get(i).get();
} catch (Exception e) {
for (int j = i + 1; j < futureList.size(); j++) {
futureList.get(j).cancel(true);
}
throw new AmazonClientException("Querying Amazon DynamoDB failed.", e);
}
}
futureList = null;
return geoQueryResult;
}
/**
* Filter out any points outside of the queried area from the input list.
*
* @param list
* List of items return by Amazon DynamoDB. It may contains points outside of the actual area queried.
*
* @param latLngRect
* Queried area. Any points outside of this area need to be discarded.
*
* @return List of items within the queried area.
*/
private List<Map<String, AttributeValue>> filter(List<Map<String, AttributeValue>> list,
GeoQueryRequest geoQueryRequest) {
List<Map<String, AttributeValue>> result = new ArrayList<Map<String, AttributeValue>>();
S2LatLngRect latLngRect = null;
S2LatLng centerLatLng = null;
double radiusInMeter = 0;
if (geoQueryRequest instanceof QueryRectangleRequest) {
latLngRect = S2Util.getBoundingLatLngRect(geoQueryRequest);
} else if (geoQueryRequest instanceof QueryRadiusRequest) {
GeoPoint centerPoint = ((QueryRadiusRequest) geoQueryRequest).getCenterPoint();
centerLatLng = S2LatLng.fromDegrees(centerPoint.getLatitude(), centerPoint.getLongitude());
radiusInMeter = ((QueryRadiusRequest) geoQueryRequest).getRadiusInMeter();
}
for (Map<String, AttributeValue> item : list) {
String geoJson = item.get(config.getGeoJsonAttributeName()).getS();
GeoPoint geoPoint = GeoJsonMapper.geoPointFromString(geoJson);
S2LatLng latLng = S2LatLng.fromDegrees(geoPoint.getLatitude(), geoPoint.getLongitude());
if (latLngRect != null && latLngRect.contains(latLng)) {
result.add(item);
} else if (centerLatLng != null && radiusInMeter > 0
&& centerLatLng.getEarthDistance(latLng) <= radiusInMeter) {
result.add(item);
}
}
return result;
}
/**
* Worker thread to query Amazon DynamoDB.
* */
private class GeoQueryThread extends Thread {
private GeoQueryRequest geoQueryRequest;
private GeoQueryResult geoQueryResult;
private GeohashRange range;
public GeoQueryThread(GeoQueryRequest geoQueryRequest, GeoQueryResult geoQueryResult, GeohashRange range) {
this.geoQueryRequest = geoQueryRequest;
this.geoQueryResult = geoQueryResult;
this.range = range;
}
public void run() {
QueryRequest queryRequest = DynamoDBUtil.copyQueryRequest(geoQueryRequest.getQueryRequest());
long hashKey = S2Manager.generateHashKey(range.getRangeMin(), config.getHashKeyLength());
List<QueryResult> queryResults = dynamoDBManager.queryGeohash(queryRequest, hashKey, range);
for (QueryResult queryResult : queryResults) {
if (isInterrupted()) {
return;
}
// getQueryResults() returns a synchronized list.
geoQueryResult.getQueryResults().add(queryResult);
List<Map<String, AttributeValue>> filteredQueryResult = filter(queryResult.getItems(), geoQueryRequest);
// getItem() returns a synchronized list.
geoQueryResult.getItem().addAll(filteredQueryResult);
}
}
}
}