/* * Copyright (c) 2016, Metron, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Metron, Inc. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.metsci.glimpse.dnc.convert; import static com.google.common.base.Objects.equal; import static com.metsci.glimpse.dnc.util.DncMiscUtils.filenameToLowercase; import static com.metsci.glimpse.dnc.util.DncMiscUtils.last; import static com.metsci.glimpse.dnc.util.DncMiscUtils.sorted; import static com.metsci.glimpse.dnc.util.DncMiscUtils.toArrayList; import static com.metsci.glimpse.util.GeneralUtils.ints; import static gov.nasa.worldwind.formats.vpf.VPFConstants.DATABASE_HEADER_TABLE; import static gov.nasa.worldwind.formats.vpf.VPFConstants.EDGE_PRIMITIVE_TABLE; import static java.util.Arrays.sort; import static java.util.Collections.reverse; import static java.util.Collections.unmodifiableList; import java.io.File; import java.io.FileFilter; import java.nio.file.Files; import java.util.ArrayList; import java.util.Comparator; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import gov.nasa.worldwind.formats.vpf.VPFBasicPrimitiveDataFactory; import gov.nasa.worldwind.formats.vpf.VPFBufferedRecordData; import gov.nasa.worldwind.formats.vpf.VPFCoverage; import gov.nasa.worldwind.formats.vpf.VPFFeature; import gov.nasa.worldwind.formats.vpf.VPFFeatureClass; import gov.nasa.worldwind.formats.vpf.VPFFeatureTableFilter; import gov.nasa.worldwind.formats.vpf.VPFFeatureType; import gov.nasa.worldwind.formats.vpf.VPFLibrary; import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData; import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData.EdgeInfo; import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData.FaceInfo; import gov.nasa.worldwind.formats.vpf.VPFPrimitiveData.Ring; import gov.nasa.worldwind.formats.vpf.VPFPrimitiveDataFactory; import gov.nasa.worldwind.formats.vpf.VPFSurfaceLine; import gov.nasa.worldwind.formats.vpf.VPFTile; import gov.nasa.worldwind.formats.vpf.VPFUtils; import gov.nasa.worldwind.geom.LatLon; import gov.nasa.worldwind.util.CompoundVecBuffer; import gov.nasa.worldwind.util.VecBuffer; import gov.nasa.worldwind.util.VecBufferSequence; public class Vpf { public static final Comparator<VPFLibrary> vpfLibraryNameComparator = new Comparator<VPFLibrary>() { public int compare(VPFLibrary a, VPFLibrary b) { return compareStringsCaseSecondary(a.getName(), b.getName()); } }; public static final Comparator<VPFCoverage> vpfCoverageNameComparator = new Comparator<VPFCoverage>() { public int compare(VPFCoverage a, VPFCoverage b) { return compareStringsCaseSecondary(a.getName(), b.getName()); } }; public static final Comparator<VPFFeatureClass> vpfFeatureClassNameComparator = new Comparator<VPFFeatureClass>() { public int compare(VPFFeatureClass a, VPFFeatureClass b) { return compareStringsCaseSecondary(a.getClassName(), b.getClassName()); } }; /** * Like case-insensitive comparison, but uses case-sensitive comparison as a tie-breaker. */ public static int compareStringsCaseSecondary(String a, String b) { int caseInsensitiveComparison = a.compareToIgnoreCase( b ); if ( caseInsensitiveComparison != 0 ) return caseInsensitiveComparison; int caseSensitiveComparison = a.compareTo( b ); return caseSensitiveComparison; } public static VPFFeatureClass[] readAllFeatureClasses(VPFLibrary lib) { return readFeatureClasses(lib, VPFFeatureType.values()); } public static VPFFeatureClass[] readFeatureClasses(VPFLibrary lib, VPFFeatureType... types) { EnumSet<VPFFeatureType> typeSet = EnumSet.noneOf(VPFFeatureType.class); for (VPFFeatureType t : types) typeSet.add(t); FileFilter featureTableFilter = new VPFFeatureTableFilter() { public boolean accept(File file) { // On a case-insensitive filesystem, java.io.File equality is case-insensitive, // so the file-equality test here always returns true. // // On a case-sensitive filesystem, Worldwind's VPF reader only works right with // lowercase filenames. In such a case, lowercase symlinks have been created for // any files with names containing uppercase chars. Consequently: // // 1. Every file is accessible using the lowercase version of its filename // 2. Files with uppercase chars should be ignored, to avoid duplicates // // Therefore, only accept files whose names have no uppercase chars. // return ( super.accept(file) && equal(file, filenameToLowercase(file)) ); } }; Set<VPFFeatureClass> featureClasses = new LinkedHashSet<>(); for (VPFCoverage cov : lib.getCoverages()) { if (cov.isReferenceCoverage()) continue; VPFFeatureClass[] fcs = VPFUtils.readFeatureClasses(cov, featureTableFilter); for (VPFFeatureClass fc : fcs) { VPFFeatureType type = fc.getType(); if (typeSet.contains(type)) featureClasses.add(fc); } } VPFFeatureClass[] featureClassesArray = featureClasses.toArray(new VPFFeatureClass[0]); sort( featureClassesArray, vpfFeatureClassNameComparator); return featureClassesArray; } public static Map<VPFCoverage,VPFPrimitiveData> createPrimitiveDatas(VPFLibrary lib, VPFTile tile) { VPFPrimitiveDataFactory factory = new VPFBasicPrimitiveDataFactory(tile); Map<VPFCoverage,VPFPrimitiveData> primitiveDatas = new LinkedHashMap<>(); for (VPFCoverage cov : sorted(lib.getCoverages(), vpfCoverageNameComparator)) { primitiveDatas.put(cov, factory.createPrimitiveData(cov)); } return primitiveDatas; } /** * Returns a map from database name to database dir */ public static Map<String,File> vpfDatabaseDirsByName(File parentDir) { Map<String,File> dbDirs = new LinkedHashMap<>(); File[] children = parentDir.listFiles(); if (children != null) { sort(children); for (File dbDir : children) { File dhtFile = findDhtFile(dbDir); if (dhtFile != null) { String dbName = readDatabaseName(dhtFile); if (dbName != null && !dbDirs.containsKey(dbName)) { dbDirs.put(dbName, dhtFile.getParentFile().getAbsoluteFile()); } } } } return dbDirs; } public static File findDhtFile(File dbDir) { File fileOrSymlink = null; File fileActual = null; File[] children = dbDir.listFiles(); if (children != null) { sort(children); for (File f : children) { if (f.getName().equalsIgnoreCase(DATABASE_HEADER_TABLE) && f.isFile()) { fileOrSymlink = f; if (!Files.isSymbolicLink(f.toPath())) { fileActual = f; } } } } return (fileActual == null ? fileOrSymlink : fileActual); } public static String readDatabaseName(File dhtFile) { if (dhtFile != null) { VPFBufferedRecordData dht = VPFUtils.readTable(dhtFile); if (dht != null && dht.getNumRecords() >= 1) { Object o = dht.getRecord(1).getValue("database_name"); if (o instanceof String) { return ((String) o); } } } return null; } public static class Edge { public final int start; public final int end; public final List<LatLon> locations; Edge(int start, int end, List<LatLon> locations) { this.start = start; this.end = end; this.locations = unmodifiableList(new ArrayList<LatLon>(locations)); } public Edge flipped() { List<LatLon> locations2 = new ArrayList<LatLon>(locations); reverse(locations2); return new Edge(end, start, locations2); } } public static boolean edgeIsReversed(int edgeOrientation) { switch (edgeOrientation) { case 1: return false; case -1: return true; default: throw new RuntimeException("Unrecognized edge-orientation code: " + edgeOrientation); } } public static List<Edge> extractRingEdges(VPFPrimitiveData primitiveData, VecBufferSequence edgeTableData, Ring ring) { List<Edge> edges = new ArrayList<>(); for (int i = 0; i < ring.getNumEdges(); i++) { int id = ring.getEdgeId(i); EdgeInfo info = (EdgeInfo) primitiveData.getPrimitiveInfo(EDGE_PRIMITIVE_TABLE, id); List<LatLon> locations = toArrayList( edgeTableData.slice(ints(id), 0, 1).getLocations() ); Edge edge = new Edge(info.getStartNode(), info.getEndNode(), locations); boolean reverse = edgeIsReversed(ring.getEdgeOrientation(i)); edges.add( reverse ? edge.flipped() : edge ); } return edges; } public static class RingEdgesBacktrackException extends RuntimeException { public final List<Edge> edgesExtracted; public final List<Edge> edgesRegularizedSoFar; public final Edge currentEdge; public RingEdgesBacktrackException(List<Edge> edgesExtracted, List<Edge> edgesRegularizedSoFar) { this(edgesExtracted, edgesRegularizedSoFar, null); } public RingEdgesBacktrackException(List<Edge> edgesExtracted, List<Edge> edgesRegularizedSoFar, Edge currentEdge) { super("Ring-edges backtrack failed"); this.edgesExtracted = unmodifiableList(new ArrayList<Edge>(edgesExtracted)); this.edgesRegularizedSoFar = unmodifiableList(new ArrayList<Edge>(edgesRegularizedSoFar)); this.currentEdge = currentEdge; } @Override public String toString() { StringBuilder s = new StringBuilder(super.toString()); s.append(" ... edges:"); for (Edge edge : edgesExtracted) s.append(edge.start).append("-").append(edge.end).append(","); s.setLength(s.length()-1); s.append(" ... regularized:"); for (Edge edge : edgesRegularizedSoFar) s.append(edge.start).append("-").append(edge.end).append(","); s.setLength(s.length()-1); if (currentEdge != null) { s.append(" ... current:"); s.append(currentEdge.start).append("-").append(currentEdge.end); } return s.toString(); } } public static List<Edge> regularizeRingEdges(List<Edge> extractedRingEdges) { List<Edge> regularized = new ArrayList<>(); Stack<Edge> stack = new Stack<Edge>(); for (int i = 0; i < extractedRingEdges.size(); i++) { Edge current = extractedRingEdges.get(i); regularized.add(current); stack.push(current); // When an edge is completely contained in the polygon, WWJ only gives us the edge once, // making sort of a T intersection (... AB, BC, BD, DE, ...). To make a well-behaved // polygon, we insert "backtrack" edges (... AB, BC, *CB*, BD, DE, ...). // // Note that we may have to backtrack after the last edge, to get back to the start of // the first edge -- hence the "(i+1) % size" here. // Edge next = extractedRingEdges.get((i + 1) % extractedRingEdges.size()); while (last(regularized).end != next.start) { // If the stack is empty, it means next.start is a location the polygon hasn't visited // before ... which means that these edges just don't make a well-behaved polygon. // if (stack.isEmpty()) throw new RingEdgesBacktrackException(extractedRingEdges, regularized, current); regularized.add( stack.pop().flipped() ); } } return regularized; } public static List<LatLon> locations(List<Edge> edges) { List<LatLon> locations = new ArrayList<>(); for (Edge edge : edges) locations.addAll(edge.locations); return locations; } public static List<LatLon> ringLocations(VPFPrimitiveData primitiveData, VecBufferSequence edgeTableData, Ring ring) { return locations( regularizeRingEdges( extractRingEdges(primitiveData, edgeTableData, ring) ) ); } public static List<List<LatLon>> vpfAreaRings(VPFFeature areaFeature, VPFPrimitiveData primitiveData) { List<List<LatLon>> vertices = new ArrayList<>(); VecBufferSequence edgeTableData = primitiveData.getPrimitiveCoords(EDGE_PRIMITIVE_TABLE); String primitiveName = areaFeature.getFeatureClass().getPrimitiveTableName(); for (int id : areaFeature.getPrimitiveIds()) { FaceInfo faceInfo = (FaceInfo) primitiveData.getPrimitiveInfo(primitiveName, id); vertices.add( ringLocations(primitiveData, edgeTableData, faceInfo.getOuterRing()) ); for (Ring innerRing : faceInfo.getInnerRings()) { vertices.add( ringLocations(primitiveData, edgeTableData, innerRing) ); } } return vertices; } public static List<LatLon> vpfLineVertices(VPFFeature lineFeature, VPFPrimitiveData primitiveData) { VPFSurfaceLine line = new VPFSurfaceLine(lineFeature, primitiveData); List<LatLon> lines = new ArrayList<>(); LatLon vStart = null; for (LatLon vEnd : line.getLocations()) { if (vStart != null) { lines.add(vStart); lines.add(vEnd); } vStart = vEnd; } return lines; } public static LatLon vpfPointVertex(VPFFeature pointFeature, VPFPrimitiveData primitiveData) { List<LatLon> points = new ArrayList<>(); String primitiveName = pointFeature.getFeatureClass().getPrimitiveTableName(); CompoundVecBuffer combinedCoords = primitiveData.getPrimitiveCoords(primitiveName); if (combinedCoords != null) { for (int id : pointFeature.getPrimitiveIds()) { VecBuffer coords = combinedCoords.subBuffer(id); if (coords == null || coords.getSize() == 0) continue; for (LatLon location : coords.getLocations()) points.add(location); } } if (points.size() != 1) throw new RuntimeException("Point feature has " + points.size() + " vertices"); return points.get(0); } }