org.opentripplanner.analyst.PointSet Maven / Gradle / Ivy
package org.opentripplanner.analyst;
import com.csvreader.CsvReader;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Polygon;
import gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import org.geojson.LngLatAlt;
import org.geotools.data.FileDataStore;
import org.geotools.data.FileDataStoreFinder;
import org.geotools.data.Query;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.referencing.CRS;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opentripplanner.analyst.pointset.PropertyMetadata;
import org.opentripplanner.common.geometry.SphericalDistanceLibrary;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.services.GraphService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
/**
* PointSets serve as named groups of destinations when calculating analyst one-to-many indicators.
* They could also serve as origins in many-to-many indicators.
*
* PointSets are one of the three main web analyst resources: Pointsets, Indicators, and TimeSurfaces
*/
public class PointSet implements Serializable {
private static final long serialVersionUID = -8962916330731463238L;
private static final Logger LOG = LoggerFactory.getLogger(PointSet.class);
/** A server-unique identifier for this PointSet */
public String id;
/** A short description of this PointSet for use in a legend or menu */
public String label;
/** A detailed textual description of this PointSet */
public String description;
public Map propMetadata = new HashMap();
// TODO why is this concurrent? what two threads are modifying the hashmap simultaneously?
public Map properties = new ConcurrentHashMap();
public int capacity = 0; // The total number of features this PointSet can hold.
/*
* Connects this population to vertices in a given Graph (map of graph ids
* to sample sets). Keeping as a graphId->sampleSet map to prevent
* duplication of pointset when used across multiple graphs.
*/
private Map samples = new ConcurrentHashMap();
/**
* Map from string IDs to their array indices. This is a view into PointSet.ids, namely its reverse mapping.
*/
private transient TObjectIntMap idIndexMap;
/*
* Used to generate SampleSets on an as needed basis.
*/
protected GraphService graphService;
/*
* In a detailed Indicator, the time to reach each target, for each origin.
* Null in non-indicator pointsets.
* TODO remove this if unused, result sets are no longer PointSets.
*/
@Deprecated
public int[][] times;
// The characteristics of the features in this PointSet. This is a column store.
// Each structured attribute must also contain an array of magnitudes with the same length as these arrays.
/** A unique identifier for each feature. */
protected String[] ids;
/** The latitude of each feature (or its centroid if it's not a point). */
protected double[] lats;
/** The longitude of each feature (or its centroid if it's not a point). */
protected double[] lons;
/** The polygon for each feature (which is reduced to a centroid point for routing purposes). */
protected Polygon[] polygons; // TODO what do we do when there are no polygons?
/**
* Rather than trying to load anything any everything, we stick to a strict
* format and rely on other tools to get the data into the correct format.
* This includes column headers in the category:subcategory:attribute format
* and coordinates in WGS84. Comment lines are allowed in these input files, and begin with a #.
*/
public static PointSet fromCsv(File filename) throws IOException {
/* First, scan through the file to count lines and check for errors. */
CsvReader reader = new CsvReader(filename.getAbsolutePath(), ',', Charset.forName("UTF8"));
reader.readHeaders();
int nCols = reader.getHeaderCount();
while (reader.readRecord()) {
if (reader.getColumnCount() != nCols) {
LOG.error("CSV record {} has the wrong number of fields.", reader.getCurrentRecord());
return null;
}
}
// getCurrentRecord is zero-based and does not include headers or blank lines
int nRecs = (int) reader.getCurrentRecord() + 1;
reader.close();
/* If we reached here, the file is entirely readable. Start over. */
reader = new CsvReader(filename.getAbsolutePath(), ',', Charset.forName("UTF8"));
PointSet ret = new PointSet(nRecs);
reader.readHeaders();
if (reader.getHeaderCount() != nCols) {
LOG.error("Number of headers changed.");
return null;
}
int latCol = -1;
int lonCol = -1;
// An array of property magnitudes corresponding to each column in the input.
// Some of these will remain null (specifically, the lat and lon columns which do not contain magnitudes)
int[][] properties = new int[nCols][ret.capacity];
for (int c = 0; c < nCols; c++) {
String header = reader.getHeader(c);
if (header.equalsIgnoreCase("lat") || header.equalsIgnoreCase("latitude")) {
latCol = c;
} else if (header.equalsIgnoreCase("lon") || header.equalsIgnoreCase("longitude")) {
lonCol = c;
} else {
ret.getOrCreatePropertyForId(header);
properties[c] = ret.properties.get(header);
}
}
if (latCol < 0 || lonCol < 0) {
LOG.error("CSV file did not contain a latitude or longitude column.");
throw new IOException();
}
ret.lats = new double[nRecs];
ret.lons = new double[nRecs];
while (reader.readRecord()) {
int rec = (int) reader.getCurrentRecord();
for (int c = 0; c < nCols; c++) {
if(c==latCol || c==lonCol){
continue;
}
int[] prop = properties[c];
int mag = Integer.parseInt(reader.get(c));
prop[rec] = mag;
}
ret.lats[rec] = Double.parseDouble(reader.get(latCol));
ret.lons[rec] = Double.parseDouble(reader.get(lonCol));
}
ret.capacity = nRecs;
return ret;
}
public static PointSet fromShapefile(File file) throws NoSuchAuthorityCodeException, IOException, FactoryException, EmptyPolygonException, UnsupportedGeometryException {
return fromShapefile(file, null, null);
}
public static PointSet fromShapefile(File file, String originIDField, List propertyFields) throws IOException, NoSuchAuthorityCodeException, FactoryException, EmptyPolygonException, UnsupportedGeometryException {
if ( ! file.exists())
throw new RuntimeException("Shapefile does not exist.");
FileDataStore store = FileDataStoreFinder.getDataStore(file);
SimpleFeatureSource featureSource = store.getFeatureSource();
CoordinateReferenceSystem sourceCRS = featureSource.getInfo().getCRS();
CoordinateReferenceSystem WGS84 = CRS.decode("EPSG:4326", true);
Query query = new Query();
query.setCoordinateSystem(sourceCRS);
query.setCoordinateSystemReproject(WGS84);
SimpleFeatureCollection featureCollection = featureSource.getFeatures(query);
// Set up fields based on first feature in collection
// This assumes that all features have the same set of properties, which I think is always the case for shapefiles
SimpleFeatureIterator it = featureCollection.features();
SimpleFeature protoFt = it.next();
if (propertyFields == null) {
propertyFields = new ArrayList();
// No property fields specified, so use all property fields
for (Property p : protoFt.getProperties()) {
propertyFields.add(p.getName().toString());
}
// If ID field is specified, don't use it as a property
if (originIDField != null && propertyFields.contains(originIDField)) {
propertyFields.remove(originIDField);
}
}
// Reset iterator
it = featureCollection.features();
PointSet ret = new PointSet(featureCollection.size());
int i=0;
while (it.hasNext()) {
SimpleFeature feature = it.next();
Geometry geom = (Geometry) feature.getDefaultGeometry();
PointFeature ft = new PointFeature();
ft.setGeom(geom);
// Set feature's ID to the specified ID field, or to index if none is specified
if (originIDField == null) {
ft.setId(Integer.toString(i));
} else {
ft.setId(feature.getProperty(originIDField).getValue().toString());
}
for(Property prop : feature.getProperties() ){
String propName = prop.getName().toString();
if (propertyFields.contains(propName)) {
Object binding = prop.getType().getBinding();
//attempt to coerce the prop's value into an integer
int val;
if(binding.equals(Integer.class)){
val = (Integer)prop.getValue();
} else if(binding.equals(Long.class)){
val = ((Long)prop.getValue()).intValue();
} else if(binding.equals(String.class)){
try{
val = Integer.parseInt((String)prop.getValue());
} catch (NumberFormatException ex ){
continue;
}
} else {
LOG.debug("Property {} of feature {} could not be interpreted as int, skipping", prop.getName().toString(), ft.getId());
continue;
}
ft.addAttribute(propName, val);
} else {
LOG.debug("Property {} not requested; igoring", propName);
}
}
ret.addFeature(ft, i);
i++;
}
ArrayList IDlist = new ArrayList();
for (String id : ret.ids) {
IDlist.add(id);
}
LOG.debug("Created PointSet from shapefile with IDs {}", IDlist);
return ret;
}
public static PointSet fromGeoJson(File filename) {
try {
FileInputStream fis = new FileInputStream(filename);
int n = validateGeoJson(fis);
if (n < 0)
return null;
fis.getChannel().position(0); // rewind file
return fromValidatedGeoJson(fis, n);
} catch (FileNotFoundException ex) {
LOG.error("GeoJSON file not found: {}", filename);
return null;
} catch (IOException ex) {
LOG.error("I/O exception while reading GeoJSON file: {}", filename);
return null;
}
}
/**
* Examines a JSON stream to see if it matches the expected OTPA format.
* TODO improve the level of detail of validation. Many files pass the validation and then crash the load function.
*
* @return the number of features in the collection if it's valid, or -1 if
* it doesn't fit the OTPA format.
*/
public static int validateGeoJson(InputStream is) {
int n = 0;
JsonFactory f = new JsonFactory();
try {
JsonParser jp = f.createParser(is);
JsonToken current = jp.nextToken();
if (current != JsonToken.START_OBJECT) {
LOG.error("Root of OTPA GeoJSON should be a JSON object.");
return -1;
}
// Iterate over the key:value pairs in the top-level JSON object
while (jp.nextToken() != JsonToken.END_OBJECT) {
String key = jp.getCurrentName();
current = jp.nextToken();
if (key.equals("features")) {
if (current != JsonToken.START_ARRAY) {
LOG.error("Error: GeoJSON features are not in an array.");
return -1;
}
// Iterate over the features in the array
while (jp.nextToken() != JsonToken.END_ARRAY) {
n += 1;
jp.skipChildren();
}
} else {
jp.skipChildren(); // ignore all other keys except features
}
}
if (n == 0)
return -1; // JSON has no features
return n;
} catch (Exception ex) {
LOG.error("Exception while validating GeoJSON: {}", ex);
return -1;
}
}
/**
* Reads with a combination of streaming and tree-model to allow very large
* GeoJSON files. The JSON should be already validated, and you must pass in
* the maximum number of features from that validation step.
*/
private static PointSet fromValidatedGeoJson(InputStream is, int n) {
JsonFactory f = new MappingJsonFactory();
PointSet ret = new PointSet(n);
int index = 0;
try {
JsonParser jp = f.createParser(is);
JsonToken current = jp.nextToken();
// Iterate over the key:value pairs in the top-level JSON object
while (jp.nextToken() != JsonToken.END_OBJECT) {
String key = jp.getCurrentName();
current = jp.nextToken();
if (key.equals("properties")) {
JsonNode properties = jp.readValueAsTree();
if(properties.get("id") != null)
ret.id = properties.get("id").asText();
if(properties.get("label") != null)
ret.label = properties.get("label").asText();
if(properties.get("description") != null)
ret.label = properties.get("description").asText();
if(properties.get("schema") != null) {
Iterator> catIter = properties.get("schema").fields();
while (catIter.hasNext()) {
Entry catEntry = catIter.next();
String catName = catEntry.getKey();
JsonNode catNode = catEntry.getValue();
PropertyMetadata cat = new PropertyMetadata(catName);
if(catNode.get("label") != null)
cat.label = catNode.get("label").asText();
if(catNode.get("style") != null) {
Iterator> styleIter = catNode.get("style").fields();
while (styleIter.hasNext()) {
Entry styleEntry = styleIter.next();
String styleName = styleEntry.getKey();
JsonNode styleValue = styleEntry.getValue();
cat.addStyle(styleName, styleValue.asText());
}
}
ret.propMetadata.put(catName, cat);
}
}
}
if (key.equals("features")) {
while (jp.nextToken() != JsonToken.END_ARRAY) {
// Read the feature into a tree model, which moves
// parser to its end.
JsonNode feature = jp.readValueAsTree();
ret.addFeature(feature, index++);
}
} else {
jp.skipChildren(); // ignore all other keys except features
}
}
} catch (Exception ex) {
LOG.error("GeoJSON parsing failure", ex);
return null;
}
return ret;
}
/**
* Add one GeoJSON feature to this PointSet from a Jackson node tree.
* com.bedatadriven.geojson only exposed its streaming Geometry parser as a
* public method. I made its tree parser public as well. Geotools also has a
* GeoJSON parser called GeometryJson (which OTP wraps in
* GeoJsonDeserializer) but it consumes straight text, not a Jackson model
* or streaming parser.
*/
private void addFeature(JsonNode feature, int index) {
PointFeature feat = null;
try {
feat = PointFeature.fromJsonNode(feature);
} catch (EmptyPolygonException e) {
LOG.warn("Empty MultiPolygon, skipping.");
return;
} catch (UnsupportedGeometryException e) {
LOG.warn(e.message);
return;
}
if (feat == null) {
return;
}
addFeature(feat, index);
}
/**
* Create a PointSet manually by defining capacity and calling
* addFeature(geom, data) repeatedly.
*
* @param capacity
* expected number of features to be added to this PointSet.
*/
public PointSet(int capacity) {
this.capacity = capacity;
ids = new String[capacity];
lats = new double[capacity];
lons = new double[capacity];
polygons = new Polygon[capacity];
}
/**
* Adds a graph service to allow for auto creation of SampleSets for a given
* graph
*/
public void setGraphService(GraphService graphService) {
this.graphService = graphService;
}
/**
* gets a sample set for a given graph id -- requires graphservice to be set
* @return sampleset for graph
*/
public SampleSet getSampleSet(String routerId) {
if(this.graphService == null)
return null;
if (this.samples.containsKey(routerId))
return this.samples.get(routerId);
Graph g = this.graphService.getRouter(routerId).graph;
return getSampleSet(g);
}
// TODO refactor the other getSampleSet methods in terms of this one.
public SampleSet getOrCreateSampleSet(Graph graph) {
SampleSet sampleSet = this.samples.get(graph.routerId);
if (sampleSet == null) {
sampleSet = new SampleSet(this, graph.getSampleFactory());
this.samples.put(graph.routerId, sampleSet);
}
return sampleSet;
}
/**
* gets a sample set for a graph object -- does not require graph service to be set
* @param g a graph objects
* @return sampleset for graph
*/
public SampleSet getSampleSet(Graph g) {
if (g == null)
return null;
SampleSet sampleSet = new SampleSet(this, g.getSampleFactory());
this.samples.put(g.routerId, sampleSet);
return sampleSet;
}
public int featureCount() {
return ids.length;
}
/**
* Add a single feature with a variable number of free-form properties.
* Attribute data contains id value pairs, ids are in form "cat_id:prop_id".
* If the properties and categories do not exist, they will be created.
* TODO: read explicit schema or infer it and validate property presence as
* they're read
*
* @param feat must be a Point, a Polygon, or a single-element MultiPolygon
*/
public void addFeature(PointFeature feat, int index) {
if (index >= capacity) {
throw new AssertionError("Number of features seems to have grown since validation.");
}
polygons[index] = feat.getPolygon();
lats[index] = feat.getLat();
lons[index] = feat.getLon();
ids[index] = feat.getId();
for (Entry ad : feat.getProperties().entrySet()) {
String propId = ad.getKey();
Integer propVal = ad.getValue();
this.getOrCreatePropertyForId(propId);
this.properties.get(propId)[index] = propVal;
}
}
public PointFeature getFeature(int index) {
PointFeature ret = new PointFeature(ids[index]);
if (polygons[index] != null) {
try {
ret.setGeom(polygons[index]);
} catch (Exception e) {
// The polygon is known to be clean; this should never happen. We
// could pass the exception up but that'd just make the calling
// function deal with an exception that will never pop. So
// we'll make the compiler happy by catching it here silently.
}
}
// ret.setGeom, if it was called, will already set the lat and lon
// properties. But since every item in this pointset is guaranteed
// to have a lat/lon coordinate, we defer to it as more authoritative.
ret.setLat(lats[index]);
ret.setLon(lons[index]);
for (Entry property : this.properties.entrySet()) {
ret.addAttribute( property.getKey(), property.getValue()[index]);
}
return ret;
}
public void setLabel(String catId, String label) {
PropertyMetadata meta = this.propMetadata.get(catId);
if(meta!=null){
meta.setLabel( label );
}
}
public void setStyle(String catId, String styleAttribute, String styleValue) {
PropertyMetadata meta = propMetadata.get(catId);
if(meta!=null){
meta.addStyle( styleAttribute, styleValue );
}
}
/**
* Gets the Category object for the given ID, creating it if it doesn't
* exist.
*/
public PropertyMetadata getOrCreatePropertyForId(String id) {
PropertyMetadata property = propMetadata.get(id);
if (property == null) {
property = new PropertyMetadata(id);
propMetadata.put(id, property);
}
if(!properties.containsKey(id))
properties.put(id, new int[capacity]);
return property;
}
public void writeJson(OutputStream out) {
writeJson(out, false);
}
/**
* Use the Jackson streaming API to output this as GeoJSON without creating
* another object. The Indicator is a column store, and is transposed WRT
* the JSON representation.
*/
public void writeJson(OutputStream out, Boolean forcePoints) {
try {
JsonFactory jsonFactory = new JsonFactory(); // ObjectMapper.getJsonFactory() is better
JsonGenerator jgen = jsonFactory.createGenerator(out);
jgen.setCodec(new ObjectMapper());
jgen.writeStartObject();
{
jgen.writeStringField("type", "FeatureCollection");
writeJsonProperties(jgen);
jgen.writeArrayFieldStart("features");
{
for (int f = 0; f < capacity; f++) {
writeFeature(f, jgen, forcePoints);
}
}
jgen.writeEndArray();
}
jgen.writeEndObject();
jgen.close();
} catch (IOException ioex) {
LOG.info("IOException, connection may have been closed while streaming JSON.");
}
}
public void writeJsonProperties(JsonGenerator jgen) throws JsonGenerationException, IOException {
jgen.writeObjectFieldStart("properties");
{
if (id != null)
jgen.writeStringField("id", id);
if (label != null)
jgen.writeStringField("label", label);
if (description != null)
jgen.writeStringField("description", description);
jgen.writeObjectFieldStart("schema");
{
for (PropertyMetadata cat : this.propMetadata.values()) {
jgen.writeObjectFieldStart(cat.id);
{
if (cat.label != null)
jgen.writeStringField("label", cat.label);
if (cat.style != null && cat.style.attributes != null) {
jgen.writeObjectFieldStart("style");
{
for (String styleKey : cat.style.attributes.keySet()) {
jgen.writeStringField(styleKey, cat.style.attributes.get(styleKey));
}
}
jgen.writeEndObject();
}
}
jgen.writeEndObject();
}
}
jgen.writeEndObject();
}
jgen.writeEndObject();
}
/**
* Pairs an array of times with the array of features in this pointset,
* writing out the resulting (ID,time) pairs to a JSON object.
*/
protected void writeTimes(JsonGenerator jgen, int[] times) throws IOException {
jgen.writeObjectFieldStart("times");
for (int i = 0; i < times.length; i++) { // capacity is now 1 if this is
// a one-to-many indicator
int t = times[i];
if (t != Integer.MAX_VALUE)
jgen.writeNumberField(ids[i], t);
}
jgen.writeEndObject();
}
/**
* This writes either a polygon or lat/lon point defining the feature. In
* the case of polygons, we convert these back to centroids on import, as
* OTPA depends on the actual point. The polygons are kept for derivative
* uses (e.g. visualization)
*
* @param i
* the feature index
* @param jgen
* the Jackson streaming JSON generator to which the geometry
* will be written
* @throws IOException
*/
private void writeFeature(int i, JsonGenerator jgen, Boolean forcePoints) throws IOException {
ObjectMapper geomSerializer = new ObjectMapper();
jgen.writeStartObject();
{
jgen.writeStringField("id", ids[i]);
jgen.writeStringField("type", "Feature");
jgen.writeFieldName("geometry");
{
if (!forcePoints && polygons != null && polygons.length >= i && polygons[i] != null) {
org.geojson.Polygon p = new org.geojson.Polygon();
List shell = new ArrayList();
for (Coordinate c : polygons[i].getExteriorRing().getCoordinates()) {
shell.add(new LngLatAlt(c.x, c.y));
}
p.add(shell);
geomSerializer.writeValue(jgen, p);
} else {
org.geojson.Point p = new org.geojson.Point(lons[i], lats[i]);
geomSerializer.writeValue(jgen, p);
}
}
jgen.writeObjectFieldStart("properties");
{
writeStructured(i, jgen);
}
jgen.writeEndObject();
}
jgen.writeEndObject();
}
/**
* This will be called once per point in an origin/destination pointset, and
* once per origin in a one- or many-to-many indicator.
*/
protected void writeStructured(int i, JsonGenerator jgen) throws IOException {
jgen.writeObjectFieldStart("structured");
for (Entry entry : properties.entrySet()) {
jgen.writeNumberField( entry.getKey(), entry.getValue()[i] );
}
jgen.writeEndObject();
}
/**
* Get a subset of this point set containing only the specified point IDs.
*/
public PointSet slice(List ids) {
PointSet ret = new PointSet(ids.size());
HashSet idsHashSet = new HashSet(ids);
ret.id = id;
ret.label = label;
ret.description = description;
int n = 0;
for (int i = 0; i < this.ids.length; i++) {
if(idsHashSet.contains(this.ids[i])) {
ret.lats[n] = this.lats[i];
ret.lons[n] = this.lons[i];
ret.ids[n] = this.ids[i];
ret.polygons[n] = this.polygons[i];
n++;
}
}
return ret;
}
public PointSet slice(int start, int end) {
PointSet ret = new PointSet(end - start);
ret.id = id;
ret.label = label;
ret.description = description;
int n = 0;
for (int i = start; i < end; i++) {
ret.lats[n] = this.lats[i];
ret.lons[n] = this.lons[i];
ret.ids[n] = this.ids[i];
ret.polygons[n] = this.polygons[i];
n++;
}
for(Entry property : this.properties.entrySet()) {
int[] data = property.getValue();
int[] magSlice = new int[end-start];
n=0;
for(int i=start; i(this.capacity, 1f, -1);
for (int i = 0; i < this.capacity; i++) {
if (ids[i] != null) {
if (idIndexMap.containsKey(ids[i])) {
LOG.error("Duplicate ID {} in pointset.", ids[i]);
}
else {
idIndexMap.put(ids[i], i);
}
}
}
// now expose to public view; reference assignment is an atomic operation
this.idIndexMap = idIndexMap;
}
}
public static PointSet regularGrid (Envelope envelope, double gridSizeMeters) {
// non-ideal but for now make a grid in projected space
// to see why this is wrong, look at a map of Iowa and note that it leans to the left
// This is because they started surveying the township and range system (which is a grid)
// from the east, and wound up further west in the north than the south, which led them
// to resurvey the baseline every 24 miles, which explains why one is often driving
// down a rural road in the midwestern US and comes to a point where the road makes two 90-
// degree curves in quick succession to reach the new survey baseline.
double gridSizeLat = SphericalDistanceLibrary.metersToDegrees(gridSizeMeters);
double gridSizeLon = SphericalDistanceLibrary.metersToLonDegrees(gridSizeMeters, (envelope.getMaxY() + envelope.getMinY()) / 2);
// how large will it be?
int npts = (int) (envelope.getHeight() / gridSizeLat + 1) * (int) (envelope.getWidth() / gridSizeLon + 1);
PointSet ret = new PointSet(npts);
int idx = 0;
for (double lon = envelope.getMinX(); lon < envelope.getMaxX(); lon += gridSizeLon) {
for (double lat = envelope.getMinY(); lat < envelope.getMaxY(); lat += gridSizeLat) {
PointFeature pf = new PointFeature("" + idx);
pf.setLat(lat);
pf.setLon(lon);
ret.addFeature(pf, idx++);
}
}
return ret;
}
/** Returns a new coordinate object for the feature at the given index in this set, or its centroid. */
public Coordinate getCoordinate(int index) {
return new Coordinate(lons[index], lats[index]);
}
/**
* Using getter methods here to allow generating coordinates and geometries on demand instead of storing them.
* This would allow for implicit geometry, as in a regular grid of points.
*/
public double getLat (int i) {
return lats[i];
}
public double getLon (int i) {
return lons[i];
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy