org.dstadler.ctw.geojson.CreateGeoJSON Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cover-the-world Show documentation
Show all versions of cover-the-world Show documentation
Read GPX tracks and produce a world-map with covered tiles.
package org.dstadler.ctw.geojson;
import static org.dstadler.ctw.gpx.CreateListOfVisitedSquares.VISITED_SQUARES_NEW_TXT;
import static org.dstadler.ctw.gpx.CreateListOfVisitedSquares.VISITED_SQUARES_TXT;
import static org.dstadler.ctw.gpx.CreateListOfVisitedSquares.VISITED_TILES_NEW_TXT;
import static org.dstadler.ctw.gpx.CreateListOfVisitedSquares.VISITED_TILES_TXT;
import static org.dstadler.ctw.utils.Constants.SQUARE_SIZE;
import static org.dstadler.ctw.utils.Constants.TILE_ZOOM;
import static org.dstadler.ctw.utils.Constants.ZONE;
import java.awt.Rectangle;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.dstadler.commons.logging.jdk.LoggerFactory;
import org.dstadler.ctw.utils.BaseTile;
import org.dstadler.ctw.utils.LatLonRectangle;
import org.dstadler.ctw.utils.MatrixUtils;
import org.dstadler.ctw.utils.OSMTile;
import org.dstadler.ctw.utils.UTMRefWithHash;
import com.github.filosganga.geogson.model.Feature;
import com.github.filosganga.geogson.model.LinearRing;
import com.github.filosganga.geogson.model.Point;
import com.github.filosganga.geogson.model.Polygon;
import com.google.common.base.Preconditions;
import com.google.gson.JsonPrimitive;
/**
* Small application which reads the list of covered squares
* (twice for all covered and only new squares)
* from a simple text-file and produces GeoJSON embedded in
* a JavaScript file which can be loaded in a leaflet.js map
* to display covered squares.
*
* Results are stored in JS files which can be used as overlay
* layer in a Leaflet-based HTML page.
*/
public class CreateGeoJSON {
private static final Logger log = LoggerFactory.make();
// squares
public static final String VISITED_SQUARES_JS = "js/VisitedSquares.js";
public static final String VISITED_SQUARES_NEW_JS = "js/VisitedSquaresNew.js";
// tiles
public static final String VISITED_TILES_JS = "js/VisitedTiles.js";
public static final String VISITED_TILES_NEW_JS = "js/VisitedTilesNew.js";
// for printing stats when writing tiles
private static final AtomicLong lastLog = new AtomicLong();
public static void main(String[] args) throws IOException {
LoggerFactory.initLogging();
log.info("Computing GeoJSON for visited squares and tiles");
writeGeoJSON(VISITED_SQUARES_TXT, VISITED_SQUARES_JS, "squares",
UTMRefWithHash::getRectangle, UTMRefWithHash::fromString, "squares");
writeGeoJSON(VISITED_SQUARES_NEW_TXT, VISITED_SQUARES_NEW_JS, "squaresnew",
UTMRefWithHash::getRectangle, UTMRefWithHash::fromString, "new squares");
writeGeoJSON(VISITED_TILES_TXT, VISITED_TILES_JS, "tiles",
OSMTile::getRectangle, OSMTile::fromString, "tiles");
writeGeoJSON(VISITED_TILES_NEW_TXT, VISITED_TILES_NEW_JS, "tilesnew",
OSMTile::getRectangle, OSMTile::fromString, "new tiles");
}
protected static > void writeGeoJSON(String squaresFile, String jsonOutputFile, String varPrefix,
Function toRectangle,
Function toObject,
String title) throws IOException {
log.info(title + ": Writing from " + squaresFile + " to " + jsonOutputFile +
" with prefix '" + varPrefix + "'");
// read list of UTMRefs for covered or new squares
Set squares = readSquares(new File(squaresFile)).
stream().
map(toObject).
collect(Collectors.toCollection(TreeSet::new));
log.info(title + ": Read " + squares.size());
List features = new ArrayList<>();
// first look for single squares/tiles which we cannot combine anyway to
// make computing the largest rectangles a bit cheaper
handleSingleAreas(toRectangle, squares, features);
// build an optimized GeoJSON as including all squares/tiles lead to a fairly large GeoJSON
// which causes performance issues e.g. on Smartphone-Browsers
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE,
minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE;
boolean found = false;
boolean[] isY = null;
if (squares.size() > 0 && squares.iterator().next() instanceof OSMTile) {
for (OSMTile tile : (Set) squares) {
if (tile.getXTile() > maxX) {
maxX = tile.getXTile();
}
if (tile.getXTile() < minX) {
minX = tile.getXTile();
}
if (tile.getYTile() > maxY) {
maxY = tile.getYTile();
}
if (tile.getYTile() < minY) {
minY = tile.getYTile();
}
found = true;
}
isY = computePopulatedRows(title, (Set) squares, minX, minY, maxX, maxY);
}
// next create as many rectangles as possible to minimize the resulting GeoJSON
int iterationCount = 0;
while (squares.size() > 0) {
final Feature rectangle;
final BaseTile next = squares.iterator().next();
if (next instanceof UTMRefWithHash) {
//noinspection unchecked
rectangle = getSquareRectangle((Set) squares, null, "squares");
} else {
// stop if all the remaining tiles are outside the default UTM-zone
if (!found) {
break;
}
//noinspection unchecked
rectangle = getTileRectangleInternal((Set) squares, null, "tiles", minX, minY, maxX, maxY, isY);
//log.info(title + ": Remaining " + squares.size() + ", found: " + rectangle);
}
if (rectangle == null) {
break;
}
features.add(rectangle);
iterationCount++;
if (iterationCount % 100 == 0 && next instanceof OSMTile) {
// recompute optimization from time to time to skip any new single squares
// which are now there because of found and removed rectangles
handleSingleAreas(toRectangle, squares, features);
// re-compute which rows are empty from time to time to speed up processing a bit
isY = computePopulatedRows(title, (Set) squares, minX, minY, maxX, maxY);
}
if (lastLog.get() + TimeUnit.SECONDS.toMillis(5) < System.currentTimeMillis()) {
log.info(title + ": Found " + features.size() + " features, having " + squares.size() + " " +
title + " remaining, details: " + rectangle);
lastLog.set(System.currentTimeMillis());
}
}
log.info(title + ": Found " + features.size() + " rectangles, having " + squares.size() + " single squares remaining");
// then add all remaining single-squares
for (T square : squares) {
features.add(GeoJSON.createSquare(toRectangle.apply(square),
null
/*square + "\n" + toRectangle.apply(square)*/));
}
// finally write out JavaScript code with embedded GeoJSON
GeoJSON.writeGeoJavaScript(jsonOutputFile, varPrefix, features);
// also write the file in pure JSON for use in later steps
GeoJSON.writeGeoJSON(GeoJSON.getJSONFileName(jsonOutputFile), features);
log.info(title + ": Wrote " + features.size() + " features with " + squares.size() + " single " + title + " from " + squaresFile + " to " + jsonOutputFile);
}
private static > boolean[] computePopulatedRows(String title,
Set squares, int minX, int minY, int maxX, int maxY) {
int[][] M = MatrixUtils.populateMatrix(squares, minX, minY, maxX, maxY);
boolean[] isY = new boolean[M.length];
int count = MatrixUtils.findPopulatedRows(M, isY);
log.info(title + ": Found " + count + " populated rows of " + isY.length + " overall");
return isY;
}
private static > void handleSingleAreas(Function toRectangle,
Set squares,
List features) {
int count = 0;
Iterator it = squares.iterator();
while (it.hasNext()) {
T square = it.next();
// check if this square is single
if (!squares.contains(square.up()) &&
!squares.contains(square.down()) &&
!squares.contains(square.left()) &&
!squares.contains(square.right())
) {
features.add(GeoJSON.createSquare(toRectangle.apply(square),
null
/*square + "\n" + toRectangle.apply(square)*/));
it.remove();
count++;
}
}
log.info("Found " + count + " single areas");
}
protected static Set readSquares(File file) throws IOException {
return file.exists() ?
new TreeSet<>(FileUtils.readLines(file, StandardCharsets.UTF_8)) :
Collections.emptySet();
}
public static Feature getTileRectangle(Set tiles, String textFile, String title) throws IOException {
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE,
minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE;
boolean found = false;
for (OSMTile tile : tiles) {
if (tile.getXTile() > maxX) {
maxX = tile.getXTile();
}
if (tile.getXTile() < minX) {
minX = tile.getXTile();
}
if (tile.getYTile() > maxY) {
maxY = tile.getYTile();
}
if (tile.getYTile() < minY) {
minY = tile.getYTile();
}
found = true;
}
// stop if all the remaining tiles are outside the default UTM-zone
if (!found) {
return null;
}
return getTileRectangleInternal(tiles, textFile, title, minX, minY, maxX, maxY, null);
}
private static Feature getTileRectangleInternal(Set tiles, String textFile, String title, int minX, int minY, int maxX, int maxY,
boolean[] isY)
throws IOException {
int[][] M = MatrixUtils.populateMatrix(tiles, minX, minY, maxX, maxY);
if (isY == null) {
isY = new boolean[M.length];
MatrixUtils.findPopulatedRows(M, isY);
}
Pair result = MatrixUtils.maxRectangle(M, isY);
Rectangle rect = result.getKey();
// stop when we do not find any real rectangles any more
if (rect.width == 1 && rect.height == 1) {
return null;
}
if (log.isLoggable(Level.FINE)) {
log.fine("Area of maximum rectangle " + rect + ": " + rect.width + "x" + rect.height +
" = " + result.getValue());
}
OSMTile squareMin = new OSMTile(TILE_ZOOM, minX + rect.x - rect.width, minY + rect.y - rect.height + 1);
OSMTile squareMax = new OSMTile(TILE_ZOOM, minX + rect.x, minY + rect.y + 1);
for (int y = squareMin.getYTile(); y < squareMax.getYTile(); y++) {
for (int x = squareMin.getXTile(); x < squareMax.getXTile(); x++) {
final boolean removed = tiles.remove(new OSMTile(TILE_ZOOM, x, y));
Preconditions.checkState(removed,
"Should always remove squares, but did not for \n%s %s",
x, y);
}
}
/*UTMRefWithHash recRefMin = UTMRefWithHash.fromString(ZONE + "U " +
(minEast + (rect.x - rect.width) * SQUARE_SIZE) + " " + (minNorth + (rect.y - rect.height) * SQUARE_SIZE + SQUARE_SIZE));
UTMRefWithHash recRefMax = UTMRefWithHash.fromString(ZONE + "U " +
(minEast + rect.x * SQUARE_SIZE) + " " + (minNorth + rect.y * SQUARE_SIZE + SQUARE_SIZE));
log.fine("Found largest square at " + rect.x + "x" + rect.y +
"\n" + recRefMin +
"\n" + recRefMax +
"\n" + squareMin.getStartLatLon() +
"\n" + squareMax.getStartLatLon() +
"\n" + OSMTile.fromLatLngZoom(squareMin.getStartLatLon().getLatitude(), squareMin.getStartLatLon().getLongitude(), 13) +
"\n" + OSMTile.fromLatLngZoom(squareMax.getStartLatLon().getLatitude(), squareMax.getStartLatLon().getLongitude(), 13));*/
if (textFile != null) {
// create list of latLngBounds for SVG elements to overlay
try (Writer writer = new BufferedWriter(new FileWriter(textFile))) {
writer.write(squareMin.toCoords());
writer.write('\n');
writer.write(squareMax.toCoords());
writer.write('\n');
writer.write(rect.x + "x" + rect.y);
writer.write('\n');
writer.write(rect.width + "x" + rect.height);
writer.write('\n');
writer.write(result.getValue().toString());
writer.write('\n');
}
}
// produce the GeoJSON for the rectangle
Feature.Builder builder = Feature.builder().withGeometry(Polygon.of(LinearRing.of(
Point.from(squareMin.toLatLng().getLongitude(), squareMin.toLatLng().getLatitude()),
Point.from(squareMax.toLatLng().getLongitude(), squareMin.toLatLng().getLatitude()),
Point.from(squareMax.toLatLng().getLongitude(), squareMax.toLatLng().getLatitude()),
Point.from(squareMin.toLatLng().getLongitude(), squareMax.toLatLng().getLatitude()),
Point.from(squareMin.toLatLng().getLongitude(), squareMin.toLatLng().getLatitude())
)));
// only set properties if we also write an overview file to keep file-size of some geo-json files at minimum
if (textFile != null) {
return builder.withProperty("popupContent", new JsonPrimitive(
result.getValue() + " " + title + ": " + rect.width + "x" + rect.height)).build();
} else {
return builder.build();
}
}
public static Feature getSquareRectangle(Set squares, String textFile, String title) throws IOException {
double minEast = Double.MAX_VALUE, maxEast = Double.MIN_VALUE,
minNorth = Double.MAX_VALUE, maxNorth = Double.MIN_VALUE;
boolean found = false;
for (UTMRefWithHash square : squares) {
if (square.getLngZone() != ZONE) {
continue;
}
if (square.getEasting() > maxEast) {
maxEast = square.getEasting();
}
if (square.getEasting() < minEast) {
minEast = square.getEasting();
}
if (square.getNorthing() > maxNorth) {
maxNorth = square.getNorthing();
}
if (square.getNorthing() < minNorth) {
minNorth = square.getNorthing();
}
found = true;
}
// stop if all the remaining squares are outside the default UTM-zone
if (!found) {
return null;
}
int[][] M = MatrixUtils.populateMatrix(squares, minEast, minNorth, maxEast, maxNorth, ZONE);
Pair result = MatrixUtils.maxRectangle(M);
Rectangle rect = result.getKey();
// stop when we do not find any real rectangles any more
if (rect.width == 1 && rect.height == 1) {
return null;
}
if (log.isLoggable(Level.FINE)) {
log.fine("Area of maximum rectangle " + rect + ": " + rect.width + "x" + rect.height +
" = " + result.getValue());
}
UTMRefWithHash recRefMinMin = new UTMRefWithHash(ZONE, 'U',
(minEast + (rect.x - rect.width) * SQUARE_SIZE), (minNorth + (rect.y - rect.height) * SQUARE_SIZE + SQUARE_SIZE));
UTMRefWithHash recRefMaxMin = new UTMRefWithHash(ZONE, 'U',
(minEast + (rect.x - rect.width) * SQUARE_SIZE), (minNorth + rect.y * SQUARE_SIZE + SQUARE_SIZE));
UTMRefWithHash recRefMinMax = new UTMRefWithHash(ZONE, 'U',
(minEast + rect.x * SQUARE_SIZE), (minNorth + (rect.y - rect.height) * SQUARE_SIZE + SQUARE_SIZE));
UTMRefWithHash recRefMaxMax = new UTMRefWithHash(ZONE, 'U',
(minEast + rect.x * SQUARE_SIZE), (minNorth + rect.y * SQUARE_SIZE + SQUARE_SIZE));
if (log.isLoggable(Level.FINE)) {
log.fine("Found largest rectangle at " + rect.x + "x" + rect.y +
"\n" + recRefMinMin +
"\n" + recRefMaxMin +
"\n" + recRefMinMax +
"\n" + recRefMaxMax +
"\n" + recRefMinMin.toLatLng() +
"\n" + recRefMaxMin.toLatLng() +
"\n" + recRefMinMax.toLatLng() +
"\n" + recRefMaxMax.toLatLng() +
"\n" + OSMTile.fromLatLngZoom(recRefMinMin.toLatLng().getLatitude(), recRefMinMin.toLatLng().getLongitude(),13) +
"\n" + OSMTile.fromLatLngZoom(recRefMaxMin.toLatLng().getLatitude(), recRefMaxMin.toLatLng().getLongitude(),13) +
"\n" + OSMTile.fromLatLngZoom(recRefMinMax.toLatLng().getLatitude(), recRefMinMax.toLatLng().getLongitude(),13) +
"\n" + OSMTile.fromLatLngZoom(recRefMaxMax.toLatLng().getLatitude(), recRefMaxMax.toLatLng().getLongitude(),13));
}
// remove all squares of the rectangle from the list of remaining squares
for (double easting = recRefMinMin.getEasting(); easting < recRefMinMax.getEasting(); easting+=SQUARE_SIZE) {
for (double northing = recRefMinMax.getNorthing(); northing < recRefMaxMax.getNorthing(); northing+=SQUARE_SIZE) {
final UTMRefWithHash ref = new UTMRefWithHash(ZONE, 'U', easting, northing);
final UTMRefWithHash refFixed = new UTMRefWithHash(ZONE, ref.toLatLng().toUTMRef().getLatZone(), easting, northing);
final boolean removed = squares.remove(
// have to fix-up latZone
refFixed);
Preconditions.checkState(
removed,
"Should always remove squares, but did not for \n%s %s: \n%s and %s",
easting, northing, ref, refFixed);
}
}
// if specified, write out a text-file with a summary of the found rectangle
if (textFile != null) {
try (Writer writer = new BufferedWriter(new FileWriter(textFile))) {
writer.write(recRefMinMin.toString());
writer.write('\n');
writer.write(recRefMaxMax.toString());
writer.write('\n');
writer.write(rect.x + "x" + rect.y);
writer.write('\n');
writer.write(rect.width + "x" + rect.height);
writer.write('\n');
writer.write(result.getValue().toString());
writer.write('\n');
}
}
// produce the GeoJSON structure for the rectangle
Feature.Builder builder = Feature.builder().withGeometry(Polygon.of(LinearRing.of(
Point.from(recRefMinMin.toLatLng().getLongitude(), recRefMinMin.toLatLng().getLatitude()),
Point.from(recRefMaxMin.toLatLng().getLongitude(), recRefMaxMin.toLatLng().getLatitude()),
Point.from(recRefMaxMax.toLatLng().getLongitude(), recRefMaxMax.toLatLng().getLatitude()),
Point.from(recRefMinMax.toLatLng().getLongitude(), recRefMinMax.toLatLng().getLatitude()),
Point.from(recRefMinMin.toLatLng().getLongitude(), recRefMinMin.toLatLng().getLatitude())
)));
// only set properties if we also write an overview file to keep file-size of some geo-json files at minimum
if (textFile != null) {
return builder.withProperty("popupContent", new JsonPrimitive(
result.getValue() + " " + title + ": " + rect.width + "x" + rect.height)).build();
} else {
return builder.build();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy