/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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 org.apache.lucene.geo; import org.apache.lucene.util.SloppyMath; /** Draws shapes on the earth surface and renders using the very cool http://www.webglearth.org. * * Just instantiate this class, add the things you want plotted, and call {@link #finish} to get the * resulting HTML that you should save and load with a browser. */ public class EarthDebugger { final StringBuilder b = new StringBuilder(); private int nextShape; private boolean finished; public EarthDebugger() { b.append("<!DOCTYPE HTML>\n"); b.append("<html>\n"); b.append(" <head>\n"); b.append(" <script src=\"http://www.webglearth.com/v2/api.js\"></script>\n"); b.append(" <script>\n"); b.append(" function initialize() {\n"); b.append(" var earth = new WE.map('earth_div');\n"); } public EarthDebugger(double centerLat, double centerLon, double altitudeMeters) { b.append("<!DOCTYPE HTML>\n"); b.append("<html>\n"); b.append(" <head>\n"); b.append(" <script src=\"http://www.webglearth.com/v2/api.js\"></script>\n"); b.append(" <script>\n"); b.append(" function initialize() {\n"); b.append(" var earth = new WE.map('earth_div', {center: [" + centerLat + ", " + centerLon + "], altitude: " + altitudeMeters + "});\n"); } public void addPolygon(Polygon poly) { addPolygon(poly, "#00ff00"); } public void addPolygon(Polygon poly, String color) { String name = "poly" + nextShape; nextShape++; b.append(" var " + name + " = WE.polygon([\n"); double[] polyLats = poly.getPolyLats(); double[] polyLons = poly.getPolyLons(); for(int i=0;i<polyLats.length;i++) { b.append(" [" + polyLats[i] + ", " + polyLons[i] + "],\n"); } b.append(" ], {color: '" + color + "', fillColor: \"#000000\", fillOpacity: 0.0001});\n"); b.append(" " + name + ".addTo(earth);\n"); for (Polygon hole : poly.getHoles()) { addPolygon(hole, "#ffffff"); } } private static double MAX_KM_PER_STEP = 100.0; // Web GL earth connects dots by tunneling under the earth, so we approximate a great circle by sampling it, to minimize how deep in the // earth each segment tunnels: private int getStepCount(double minLat, double maxLat, double minLon, double maxLon) { double distanceMeters = SloppyMath.haversinMeters(minLat, minLon, maxLat, maxLon); return Math.max(1, (int) Math.round((distanceMeters / 1000.0) / MAX_KM_PER_STEP)); } // first point is inclusive, last point is exclusive! private void drawSegment(double minLat, double maxLat, double minLon, double maxLon) { int steps = getStepCount(minLat, maxLat, minLon, maxLon); for(int i=0;i<steps;i++) { b.append(" [" + (minLat + (maxLat - minLat) * i / steps) + ", " + (minLon + (maxLon - minLon) * i / steps) + "],\n"); } } public void addRect(double minLat, double maxLat, double minLon, double maxLon) { addRect(minLat, maxLat, minLon, maxLon, "#ff0000"); } public void addRect(double minLat, double maxLat, double minLon, double maxLon, String color) { String name = "rect" + nextShape; nextShape++; b.append(" // lat: " + minLat + " TO " + maxLat + "; lon: " + minLon + " TO " + maxLon + "\n"); b.append(" var " + name + " = WE.polygon([\n"); b.append(" // min -> max lat, min lon\n"); drawSegment(minLat, maxLat, minLon, minLon); b.append(" // max lat, min -> max lon\n"); drawSegment(maxLat, maxLat, minLon, maxLon); b.append(" // max -> min lat, max lon\n"); drawSegment(maxLat, minLat, maxLon, maxLon); b.append(" // min lat, max -> min lon\n"); drawSegment(minLat, minLat, maxLon, minLon); b.append(" // min lat, min lon\n"); b.append(" [" + minLat + ", " + minLon + "]\n"); b.append(" ], {color: \"" + color + "\", fillColor: \"" + color + "\"});\n"); b.append(" " + name + ".addTo(earth);\n"); } /** Draws a line a fixed latitude, spanning the min/max longitude */ public void addLatLine(double lat, double minLon, double maxLon) { String name = "latline" + nextShape; nextShape++; b.append(" var " + name + " = WE.polygon([\n"); double lon; int steps = getStepCount(lat, minLon, lat, maxLon); for(lon = minLon;lon<=maxLon;lon += (maxLon-minLon)/steps) { b.append(" [" + lat + ", " + lon + "],\n"); } b.append(" [" + lat + ", " + maxLon + "],\n"); lon -= (maxLon-minLon)/steps; for(;lon>=minLon;lon -= (maxLon-minLon)/steps) { b.append(" [" + lat + ", " + lon + "],\n"); } b.append(" ], {color: \"#ff0000\", fillColor: \"#ffffff\", opacity: 1, fillOpacity: 0.0001});\n"); b.append(" " + name + ".addTo(earth);\n"); } /** Draws a line a fixed longitude, spanning the min/max latitude */ public void addLonLine(double minLat, double maxLat, double lon) { String name = "lonline" + nextShape; nextShape++; b.append(" var " + name + " = WE.polygon([\n"); double lat; int steps = getStepCount(minLat, lon, maxLat, lon); for(lat = minLat;lat<=maxLat;lat += (maxLat-minLat)/steps) { b.append(" [" + lat + ", " + lon + "],\n"); } b.append(" [" + maxLat + ", " + lon + "],\n"); lat -= (maxLat-minLat)/36; for(;lat>=minLat;lat -= (maxLat-minLat)/steps) { b.append(" [" + lat + ", " + lon + "],\n"); } b.append(" ], {color: \"#ff0000\", fillColor: \"#ffffff\", opacity: 1, fillOpacity: 0.0001});\n"); b.append(" " + name + ".addTo(earth);\n"); } public void addPoint(double lat, double lon) { b.append(" WE.marker([" + lat + ", " + lon + "]).addTo(earth);\n"); } public void addCircle(double centerLat, double centerLon, double radiusMeters, boolean alsoAddBBox) { addPoint(centerLat, centerLon); String name = "circle" + nextShape; nextShape++; b.append(" var " + name + " = WE.polygon([\n"); inverseHaversin(b, centerLat, centerLon, radiusMeters); b.append(" ], {color: '#00ff00', fillColor: \"#000000\", fillOpacity: 0.0001 });\n"); b.append(" " + name + ".addTo(earth);\n"); if (alsoAddBBox) { Rectangle box = Rectangle.fromPointDistance(centerLat, centerLon, radiusMeters); addRect(box.minLat, box.maxLat, box.minLon, box.maxLon); addLatLine(Rectangle.axisLat(centerLat, radiusMeters), box.minLon, box.maxLon); } } public String finish() { if (finished) { throw new IllegalStateException("already finished"); } finished = true; b.append(" WE.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{\n"); b.append(" attribution: '© OpenStreetMap contributors'\n"); b.append(" }).addTo(earth);\n"); b.append(" }\n"); b.append(" </script>\n"); b.append(" <style>\n"); b.append(" html, body{padding: 0; margin: 0;}\n"); b.append(" #earth_div{top: 0; right: 0; bottom: 0; left: 0; position: absolute !important;}\n"); b.append(" </style>\n"); b.append(" <title>WebGL Earth API: Hello World</title>\n"); b.append(" </head>\n"); b.append(" <body onload=\"initialize()\">\n"); b.append(" <div id=\"earth_div\"></div>\n"); b.append(" </body>\n"); b.append("</html>\n"); return b.toString(); } private static void inverseHaversin(StringBuilder b, double centerLat, double centerLon, double radiusMeters) { double angle = 0; int steps = 100; newAngle: while (angle < 360) { double x = Math.cos(SloppyMath.toRadians(angle)); double y = Math.sin(SloppyMath.toRadians(angle)); double factor = 2.0; double step = 1.0; int last = 0; double lastDistanceMeters = 0.0; //System.out.println("angle " + angle + " slope=" + slope); while (true) { double lat = wrapLat(centerLat + y * factor); double lon = wrapLon(centerLon + x * factor); double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon); if (last == 1 && distanceMeters < lastDistanceMeters) { // For large enough circles, some angles are not possible: //System.out.println(" done: give up on angle " + angle); angle += 360./steps; continue newAngle; } if (last == -1 && distanceMeters > lastDistanceMeters) { // For large enough circles, some angles are not possible: //System.out.println(" done: give up on angle " + angle); angle += 360./steps; continue newAngle; } lastDistanceMeters = distanceMeters; //System.out.println(" iter lat=" + lat + " lon=" + lon + " distance=" + distanceMeters + " vs " + radiusMeters); if (Math.abs(distanceMeters - radiusMeters) < 0.1) { b.append(" [" + lat + ", " + lon + "],\n"); break; } if (distanceMeters > radiusMeters) { // too big //System.out.println(" smaller"); factor -= step; if (last == 1) { //System.out.println(" half-step"); step /= 2.0; } last = -1; } else if (distanceMeters < radiusMeters) { // too small //System.out.println(" bigger"); factor += step; if (last == -1) { //System.out.println(" half-step"); step /= 2.0; } last = 1; } } angle += 360./steps; } } // craziness for plotting stuff :) private static double wrapLat(double lat) { //System.out.println("wrapLat " + lat); if (lat > 90) { //System.out.println(" " + (180 - lat)); return 180 - lat; } else if (lat < -90) { //System.out.println(" " + (-180 - lat)); return -180 - lat; } else { //System.out.println(" " + lat); return lat; } } private static double wrapLon(double lon) { //System.out.println("wrapLon " + lon); if (lon > 180) { //System.out.println(" " + (lon - 360)); return lon - 360; } else if (lon < -180) { //System.out.println(" " + (lon + 360)); return lon + 360; } else { //System.out.println(" " + lon); return lon; } } }