package net.sourceforge.gpstools;
/* gpsdings
* Copyright (C) 2006 Moritz Ringler
* $Id: GoogleMapMaker.java 441 2010-12-13 20:04:20Z ringler $
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.awt.geom.Rectangle2D;
import java.io.PrintStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import net.sourceforge.gpstools.gpx.Trkpt;
import net.sourceforge.gpstools.gpx.Wpt;
import net.sourceforge.gpstools.utils.GpsFormat;
import net.sourceforge.gpstools.utils.GpsMath;
import net.sourceforge.gpstools.utils.TemplateEvent;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Options;
public class GoogleMapMaker extends AbstractMapMaker {
private boolean encodePolylines = false;
public GoogleMapMaker() {
super();
}
/** @deprecated use {@link #setGMapApiKey} instead */
@Deprecated
public void setApiKey(String gMapApiKey) {
setGMapApiKey(gMapApiKey);
}
public void setEncodePolylines(boolean b) {
encodePolylines = b;
}
@Override
protected void printHTML(OutputStream out) throws IOException {
message("Writing HTML");
processTemplate(
"/net/sourceforge/gpstools/res/GoogleMap.html.template", out);
}
@Override
protected void printJS(OutputStream out) throws IOException {
message("Writing Google Maps Javascript");
Rectangle2D bounds = getBounds();
if (bounds == null) {
message("no trkpts or wpts");
return;
}
processTemplate("/net/sourceforge/gpstools/res/GoogleMap.js.template",
out);
}
private void wptsToGMarkers(Appendable mapjs) throws IOException {
GpsFormat format = GpsFormat.getInstance();
for (Wpt wpt : getGpx().getWpt()) {
String desc = wpt.getDesc();
desc = (desc == null) ? "" : desc.trim();
String name = wpt.getName();
name = (name == null) ? "Wpt" : name.trim();
int linkstart = desc.indexOf("http://");
if (linkstart >= 0) {
String link = desc.substring(linkstart);
link = link.replaceFirst("\\s.*", "");
mapjs.append("map.addOverlay(createUrlMarker(new GLatLng(");
mapjs.append(format.asLatLon(wpt.getLat(), wpt.getLon()));
mapjs.append("), {clickable: true, title: \"");
mapjs.append(name).append("\"}, \"").append(link)
.append("\"));");
} else {
mapjs.append("map.addOverlay(createMarker(new GLatLng(");
mapjs.append(format.asLatLon(wpt.getLat(), wpt.getLon()));
mapjs.append("), {clickable: false, title: \"");
mapjs.append(name).append("\"}));\n");
}
}
}
@Override
protected CharSequence draw(Trkpt[] pts, String color, String name) {
StringBuilder builder = new StringBuilder();
try {
if (encodePolylines) {
trksegToEncodedGPolyline(pts, color, builder);
} else {
trksegToUnencodedGPolyline(pts, color, builder);
}
} catch (IOException ex) {
throw new Error(ex);
// won't happen. StringBuilder throws no IOExceptions
}
return builder;
}
private static void trksegToUnencodedGPolyline(Trkpt[] pts, String color,
Appendable mapjs) throws IOException {
GpsFormat format = GpsFormat.getInstance();
mapjs.append("points = [];\n");
for (Trkpt pt : pts) {
mapjs.append("points.push(new GLatLng(");
mapjs.append(format.asLatLon(pt.getLat(), pt.getLon()));
mapjs.append("));\n");
}
mapjs.append("map.addOverlay(new GPolyline(points, \"");
mapjs.append(color).append("\", 2));\n");
}
private static void trksegToEncodedGPolyline(Trkpt[] pts, String color,
Appendable mapjs) throws IOException {
GpsFormat format = GpsFormat.getInstance();
final int len = pts.length;
int[] ilevels = new int[len];
mapjs.append("//new segment\n");
/*
* we encode at most 100 points simultaneously to avoid big rounding
* errors and overlong strings
*/
for (int start = 0, stop = 100; stop != len; start = stop - 1) {
mapjs.append("map.addOverlay(GPolyline.fromEncoded({\n");
mapjs.append(" color: \"").append(color).append("\",\n");
mapjs.append(" weight: 2,\n");
mapjs.append(" opacity: 0.5,\n");
/* encode first point */
mapjs.append(" points: \"");
Trkpt previous;
Trkpt current = pts[start];
format.asGPolylineCoordinate(current.getLat(), null, mapjs);
format.asGPolylineCoordinate(current.getLon(), null, mapjs);
stop = start + 100;
if (len <= stop) {
stop = len;
}
/* encode other points */
for (int i = start + 1; i < stop; i++) {
previous = current;
current = pts[i];
format.asGPolylineCoordinate(current.getLat(),
previous.getLat(), mapjs);
format.asGPolylineCoordinate(current.getLon(),
previous.getLon(), mapjs);
}
mapjs.append("\",\n");
/* calculate and encode levels */
mapjs.append(" levels: \"");
calculateLevels(pts, start, stop, ilevels);
for (int i = start; i < stop; i++) {
format.gEncodeUnsignedInt(ilevels[i], mapjs);
}
mapjs.append("\",\n");
mapjs.append(" zoomFactor: 2,\n");
mapjs.append(" numLevels: 18\n");
mapjs.append("}));\n");
start = stop - 1;
}
}
/*
* Java port of Javascript code written by Mark McClure in September 2006.
* http
* ://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/encodePolyline.js
*/
private static int NUM_LEVELS = 18;
private static int SMALL0 = 500;
private static int UNSET = -1;
private static int[] calculateLevels(Trkpt[] points, int start, int stop,
int[] levels) {
int i, j;
Arrays.fill(levels, start, stop, UNSET);
// Set the endpoints to show at all zoom levels
levels[start] = NUM_LEVELS - 1;
levels[stop - 1] = NUM_LEVELS - 1;
// Starting at the largest possible level, we step down through
// the levels labelling points which need to appear at
// lower resolutions.
for (int level = NUM_LEVELS - 1; level >= 0; level--) {
// Set "small" for the current level.
// More generally, we might use
// small = small0*Math.pow(2.0, -18*(NUM_LEVELS-level)/NUM_LEVELS);
double small = SMALL0 * Math.pow(2.0, level - 18);
// Figure out which points need to occur at this level,
// the main criteria being that we've stepped a certain
// distance (called "small") from the previous set point.
i = j = 1;
while (i < stop - 1) {
double dist = GpsMath.kmDistance(points[i].getLat(),
points[i].getLon(), points[j].getLat(),
points[j].getLon());
if (dist >= small && levels[i] == UNSET) {
levels[i] = level;
j = i;
}
if (levels[i] != UNSET) {
j = i;
}
i++;
}
}
// Set any unset points to level 0.
for (i = start; i < stop; i++) {
if (levels[i] == UNSET) {
levels[i] = 0;
}
}
return levels;
}
private void zoomMap(Appendable mapjs) throws IOException {
Rectangle2D.Double bd = getBounds();
float padding = getPadding();
GpsFormat format = GpsFormat.getInstance();
mapjs.append("map.setZoom(\n");
mapjs.append(" map.getBoundsZoomLevel(\n");
mapjs.append(" new GLatLngBounds(\n");
mapjs.append(" new GLatLng(");
mapjs.append(
format.asLatLon(bd.x - bd.width * padding, bd.y - bd.height
* padding)).append("),\n");
mapjs.append(" new GLatLng(");
mapjs.append(format.asLatLon(bd.x + bd.width * (1.0f + padding), bd.y
+ bd.height * (1.0f + padding)));
mapjs.append(")\n");
mapjs.append(" )\n");
mapjs.append(" )\n");
mapjs.append(");\n");
}
private void centerMap(Appendable mapjs) throws IOException {
Rectangle2D.Double bd = getBounds();
mapjs.append("map.setCenter(new GLatLng(");
mapjs.append(GpsFormat.getInstance().asLatLon(bd.x + 0.5 * bd.width,
bd.y + 0.5 * bd.height));
mapjs.append("), 13);\n");
}
private void loadPhotos(Appendable html) throws IOException {
if (getPhotoJavaScript() != null) {
html.append("addPhotoMarkers(myMap, '");
html.append(getPhotoURLPrefix());
html.append("', 200);");
}
}
@Override
protected void includePhotoJS(Appendable html) throws IOException {
if (photojs != null) {
includeGPSDingsJS(html);
}
super.includePhotoJS(html);
}
@Override
public void handleTemplateEvent(TemplateEvent e) throws IOException {
Appendable app = e.getDest();
String var = e.getVariable();
if ("cssURL".equals(var)) {
app.append(url.get(Output.CSS));
} else if ("jsURL".equals(var)) {
app.append(getURL(Output.MapJS));
} else if ("includeGPSDingsJS".equals(var)) {
includeGPSDingsJS(app);
} else if ("includePhotoJS".equals(var)) {
includePhotoJS(app);
} else if ("loadPhotos".equals(var)) {
loadPhotos(app);
} else if ("title".equals(var)) {
app.append(getHTMLTitle());
} else if ("gMapsApiKey".equals(var)) {
app.append(getGMapApiKey());
} else if ("boundsCenter".equals(var)) {
centerMap(app);
} else if ("trk".equals(var)) {
if (getIncludeTrks()) {
drawTracks(app);
}
} else if ("wpt".equals(var)) {
if (getIncludeWpts()) {
wptsToGMarkers(app);
}
} else if ("boundsZoom".equals(var)) {
zoomMap(app);
} else {
message("Skipping unknown variable " + var);
}
}
public static void main(String[] argv) {
(new GMapCommandLine(argv)).execute();
}
private static class GMapCommandLine extends MapCommandLine<GoogleMapMaker> {
public final static String OPT_ENCODE = "encode-polylines";
public GMapCommandLine(String[] argv) {
super(new GoogleMapMaker(), argv);
}
@Override
public void printHelp(PrintStream out) {
try {
super.printHelp(out);
cat("googlemap.txt");
} catch (Exception ex) {
throw new Error(ex);
}
}
@Override
protected Options createOptions() {
Options options = super.createOptions();
options.addOption(makeOption(OPT_TITLE, String.class, 1, "title"));
options.addOption(makeOption(OPT_ENCODE, Boolean.class, 0, null));
return options;
}
@Override
protected CommandLine processOptions(Options options)
throws org.apache.commons.cli.ParseException, IOException,
java.text.ParseException {
CommandLine cl = super.processOptions(options);
char c;
/* apiKey option */
opt = OPT_API_KEY;
c = opt.charAt(0);
if (cl.hasOption(c)) {
app.setApiKey(cl.getOptionValue(c));
}
/* output options */
opt = OPT_HTML;
c = opt.charAt(0);
if (cl.hasOption(c)) {
if (!cl.hasOption(OPT_API_KEY.charAt(0))) {
throw new MissingOptionException(
"HTML output requires that a google maps api key "
+ "is specified with the -"
+ OPT_API_KEY.charAt(0) + ",--"
+ OPT_API_KEY + " option.");
}
app.enableOutput(Output.HTML, cl.getOptionValue(c));
}
if (cl.hasOption('c') || cl.hasOption('C')) {
System.err
.println("WARNING: The css and CSS options no longer have any effect");
}
/* encode option */
opt = OPT_ENCODE;
c = opt.charAt(0);
app.setEncodePolylines(cl.hasOption(c));
return cl;
}
}
}