/** * This file is part of OSM2ShareNav * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as published by * the Free Software Foundation. * * Copyright (C) 2007 Harald Mueller * Copyright (C) 2008 Kai Krueger */ package net.sharenav.osmToShareNav; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.ProcessBuilder; import java.util.Calendar; import java.util.Enumeration; import java.util.Hashtable; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import javax.swing.JFrame; import javax.swing.JOptionPane; import net.sharenav.osmToShareNav.area.Area; import net.sharenav.osmToShareNav.area.SeaGenerator; import net.sharenav.osmToShareNav.area.SeaGenerator2; import net.sharenav.osmToShareNav.model.Damage; import net.sharenav.osmToShareNav.model.Relation; import net.sharenav.osmToShareNav.model.RouteAccessRestriction; import net.sharenav.osmToShareNav.model.TollRule; import net.sharenav.osmToShareNav.model.TravelMode; import net.sharenav.osmToShareNav.model.TravelModes; /** * This is the main class of Osm2ShareNav. * It triggers all the steps necessary to create a ShareNav JAR file * ready for downloading to the mobile phone. */ public class BundleShareNav implements Runnable { static boolean compressed = true; static Calendar startTime; static Configuration config; static String dontCompress[] = null; private static volatile boolean createSuccessfully; /** * @param args */ public static void main(String[] args) { long maxMem = Runtime.getRuntime().maxMemory() / (1024 * 1024); String dataModel = System.getProperty("sun.arch.data.model"); String warning = null; if ( (maxMem < 800 && dataModel.equals("32")) || (maxMem < 1500 && dataModel.equals("64")) ) { warning = "Heap space might be not set or set too low! (available memory of " + dataModel + " bit system is " + maxMem + "MB)\r\n" + " Use command line options to avoid out-of-memory errors during map making.\r\n" + " On 32 bit systems start Osm2ShareNav e.g. with:\r\n" + " java -Xmx1024M -jar Osm2ShareNav-xxxx.jar\r\n" + " to increase the heap space to 1024 MB\r\n" + " On 64 bit systems use e.g.:\r\n" + " java -Xmx4096M -XX:+UseCompressedOops -jar Osm2ShareNav-xxxx.jar\r\n" + " for 4096 MB heap space and an option to reduce memory requirements\r\n"; } BundleShareNav bgm = new BundleShareNav(); GuiConfigWizard gcw = null; Configuration c; if (args.length == 0 || (args.length == 1 && args[0].startsWith("--properties="))) { if (warning != null) { JFrame frame = new JFrame("Alert"); JOptionPane.showMessageDialog(frame, warning, "Osm2ShareNav", JOptionPane.WARNING_MESSAGE); } gcw = new GuiConfigWizard(); c = gcw.startWizard(args); } else { if (warning != null) { System.out.println("Available memory: " + maxMem + "MB (" + dataModel + " bit system)"); System.out.println("WARNING:"); System.out.println(warning); } c = new Configuration(args); if (c.verbose >= 0 && warning == null) { System.out.println("Available memory: " + maxMem + "MB (" + dataModel + " bit system)"); } } /** * Decouple the computational thread from * the GUI thread to make the GUI more smooth * Not sure if this is actually necessary, but * it shouldn't harm either. */ if (c.getDontCompress().equals("*")) { compressed = false; } else { dontCompress = c.getDontCompress().split("[;,]"); if (dontCompress[0].equals("")) { dontCompress = null; } } config = c; Thread t = new Thread(bgm); createSuccessfully = false; t.start(); try { t.join(); } catch (InterruptedException e) { // Nothing to do } if (gcw != null) { if (createSuccessfully) { JOptionPane.showMessageDialog(gcw, "A ShareNav bundle was successfully created and can now be copied to device or run."); } else { JOptionPane.showMessageDialog(gcw, "A fatal error occured during processing. Please have a look at the output logs."); } gcw.reenableClose(); } } private static void expand(Configuration c, String tmpDir) throws ZipException, IOException { if (c.verbose >= 0) { System.out.println("Preparing " + c.getBaseBundleFileName()); } InputStream appStream = c.getBaseBundleFile(); if (appStream == null) { System.out.println("ERROR: Couldn't find the jar file for " + c.getBaseBundleFileName()); System.out.println("Check the app parameter in the properties file for misspellings"); System.exit(1); } File file = new File(c.getTempBaseDir() + "/" + c.getBaseBundleFileName()); writeFile(appStream, file.getAbsolutePath()); ZipFile zf = new ZipFile(file.getCanonicalFile()); for (Enumeration<? extends ZipEntry> e = zf.entries(); e.hasMoreElements();) { ZipEntry ze = e.nextElement(); if (ze.isDirectory()) { // System.out.println("dir " + ze.getName()); } else { // System.out.println("file " + ze.getName()); InputStream stream = zf.getInputStream(ze); writeFile(stream, tmpDir + "/" + ze.getName()); } } } /** * Rename the Copying files to .txt suffix for easy access on all OS's * */ private static void renameCopying(Configuration c) { String tmpDir = c.getTempDir(); File copying = new File(tmpDir + "/COPYING"); File copying2 = new File(tmpDir + "/COPYING.txt"); File copyingosm = new File(tmpDir + "/COPYING-OSM"); File copyingosm2 = new File(tmpDir + "/COPYING-OSM.txt"); File copyingmaps = new File(tmpDir + "/COPYING-MAPS"); File copyingmaps2 = new File(tmpDir + "/COPYING-MAPS.txt"); copying.renameTo(copying2); copyingosm.renameTo(copyingosm2); copyingmaps.renameTo(copyingmaps2); } /** * Rewrite or remove the Manifest file to change the bundle name to reflect the one * specified in the properties file. * * @param c */ private static void rewriteManifestFile(Configuration c, boolean rename) { String tmpDir = c.getTempDir(); try { File manifest = new File(tmpDir + "/META-INF/MANIFEST.MF"); File manifest2 = new File(tmpDir + "/META-INF/MANIFEST.tmp"); FileWriter fw = null; if (rename) { BufferedReader fr = new BufferedReader(new FileReader(manifest)); fw = new FileWriter(manifest2); String line; Pattern p1 = Pattern.compile("MIDlet-(\\d):\\s(.*),(.*),(.*)"); while (true) { line = fr.readLine(); if (line == null) { break; } Matcher m1 = p1.matcher(line); if (m1.matches()) { fw.write("MIDlet-" + m1.group(1) + ": " + c.getBundleName() + "," + m1.group(3) + "," + m1.group(4) + "\n"); } else if (line.startsWith("MIDlet-Name: ")) { fw.write("MIDlet-Name: " + c.getBundleName() + "\n"); } else { fw.write(line + "\n"); } if (line.startsWith("MicroEdition-Profile: ") ) { if (config.getAddToManifest().length() != 0) { fw.write(config.getAddToManifest() + "\n"); } } } fr.close(); } manifest.delete(); if (rename) { fw.close(); manifest2.renameTo(manifest); } } catch (IOException ioe) { System.out.println("Something went wrong rewriting the manifest file"); return; } } private static void writeJADfile(Configuration c, long jarLength) throws IOException { String tmpDir = c.getTempDir(); File manifest = new File(tmpDir + "/META-INF/MANIFEST.MF"); BufferedReader fr = new BufferedReader(new FileReader(manifest)); File jad = new File(c.getBundleFileName() + ".jad"); FileWriter fw = new FileWriter(jad); /** * Copy over the information from the manifest file, to the jad file. * This way we use the information generated by the build process * of ShareNav, to duplicate as little data as possible. */ try { String line = fr.readLine();; while (true) { if (line == null) { break; } String nextline = fr.readLine(); if (nextline != null && nextline.substring(0, 1).equals(" ")) { line += nextline.substring(1); continue; } if (line.startsWith("MIDlet") || line.startsWith("MicroEdition") || (config.getAddToManifest().length() != 0 && line.startsWith(config.getAddToManifest())) ) { fw.write(line + "\n"); } line = nextline; } } catch (IOException ioe) { //This will probably be the end of the file } /** * Add some additional fields to the jad file, that aren't present in the manifest file. */ fw.write("MIDlet-Jar-Size: " + jarLength + "\n"); fw.write("MIDlet-Jar-URL: " + c.getBundleFileName() + ".jar\n"); fw.close(); fr.close(); } private static void pack(Configuration c) throws ZipException, IOException { File n = null; if (config.getMapName().equals("") || !config.mapzip) { n = new File(c.getBundleFileName() + (config.sourceIsApk ? ".apk": ".jar")); rewriteManifestFile(c, true); } else { n = new File(c.getMapFileName()); rewriteManifestFile(c, false); renameCopying(c); } BufferedOutputStream fo = new BufferedOutputStream(new FileOutputStream(n)); ZipOutputStream zf = new ZipOutputStream(fo); zf.setLevel(9); if (compressed == false) { zf.setMethod(ZipOutputStream.STORED); } File src = new File(c.getTempDir()); if (src.isDirectory() == false) { throw new Error("TempDir is not a directory"); } packDir(zf, src, ""); String bundleName = n.getAbsolutePath(); String jarSigner = config.getJarsignerPath(); // resolve Windows environment variables case-insensitive java.util.Map<String, String> env = System.getenv(); for (String envName : env.keySet()) { jarSigner = jarSigner.replaceAll("(?i)%" + envName + "%", Matcher.quoteReplacement(env.get(envName))); } zf.close(); if (config.sourceIsApk && config.signApk && !config.mapzip) { Process signer = null; // FIXME add "-storepass" handling with a password field on GUI String command[] = { jarSigner, "-verbose", "-verbose", "-verbose", "-digestalg", "SHA1", "-sigalg", "MD5withRSA", bundleName, "sharenav" }; String passString = config.getSignApkPassword(); if (! passString.equals("")) { command[2] = "-storepass"; command[3] = passString; } try { String jarsignerOutputLine = null; System.out.println("Signing with external program " + command[0] + " (set jarsignerPath=<jarsigner-path-or-commandname> in .properties to change)"); ProcessBuilder pBuilder = new ProcessBuilder(command); pBuilder.redirectErrorStream(true); signer = pBuilder.start(); //signer = Runtime.getRuntime().exec(command); // Runtime.getRuntime().exec(jarSigner + " -verbose -digestalg SHA1 -sigalg MD5withRSA " + bundleName + " sharenav"); //DataInputStream jarsignerOutputDataStream = new InputStream(signer.getInputStream()); BufferedReader jarsignerOutput = new BufferedReader(new InputStreamReader(signer.getInputStream())); // if jarsigner asks for a password, this makes it stop // asking and show the query/error output signer.getOutputStream().flush(); signer.getOutputStream().close(); while ((jarsignerOutputLine = jarsignerOutput.readLine()) != null) { System.out.println(jarsignerOutputLine); } } catch (IOException ioe) { System.out.println("Error: IO exception " + ioe); showSigningMessage(bundleName); } if (signer != null) { try { signer.waitFor(); int exitStatus = signer.exitValue(); if (exitStatus != 0) { System.out.println("ERROR: jarsigner exited with exit status " + exitStatus + ", signing failed"); showSigningMessage(bundleName); } } catch (InterruptedException ie) { System.out.println("Error: interrupted execution " + ie); showSigningMessage(bundleName); } } } // System.out.println("Bundlename: " + bundleName + " jarSigner: " + jarSigner); if (config.getMapName().equals("") && !config.sourceIsApk) { writeJADfile(c, n.length()); } Calendar endTime = Calendar.getInstance(); if (config.verbose >= 0) { System.out.println(n.getName() + " created successfully with " + (n.length() / 1024 / 1024) + " MiB in " + getDuration(endTime.getTimeInMillis() - startTime.getTimeInMillis())); } } public static void showSigningMessage(String bundleName) { System.out.println("Error: Wasn't able to sign " + bundleName); System.out.println("You may need to set up jarsigner path and/or settings for signing, see the ShareNav Wiki Properties page under \"Signing an apk\""); System.out.println("You may also need to install the java development environment"); } private static String getDuration(long duration) { final int millisPerSecond = 1000; final int millisPerMinute = 1000*60; final int millisPerHour = 1000*60*60; final int millisPerDay = 1000*60*60*24; int days = (int) (duration / millisPerDay); int hours = (int) (duration % millisPerDay / millisPerHour); int minutes = (int) (duration % millisPerHour / millisPerMinute); int seconds = (int) (duration % millisPerMinute / millisPerSecond); return String.format("%d %02d:%02d:%02d", days, hours, minutes, seconds); } private static void packDir(ZipOutputStream os, File d, String path) throws IOException { File[] files = d.listFiles(); if (config.sourceIsApk) { for (int i = 0; i < files.length; i++) { if (files[i].isDirectory() && files[i].getName().equals("META-INF") && path.length() == 0) { // put META-INF first, not sure if it matters but is customary File tmp = files[0]; files[0] = files[i]; files[i] = tmp; } } } for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { if (path.length() > 0) { packDir(os, files[i], path + "/" + files[i].getName()); } else { packDir(os, files[i], files[i].getName()); } } else { // System.out.println(); ZipEntry ze = null; if (path.length() > 0) { ze = new ZipEntry(path + "/" + files[i].getName()); } else { ze = new ZipEntry(files[i].getName()); } int ch; int count = 0; boolean storethis = false; if (compressed && dontCompress != null) { for (String extension : dontCompress) { if (files[i].getName().toLowerCase().endsWith(extension)) { ze.setMethod(ZipOutputStream.STORED); storethis = true; } } } //byte buffer to read in larger chunks byte[] bb = new byte[4096]; FileInputStream stream = new FileInputStream(files[i]); if ((!compressed) || storethis) { CRC32 crc = new CRC32(); count = 0; while ((ch = stream.read(bb)) != -1) { crc.update(bb, 0, ch); } ze.setCrc(crc.getValue()); ze.setSize(files[i].length()); } // ze. os.putNextEntry(ze); count = 0; stream.close(); stream = new FileInputStream(files[i]); while ((ch = stream.read(bb)) != -1) { os.write(bb, 0, ch); count += ch; } stream.close(); // System.out.println("Wrote " + path + "/" + files[i].getName() + " byte:" + count); } } } /** * @param stream * @param string */ private static void writeFile(InputStream stream, String name) { File f = new File(name); try { if (! f.canWrite()) { createPath(f.getParentFile()); } FileOutputStream fo = new FileOutputStream(name); int ch; int count = 0; byte[] bb = new byte[4096]; while ((ch = stream.read(bb)) != -1) { fo.write(bb, 0, ch); count += ch; } fo.close(); // System.out.println("Wrote " + name + " byte:" + count); } catch (Exception e) { e.printStackTrace(); throw new Error("Failed to write " + name + " err:" + e.getMessage()); } } /** * Ensures that the path denoted with <code>f</code> will exist * on the file-system. * @param f File whose directory must exist */ private static void createPath(File f) { if (! f.canWrite()) { createPath(f.getParentFile()); } f.mkdir(); } /** * remove a directory and all its subdirectories and files * @param path * @return */ static private boolean deleteDirectory(File path) { if (path.exists()) { File[] files = path.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteDirectory(files[i]); } else { files[i].delete(); } } } return( path.delete() ); } static private void validateConfig(Configuration config) { if (config == null) { System.out.println("ERROR: can't find config, exiting"); System.exit(1); } if ((config.enableEditingSupport) && (!(config.getAppParam().contains("editing")) && !(config.getAppParam().contains("full-connected")))) { System.out.println("ERROR: You are creating a map with editing support, but use a app version that does not support editing\n" + " please fix your .properties file (full-connected app versions have editing support)"); System.exit(1); } if ((config.getPlanetName() == null || config.getPlanetName().equals(""))) { System.out.println("ERROR: You haven't specified a planet file\n" + " please fix your .properties file"); System.exit(1); } } /* (non-Javadoc) * @see java.lang.Runnable#run() */ @Override public void run() { InputStream fr; try { validateConfig(config); if (config.verbose >= 0) { System.out.println(config.toString()); } // the legend must be parsed after the configuration to apply parameters to the travel modes specified in useRouting TravelModes.stringToTravelModes(config.useRouting); config.parseLegend(); // Maybe some of these should be configurable in the future. SeaGenerator.setOptions(config, false, false, false, true, 100); startTime = Calendar.getInstance(); TravelMode tm = null; if (Configuration.attrToBoolean(config.useRouting) >= 0) { for (int i = 0; i < TravelModes.travelModeCount; i++) { tm = TravelModes.travelModes[i]; if (config.verbose >= 0) { System.out.println("Route and toll rules in " + config.getStyleFileName() + " for " + tm.getName() + ":"); if ( (tm.travelModeFlags & TravelMode.AGAINST_ALL_ONEWAYS) > 0) { System.out.println(" Going against all accessible oneways is allowed"); } if ( (tm.travelModeFlags & TravelMode.BICYLE_OPPOSITE_EXCEPTIONS) > 0) { System.out.println(" Opposite direction exceptions for bicycles get applied"); } } int routeAccessRestrictionCount = 0; if (TravelModes.getTravelMode(i).getRouteAccessRestrictions().size() > 0) { for (RouteAccessRestriction r: tm.getRouteAccessRestrictions()) { routeAccessRestrictionCount++; if (config.verbose >= 0) { System.out.println(" " + r.toString()); } } } if (routeAccessRestrictionCount == 0) { System.out.println("Warning: No access restrictions in " + config.getStyleFileName() + " for " + tm.getName()); } int tollRuleCount = 0; if (TravelModes.getTravelMode(i).getTollRules().size() > 0) { for (TollRule r: tm.getTollRules()) { tollRuleCount++; if (config.verbose >= 0) { System.out.println(" " + r.toString()); } } } if (tollRuleCount == 0) { System.out.println("Warning: No toll rules in " + config.getStyleFileName() + " for " + tm.getName()); } } System.out.println(""); } if (LegendParser.getDamages().size() == 0) { System.out.println("No damage markers in " + config.getStyleFileName()); } else { if (config.verbose >= 0) { System.out.println("Rules specified in " + config.getStyleFileName() + " for marking damages:"); for (Damage damage: LegendParser.getDamages()) { System.out.println(" Ways/Areas with key " + damage.key + "=" + damage.values); } } } String tmpDir = config.getTempDir(); if (config.verbose >= 0) { System.out.println("Unpacking application to " + tmpDir); } expand(config, tmpDir); File target = new File(tmpDir); createPath(target); if (config.sourceIsApk) { // create /assets File targetAssets = new File(tmpDir + "/assets"); createPath(targetAssets); // unsign the APK by deleting files from META-INF (e.g. from debug signing) to be able to sign the APK with the private key File f = new File(tmpDir + "/META-INF/MANIFEST.MF"); f.delete(); f = new File(tmpDir + "/META-INF/CERT.SF"); f.delete(); f = new File(tmpDir + "/META-INF/CERT.RSA"); f.delete(); if (config.mapzip) { f = new File(tmpDir + "/META-INF/SHARENAV.SF"); f.delete(); f = new File(tmpDir + "/META-INF/SHARENAV.RSA"); f.delete(); } } OsmParser parser = config.getPlanetParser(); SeaGenerator2 sg2 = new SeaGenerator2(); long startTime = System.currentTimeMillis(); long time; SeaGenerator2.setOptions(config, true, true, true, true, 100); if (config.getGenerateSea()) { if (config.verbose >= 0) { System.out.println("Starting SeaGenerator"); } sg2.generateSea(parser); time = (System.currentTimeMillis() - startTime); if (config.verbose >= 0) { System.out.println("SeaGenerator run"); System.out.println(" Time taken: " + time / 1000 + " seconds"); } } if (config.verbose >= 0) { System.out.println("Starting relation handling"); } startTime = System.currentTimeMillis(); Area.setParser(parser); new Relations(parser, config); if (config.verbose >= 0) { System.out.println("Relations processed"); time = (System.currentTimeMillis() - startTime); System.out.println(" Time taken: " + time / 1000 + " seconds"); } /** * Display some stats about the type of relations we currently aren't handling * to see which ones would be particularly useful to deal with eventually */ Hashtable<String, Integer> relTypes = new Hashtable<String, Integer>(); for (Relation r : parser.getRelations()) { String type = r.getAttribute("type"); if (type == null) { type = "unknown"; } Integer count = relTypes.get(type); if (count != null) { count = new Integer(count.intValue() + 1); } else { count = new Integer(1); } relTypes.put(type, count); } if (config.verbose >= 0) { System.out.println("Types of relations present but ignored: "); for (Entry<String, Integer> e : relTypes.entrySet()) { System.out.println(" " + e.getKey() + ": " + e.getValue()); } } relTypes = null; if (config.verbose >= 0) { System.out.println("Splitting long ways"); } int numWays = parser.getWays().size(); startTime = System.currentTimeMillis(); new SplitLongWays(parser); time = (System.currentTimeMillis() - startTime); if (config.verbose >= 0) { System.out.println("Splitting long ways increased ways from " + numWays + " to " + parser.getWays().size()); System.out.println(" Time taken: " + time / 1000 + " seconds"); } OxParser.printMemoryUsage(1); RouteData rd = null; if (Configuration.attrToBoolean(config.useRouting) >= 0 ) { rd = new RouteData(parser, target.getCanonicalPath()); if (config.verbose >= 0) { System.out.println("Remembering " + parser.trafficSignalCount + " traffic signal nodes"); } rd.rememberDelayingNodes(); } if (config.verbose >= 0) { System.out.println("Removing unused nodes"); } new CleanUpData(parser, config); if (Configuration.attrToBoolean(config.useRouting) >= 0 ) { if (config.verbose >= 0) { System.out.println("Creating route data"); System.out.println("==================="); } rd.create(config); rd.optimise(); OsmParser.printMemoryUsage(1); } CreateShareNavData cd = new CreateShareNavData(parser, target.getCanonicalPath()); // rd.write(target.getCanonicalPath()); // cd.setRouteData(rd); cd.setConfiguration(config); new CalcNearBy(parser); cd.exportMapToMid(); //Drop parser to conserve Memory parser = null; cd = null; rd = null; if (!config.getCellOperator().equalsIgnoreCase("false")) { CellDB cellDB = new CellDB(); cellDB.parseCellDB(); } pack(config); //Cleanup after us again. The .jar and .jad file are in the main directory, //so these won't get deleted if (config.cleanupTmpDirAfterUse()) { File tmpBaseDir = new File(config.getTempBaseDir()); if (config.verbose >= 0) { System.out.println("Cleaning up temporary directory " + tmpBaseDir); } deleteDirectory(tmpBaseDir); } createSuccessfully = true; } catch (Exception e) { e.printStackTrace(); } } }