package me.osm.gazetter.striper; import static me.osm.gazetter.join.out_handlers.GazetteerSchemeConstants.GAZETTEER_SCHEME_MD5; import static me.osm.gazetter.join.out_handlers.GazetteerSchemeConstants.GAZETTEER_SCHEME_TIMESTAMP; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import me.osm.gazetter.utils.HilbertCurveHasher; import me.osm.gazetter.utils.JSONHash; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.LocalDateTime; import org.json.JSONArray; import org.json.JSONObject; import org.json.JSONString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * Reeds and writes projects JSON encoded data. * * Allows to write some technical info, also allows to get some data * without full decomposition of JSON feature. * */ public class GeoJsonWriter { private static final String GEOMETRY_TYPE = "type"; private static final String GEOJSON_TYPE_KEY = "type"; private static final String GEOJSON_TYPE_VAL = "Feature"; private static final Logger log = LoggerFactory.getLogger(GeoJsonWriter.class.getName()); private static final String TIMESTAMP_PATTERN = "\"" + GeoJsonWriter.TIMESTAMP + "\":\""; private static final String ID_PATTERN = "\"id\":\""; private static final String HHASH_PATTERN = "\"hhash\":\""; private static final String FTYPE_PATTERN = "\"ftype\":\""; private static final String ACTION_PATTERN = "\"action\":\""; private static final String ADM_LVL_PATTERN = "\"admin_level\":\""; private static final String MD5_PATTERN = "\"md5\":\""; public static final String META = "metainfo"; public static final String FULL_GEOMETRY = "fullGeometry"; public static final String PROPERTIES = "properties"; public static final String COORDINATES = "coordinates"; public static final String GEOMETRY = "geometry"; public static final String ORIGINAL_BBOX = "origBBOX"; public static final String TIMESTAMP = "timestamp"; private static final GeometryFactory factory = new GeometryFactory(); private static final DateTimeZone timeZone = DateTimeZone.getDefault(); private static final class JsonStringWrapper implements JSONString { private String s; public JsonStringWrapper(String s) { this.s = s; } @Override public String toJSONString() { return this.s; } } public static JSONObject geometryToJSON(Geometry g) { if(g == null) { return null; } if(g instanceof MultiPolygon) { JSONObject geomJSON = new JSONObject(); geomJSON.put(GEOMETRY_TYPE, "MultiPolygon"); List<String> rings = new ArrayList<>(); for(int i = 0; i < g.getNumGeometries(); i++) { rings.add(asJsonString((Polygon) g.getGeometryN(i))); } geomJSON.put(COORDINATES, new JsonStringWrapper("[" + StringUtils.join(rings, ",") + "]")); return geomJSON; } if(g instanceof Polygon) { JSONObject geomJSON = new JSONObject(); geomJSON.put(GEOMETRY_TYPE, "Polygon"); geomJSON.put(COORDINATES, new JsonStringWrapper(asJsonString((Polygon) g))); return geomJSON; } else if (g instanceof LineString) { JSONObject geomJSON = new JSONObject(); geomJSON.put(GEOMETRY_TYPE, "LineString"); geomJSON.put(COORDINATES, new JsonStringWrapper(asJsonString((LineString) g))); return geomJSON; } else if(g instanceof Point) { JSONObject geomJSON = new JSONObject(); geomJSON.put(GEOMETRY_TYPE, "Point"); geomJSON.put(COORDINATES, new JsonStringWrapper("[" + String.format(Locale.US, "%.8f", ((Point)g).getX()) + "," + String.format(Locale.US, "%.8f", ((Point)g).getY()) + "]")); return geomJSON; } return null; } public static String featureAsGeoJSON(String id, String type, Map<String, String> attributes, Geometry g, JSONObject meta) { JSONObject feature = createFeature(id, type, attributes, g, meta); addTimestamp(feature); return feature.toString(); } public static JSONObject createFeature(String id, String type, Map<String, String> attributes, Geometry g, JSONObject meta) { JSONObject feature = new JSONFeature(); if(id != null) { feature.put("id", id); } feature.put("ftype", type); feature.put(GEOJSON_TYPE_KEY, GEOJSON_TYPE_VAL); feature.put(GEOMETRY, geometryToJSON(g)); feature.put(PROPERTIES, attributes); feature.put(META, meta); feature.put(TIMESTAMP, LocalDateTime.now().toDateTime(timeZone).toInstant().toString()); return feature; } private static String asJsonString(Polygon polygon) { StringBuilder rings = new StringBuilder(); rings.append(",").append(asJsonString(polygon.getExteriorRing())); for(int i=0; i < polygon.getNumInteriorRing(); i++) { rings.append(",").append(asJsonString(polygon.getInteriorRingN(i))); } return "[" + rings.substring(1) + "]"; } private static String asJsonString(LineString ring) { StringBuilder sb = new StringBuilder(); for(Coordinate c : ring.getCoordinates()) { sb.append(",[").append(String.format(Locale.US, "%.8f", c.x)).append(",").append(String.format(Locale.US, "%.8f", c.y)).append("]"); } return "[" + sb.substring(1) + "]"; } public static String getId(String type, Point point, JSONObject meta) { long hash = HilbertCurveHasher.encode(point.getX(), point.getY()); String mainPart = type + "-" + String.format("%010d", hash) + "-" + meta.getString(GEOMETRY_TYPE).charAt(0) + meta.optLong("id"); int counter = meta.optInt("counter", -1); if(counter >= 0) { mainPart += "-" + counter; } return mainPart; } public static void addTimestamp(JSONObject json) { json.put(TIMESTAMP, getNowTimestampString()); } public static String getNowTimestampString() { LocalDateTime date = LocalDateTime.now(); String timestampString = date.toDateTime(timeZone).toInstant().toString(); return timestampString; } public static String getMD5(String line) { int indexOf = line.indexOf(MD5_PATTERN); if(indexOf >= 0) { int begin = indexOf + MD5_PATTERN.length(); int end = line.indexOf("\"", begin); return line.substring(begin, end); } log.error("Can't parse timestamp for line {}", line); return null; } public static DateTime getTimestamp(String line) { int indexOf = line.indexOf(TIMESTAMP_PATTERN); if(indexOf >= 0) { int begin = indexOf + TIMESTAMP_PATTERN.length(); int end = line.indexOf("\"", begin); String timestampString = line.substring(begin, end); try { return new DateTime(timestampString); } catch (Exception e) { log.error("Can't parse timestamp {} for line {}", timestampString, line); } } log.error("Can't parse timestamp for line {}", line); return null; } public static String getId(String line) { int begin = line.indexOf(ID_PATTERN) + ID_PATTERN.length(); int end = line.indexOf("\"", begin); return line.substring(begin, end); } public static String getHHash(String line) { int begin = line.indexOf(HHASH_PATTERN) + HHASH_PATTERN.length(); int end = line.indexOf("\"", begin); return line.substring(begin, end); } public static String getFtype(String line) { int begin = line.indexOf(FTYPE_PATTERN) + FTYPE_PATTERN.length(); int end = line.indexOf("\"", begin); return line.substring(begin, end); } public static Polygon getPolygonGeometry(JSONObject polygon) { JSONArray coords = polygon.getJSONObject("geometry").getJSONArray("coordinates"); return getPolygonGeometry(coords); } public static MultiPolygon getMultiPolygonGeometry(JSONArray polygon) { Polygon polygons[] = new Polygon[polygon.length()]; for(int i = 0; i < polygon.length(); i++) { polygons[i] = getPolygonGeometry(polygon.getJSONArray(i)); } return factory.createMultiPolygon(polygons); } public static Polygon getPolygonGeometry(JSONArray coords) { LinearRing shell = null; LinearRing[] holes = new LinearRing[coords.length() - 1]; for(int lineIndex = 0;lineIndex < coords.length(); lineIndex++) { JSONArray line = coords.getJSONArray(lineIndex); LinearRing lg = getLinearRingGeometry(line); if(lineIndex == 0) { shell = lg; } else { holes[lineIndex - 1] = lg; } } return factory.createPolygon(shell, holes); } public static LinearRing getLinearRingGeometry(JSONArray line) { Coordinate[] coords = new Coordinate[line.length()]; for(int i = 0; i < line.length(); i++) { JSONArray p = line.getJSONArray(i); coords[i] = new Coordinate(p.getDouble(0), p.getDouble(1)); } return factory.createLinearRing(coords); } public static LineString getLineStringGeometry(JSONArray coordsJSON) { Coordinate[] coords = new Coordinate[coordsJSON.length()]; for(int i = 0; i < coordsJSON.length(); i++) { JSONArray p = coordsJSON.getJSONArray(i); coords[i] = new Coordinate(p.getDouble(0), p.getDouble(1)); } return factory.createLineString(coords); } public static String getAction(String line) { if(line.contains(ACTION_PATTERN)) { int begin = line.indexOf(ACTION_PATTERN) + ACTION_PATTERN.length(); if(begin >= 0) { int end = line.indexOf("\"", begin); return line.substring(begin, end); } } return null; } public static String getAdmLevel(String line) { if(line.contains(ADM_LVL_PATTERN)) { int begin = line.indexOf(ADM_LVL_PATTERN) + ADM_LVL_PATTERN.length(); if(begin >= 0) { int end = line.indexOf("\"", begin); return line.substring(begin, end); } } return null; } public static Geometry parseGeometry(JSONObject geom) { if(geom != null) { String type = geom.optString("type"); JSONArray coords = geom.getJSONArray("coordinates"); switch (type) { case "Point": return factory.createPoint(new Coordinate(coords.getDouble(0), coords.getDouble(1))); case "LineString": return getLineStringGeometry(coords); case "Polygon": return getPolygonGeometry(coords); case "MultiPolygon": return getMultiPolygonGeometry(coords); } } return null; } private static final Set<String> hashIgnoreFields = new HashSet<String>( Arrays.asList(new String[]{GAZETTEER_SCHEME_TIMESTAMP})); public static void addMD5(JSONObject poi) { poi.put(GAZETTEER_SCHEME_MD5, DigestUtils.md5Hex(JSONHash.asCanonicalString( poi, hashIgnoreFields) )); } }