| /* |
| * 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: [").append(centerLat).append(", ").append(centerLon).append("], altitude: ").append(altitudeMeters).append("});\n"); |
| } |
| |
| public void addPolygon(Polygon poly) { |
| addPolygon(poly, "#00ff00"); |
| } |
| |
| public void addPolygon(Polygon poly, String color) { |
| String name = "poly" + nextShape; |
| nextShape++; |
| |
| b.append(" var ").append(name).append(" = WE.polygon([\n"); |
| double[] polyLats = poly.getPolyLats(); |
| double[] polyLons = poly.getPolyLons(); |
| for(int i=0;i<polyLats.length;i++) { |
| b.append(" [").append(polyLats[i]).append(", ").append(polyLons[i]).append("],\n"); |
| } |
| b.append(" ], {color: '").append(color).append("', fillColor: \"#000000\", fillOpacity: 0.0001});\n"); |
| b.append(" ").append(name).append(".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(" [").append(minLat + (maxLat - minLat) * i / steps).append(", ").append(minLon + (maxLon - minLon) * i / steps).append("],\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: ").append(minLat).append(" TO ").append(maxLat).append("; lon: ").append(minLon).append(" TO ").append(maxLon).append("\n"); |
| b.append(" var ").append(name).append(" = 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(" [").append(minLat).append(", ").append(minLon).append("]\n"); |
| b.append(" ], {color: \"").append(color).append("\", fillColor: \"").append(color).append("\"});\n"); |
| b.append(" ").append(name).append(".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 ").append(name).append(" = WE.polygon([\n"); |
| double lon; |
| int steps = getStepCount(lat, minLon, lat, maxLon); |
| for(lon = minLon;lon<=maxLon;lon += (maxLon-minLon)/steps) { |
| b.append(" [").append(lat).append(", ").append(lon).append("],\n"); |
| } |
| b.append(" [").append(lat).append(", ").append(maxLon).append("],\n"); |
| lon -= (maxLon-minLon)/steps; |
| for(;lon>=minLon;lon -= (maxLon-minLon)/steps) { |
| b.append(" [").append(lat).append(", ").append(lon).append("],\n"); |
| } |
| b.append(" ], {color: \"#ff0000\", fillColor: \"#ffffff\", opacity: 1, fillOpacity: 0.0001});\n"); |
| b.append(" ").append(name).append(".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 ").append(name).append(" = WE.polygon([\n"); |
| double lat; |
| int steps = getStepCount(minLat, lon, maxLat, lon); |
| for(lat = minLat;lat<=maxLat;lat += (maxLat-minLat)/steps) { |
| b.append(" [").append(lat).append(", ").append(lon).append("],\n"); |
| } |
| b.append(" [").append(maxLat).append(", ").append(lon).append("],\n"); |
| lat -= (maxLat-minLat)/36; |
| for(;lat>=minLat;lat -= (maxLat-minLat)/steps) { |
| b.append(" [").append(lat).append(", ").append(lon).append("],\n"); |
| } |
| b.append(" ], {color: \"#ff0000\", fillColor: \"#ffffff\", opacity: 1, fillOpacity: 0.0001});\n"); |
| b.append(" ").append(name).append(".addTo(earth);\n"); |
| } |
| |
| public void addPoint(double lat, double lon) { |
| b.append(" WE.marker([").append(lat).append(", ").append(lon).append("]).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 ").append(name).append(" = WE.polygon([\n"); |
| inverseHaversin(b, centerLat, centerLon, radiusMeters); |
| b.append(" ], {color: '#00ff00', fillColor: \"#000000\", fillOpacity: 0.0001 });\n"); |
| b.append(" ").append(name).append(".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(" [").append(lat).append(", ").append(lon).append("],\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; |
| } |
| } |
| } |