From dfa586ac2b3d576956ef12444f991c53adf52584 Mon Sep 17 00:00:00 2001 From: Cyberes Date: Wed, 3 Feb 2021 16:48:46 -0700 Subject: [PATCH] c --- CHANGES.md | 4 + LICENSE | 251 ---- Mobile Atlas Creator.exe | Bin 0 -> 40960 bytes build.xml | 374 ++++++ gpl.txt | 339 +++++ misc/launch4j.xml | 35 + misc/mobac.ico | Bin 0 -> 13806 bytes settings.xml | 116 ++ src/main/java/mobac/Main.java | 110 ++ src/main/java/mobac/StartMOBAC.java | 88 ++ src/main/java/mobac/data/gpx/GPXUtils.java | 123 ++ .../java/mobac/data/gpx/gpx11/BoundsType.java | 150 +++ .../mobac/data/gpx/gpx11/CopyrightType.java | 147 +++ .../java/mobac/data/gpx/gpx11/EmailType.java | 112 ++ .../mobac/data/gpx/gpx11/ExtensionsType.java | 98 ++ src/main/java/mobac/data/gpx/gpx11/Gpx.java | 272 ++++ .../java/mobac/data/gpx/gpx11/LinkType.java | 144 +++ .../mobac/data/gpx/gpx11/MetadataType.java | 314 +++++ .../java/mobac/data/gpx/gpx11/PersonType.java | 141 +++ .../java/mobac/data/gpx/gpx11/PtType.java | 161 +++ .../java/mobac/data/gpx/gpx11/PtsegType.java | 94 ++ .../java/mobac/data/gpx/gpx11/RteType.java | 318 +++++ .../java/mobac/data/gpx/gpx11/TrkType.java | 318 +++++ .../java/mobac/data/gpx/gpx11/TrksegType.java | 121 ++ .../java/mobac/data/gpx/gpx11/WptType.java | 566 +++++++++ .../mobac/data/gpx/gpx11/package-info.java | 34 + .../mobac/data/gpx/interfaces/GpxPoint.java | 28 + .../mobac/exceptions/AtlasTestException.java | 45 + .../exceptions/DownloadFailedException.java | 43 + .../exceptions/InvalidNameException.java | 27 + .../exceptions/MOBACOutOfMemoryException.java | 47 + .../exceptions/MapCreationException.java | 68 + .../MapDownloadSkippedException.java | 33 + .../exceptions/MapSourceCreateException.java | 29 + .../MapSourceInitializationException.java | 36 + .../exceptions/MapSourcesUpdateException.java | 39 + .../exceptions/StopAllDownloadsException.java | 29 + .../java/mobac/exceptions/TileException.java | 29 + .../mobac/exceptions/TileStoreException.java | 35 + .../UnrecoverableDownloadException.java | 49 + .../exceptions/UpdateFailedException.java | 29 + .../mobac/externaltools/ExternalToolDef.java | 144 +++ .../externaltools/ExternalToolsLoader.java | 116 ++ .../mobac/externaltools/ToolParameters.java | 26 + src/main/java/mobac/gui/AtlasProgress.java | 751 +++++++++++ src/main/java/mobac/gui/MainGUI.java | 1128 +++++++++++++++++ src/main/java/mobac/gui/SplashFrame.java | 65 + .../actions/AddGpxTrackAreaPolygonMap.java | 164 +++ .../gui/actions/AddGpxTrackPolygonMap.java | 226 ++++ .../java/mobac/gui/actions/AddMapLayer.java | 40 + .../mobac/gui/actions/AddPolygonMapLayer.java | 102 ++ .../gui/actions/AddRectangleMapAutocut.java | 94 ++ .../java/mobac/gui/actions/AtlasConvert.java | 74 ++ .../java/mobac/gui/actions/AtlasCreate.java | 55 + src/main/java/mobac/gui/actions/AtlasNew.java | 80 ++ .../java/mobac/gui/actions/BookmarkAdd.java | 48 + .../mobac/gui/actions/BookmarkManage.java | 34 + .../mobac/gui/actions/DebugSetLogLevel.java | 43 + .../mobac/gui/actions/DebugShowLogFile.java | 62 + .../gui/actions/DebugShowMapSourceNames.java | 130 ++ .../gui/actions/DebugShowMapTileGrid.java | 38 + .../mobac/gui/actions/DebugShowReport.java | 31 + .../java/mobac/gui/actions/GpxAddPoint.java | 70 + src/main/java/mobac/gui/actions/GpxClear.java | 53 + .../java/mobac/gui/actions/GpxEditor.java | 131 ++ .../mobac/gui/actions/GpxElementListener.java | 169 +++ src/main/java/mobac/gui/actions/GpxLoad.java | 173 +++ src/main/java/mobac/gui/actions/GpxNew.java | 52 + src/main/java/mobac/gui/actions/GpxSave.java | 107 ++ .../java/mobac/gui/actions/HelpLicenses.java | 30 + .../mobac/gui/actions/OpenInWebbrowser.java | 71 ++ .../java/mobac/gui/actions/PanelShowHide.java | 38 + .../gui/actions/RefreshCustomMapsources.java | 57 + .../gui/actions/SelectionModeCircle.java | 34 + .../gui/actions/SelectionModePolygon.java | 34 + .../gui/actions/SelectionModeRectangle.java | 33 + .../mobac/gui/actions/ShowAboutDialog.java | 30 + .../mobac/gui/actions/ShowHelpAction.java | 29 + .../java/mobac/gui/actions/ShowReadme.java | 50 + .../gui/atlastree/DragDropController.java | 217 ++++ .../java/mobac/gui/atlastree/JAtlasTree.java | 412 ++++++ .../mobac/gui/atlastree/MouseController.java | 55 + .../java/mobac/gui/atlastree/NodeEditor.java | 55 + .../mobac/gui/atlastree/NodeRenderer.java | 79 ++ .../gui/atlastree/NodeTransferWrapper.java | 54 + .../gui/components/FilledLayeredPane.java | 45 + .../mobac/gui/components/JAtlasNameField.java | 36 + .../gui/components/JBookmarkMenuItem.java | 42 + .../gui/components/JCollapsiblePanel.java | 343 +++++ .../gui/components/JCoordinateField.java | 185 +++ .../gui/components/JDirectoryChooser.java | 58 + .../mobac/gui/components/JDistanceSlider.java | 56 + .../mobac/gui/components/JDropDownButton.java | 73 ++ .../java/mobac/gui/components/JIntCombo.java | 106 ++ .../java/mobac/gui/components/JIntField.java | 135 ++ .../mobac/gui/components/JMapSizeCombo.java | 68 + .../java/mobac/gui/components/JMenuItem2.java | 52 + .../mobac/gui/components/JObjectCheckBox.java | 72 ++ .../gui/components/JProfilesComboBox.java | 76 ++ .../mobac/gui/components/JRegexTextField.java | 71 ++ .../mobac/gui/components/JTileSizeCombo.java | 67 + .../mobac/gui/components/JTimeSlider.java | 81 ++ .../mobac/gui/components/JZoomCheckBox.java | 40 + .../java/mobac/gui/dialogs/AboutDialog.java | 109 ++ .../java/mobac/gui/dialogs/FontChooser.java | 182 +++ src/main/java/mobac/gui/dialogs/Help.java | 111 ++ .../mobac/gui/dialogs/LicensesDialog.java | 135 ++ .../mobac/gui/dialogs/ManageBookmarks.java | 113 ++ .../mobac/gui/dialogs/MessageDialogs.java | 45 + .../gui/dialogs/WorkinprogressDialog.java | 174 +++ src/main/java/mobac/gui/gpxtree/GpxEntry.java | 65 + .../java/mobac/gui/gpxtree/GpxRootEntry.java | 62 + .../mobac/gui/gpxtree/GpxTreeListener.java | 100 ++ src/main/java/mobac/gui/gpxtree/RteEntry.java | 53 + src/main/java/mobac/gui/gpxtree/TrkEntry.java | 49 + .../java/mobac/gui/gpxtree/TrksegEntry.java | 41 + src/main/java/mobac/gui/gpxtree/WptEntry.java | 48 + .../gui/listeners/AtlasModelListener.java | 58 + src/main/java/mobac/gui/mapview/GridZoom.java | 51 + .../java/mobac/gui/mapview/JMapViewer.java | 559 ++++++++ .../java/mobac/gui/mapview/JobDispatcher.java | 90 ++ .../mobac/gui/mapview/MemoryTileCache.java | 284 +++++ .../java/mobac/gui/mapview/PreviewMap.java | 505 ++++++++ src/main/java/mobac/gui/mapview/ScaleBar.java | 95 ++ src/main/java/mobac/gui/mapview/Tile.java | 266 ++++ .../java/mobac/gui/mapview/TileLoader.java | 141 +++ src/main/java/mobac/gui/mapview/WgsGrid.java | 284 +++++ ...AbstractPolygonSelectionMapController.java | 67 + .../controller/DefaultMapController.java | 187 +++ .../mapview/controller/GpxMapController.java | 113 ++ .../mapview/controller/JMapController.java | 83 ++ .../controller/MapKeyboardController.java | 314 +++++ .../PolygonCircleSelectionMapController.java | 83 ++ .../PolygonSelectionMapController.java | 63 + .../RectangleSelectionMapController.java | 128 ++ .../mapview/interfaces/MapEventListener.java | 43 + .../gui/mapview/interfaces/MapLayer.java | 45 + .../gui/mapview/interfaces/MapTileLayer.java | 40 + .../interfaces/TileLoaderListener.java | 35 + .../mapview/layer/DefaultMapTileLayer.java | 79 ++ .../mobac/gui/mapview/layer/GpxLayer.java | 172 +++ .../layer/MapAreaHighlightingLayer.java | 189 +++ .../mobac/gui/mapview/layer/MapGridLayer.java | 40 + .../mapview/layer/OverlayMapTileLayer.java | 72 ++ .../mapview/layer/PolygonSelectionLayer.java | 68 + .../layer/RectangleSelectionLayer.java | 64 + .../mobac/gui/mapview/layer/ShapeLayer.java | 55 + .../mapview/layer/TileStoreCoverageLayer.java | 129 ++ .../mobac/gui/panels/JCoordinatesPanel.java | 219 ++++ src/main/java/mobac/gui/panels/JGpxPanel.java | 267 ++++ .../java/mobac/gui/panels/JProfilesPanel.java | 189 +++ .../gui/panels/JTileImageParametersPanel.java | 282 +++++ .../gui/panels/JTileStoreCoveragePanel.java | 144 +++ .../java/mobac/gui/settings/SettingsGUI.java | 970 ++++++++++++++ .../mobac/gui/settings/SettingsGUIPaper.java | 478 +++++++ .../gui/settings/SettingsGUITileStore.java | 261 ++++ .../gui/settings/SettingsGUIWgsGrid.java | 151 +++ .../mapsources/AbstractHttpMapSource.java | 221 ++++ .../AbstractMultiLayerMapSource.java | 197 +++ .../mapsources/DefaultMapSourcesManager.java | 234 ++++ .../java/mobac/mapsources/MapSourceTools.java | 96 ++ .../mobac/mapsources/MapSourceUrlUpdater.java | 138 ++ .../mobac/mapsources/MapSourcesManager.java | 59 + .../MapSourcesPropertiesManager.java | 82 ++ .../custom/BeanShellHttpMapSource.java | 196 +++ .../mapsources/custom/CustomCloudMade.java | 63 + .../custom/CustomLocalImageFileMapSource.java | 276 ++++ .../custom/CustomLocalTileFilesMapSource.java | 300 +++++ .../CustomLocalTileSQliteMapSource.java | 320 +++++ .../custom/CustomLocalTileZipMapSource.java | 316 +++++ .../mapsources/custom/CustomMapSource.java | 223 ++++ .../custom/CustomMapSourceType.java | 21 + .../custom/CustomMultiLayerMapSource.java | 137 ++ .../mapsources/custom/CustomWmsMapSource.java | 77 ++ .../custom/StandardMapSourceLayer.java | 114 ++ .../mobac/mapsources/custom/package-info.java | 12 + .../mapsources/impl/DebugLocalMapSource.java | 42 + .../mobac/mapsources/impl/DebugMapSource.java | 124 ++ .../impl/DebugRandomLocalMapSource.java | 137 ++ .../impl/DebugTransparentLocalMapSource.java | 137 ++ .../mapsources/impl/LocalhostTestSource.java | 39 + .../mapsources/impl/SimpleMapSource.java | 101 ++ .../loader/BeanShellMapSourceLoader.java | 44 + .../loader/CustomMapSourceLoader.java | 126 ++ .../loader/EclipseMapPackLoader.java | 79 ++ .../mapsources/loader/MapPackClassLoader.java | 50 + .../mapsources/loader/MapPackManager.java | 481 +++++++ .../openstreetmap/AbstractOsmMapSource.java | 53 + .../mappacks/openstreetmap/CloudMade.java | 78 ++ .../mappacks/openstreetmap/Hikebikemap.java | 84 ++ .../mappacks/openstreetmap/MapQuest.java | 68 + .../mappacks/openstreetmap/Mapnik.java | 65 + .../mappacks/openstreetmap/OpenPisteMap.java | 135 ++ .../mappacks/openstreetmap/OpenSeaMap.java | 121 ++ .../openstreetmap/OsmPublicTransport.java | 46 + .../mappacks/openstreetmap/OsmStandard.java | 49 + .../openstreetmap/WanderreitkarteAbo.java | 61 + .../mappacks/openstreetmap/mapsources.list | 7 + .../region_america_north/ChartbundleENRA.java | 42 + .../region_america_north/ChartbundleENRH.java | 42 + .../region_america_north/ChartbundleENRL.java | 42 + .../region_america_north/ChartbundleSEC.java | 42 + .../region_america_north/ChartbundleTAC.java | 42 + .../region_america_north/ChartbundleWAC.java | 42 + .../USNationalMapBase.java | 35 + .../USNationalMapImagery.java | 37 + .../USNationalMapImageryTopo.java | 37 + .../USNationalMapRelief.java | 37 + .../USNationalMapTopo.java | 34 + .../region_america_north/mapsources.list | 9 + .../region_europe_east/FreemapSlovakia.java | 50 + .../FreemapSlovakiaCycling.java | 48 + .../FreemapSlovakiaHiking.java | 50 + .../region_europe_east/MoldovaPointMd.java | 45 + .../region_europe_east/OSMapaTopo.java | 51 + .../OSMapaTopoContours.java | 51 + .../region_europe_east/Turaterkep.java | 32 + .../mappacks/region_europe_east/UmpWawPl.java | 48 + .../region_europe_east/mapsources.list | 8 + .../mappacks/region_oceania/NzTopoMaps.java | 68 + .../mappacks/region_oceania/mapsources.list | 1 + .../mapsources/mapspace/MapSpaceFactory.java | 35 + .../mapspace/MercatorPower2MapSpace.java | 220 ++++ .../MercatorPower2MapSpaceEllipsoidal.java | 172 +++ .../mobac/optional/JavaAdvancedImaging.java | 56 + src/main/java/mobac/program/AtlasThread.java | 456 +++++++ .../java/mobac/program/DirectoryManager.java | 215 ++++ .../java/mobac/program/EnvironmentSetup.java | 167 +++ .../java/mobac/program/JobDispatcher.java | 204 +++ src/main/java/mobac/program/Logging.java | 220 ++++ .../mobac/program/PauseResumeHandler.java | 66 + src/main/java/mobac/program/ProgramInfo.java | 109 ++ .../program/annotations/AtlasCreatorName.java | 37 + .../annotations/SupportedParameters.java | 38 + .../mobac/program/atlascreators/AFTrack.java | 114 ++ .../atlascreators/AbstractPlainImage.java | 156 +++ .../program/atlascreators/AlpineQuestMap.java | 399 ++++++ .../mobac/program/atlascreators/AndNav.java | 37 + .../program/atlascreators/AtlasCreator.java | 268 ++++ .../atlascreators/BackCountryNavigator.java | 34 + .../atlascreators/BigPlanetTracks.java | 39 + .../mobac/program/atlascreators/CacheBox.java | 262 ++++ .../program/atlascreators/CacheWolf.java | 157 +++ .../mobac/program/atlascreators/GCLive.java | 311 +++++ .../mobac/program/atlascreators/Galileo.java | 24 + .../program/atlascreators/GarminCustom.java | 91 ++ .../mobac/program/atlascreators/Glopus.java | 101 ++ .../program/atlascreators/GlopusMapFile.java | 227 ++++ .../atlascreators/GoogleEarthOverlay.java | 226 ++++ .../atlascreators/GpsSportsTracker.java | 97 ++ .../atlascreators/IPhone3MapTiles5.java | 208 +++ .../mobac/program/atlascreators/MBTiles.java | 185 +++ .../mobac/program/atlascreators/MGMaps.java | 287 +++++ .../program/atlascreators/MagellanRmp.java | 218 ++++ .../mobac/program/atlascreators/Maplorer.java | 254 ++++ .../mobac/program/atlascreators/Maverick.java | 34 + .../atlascreators/MobileTrailExplorer.java | 110 ++ .../MobileTrailExplorerCache.java | 147 +++ .../program/atlascreators/NFComPass.java | 180 +++ .../program/atlascreators/NaviComputer.java | 235 ++++ .../mobac/program/atlascreators/OSMAND.java | 27 + .../program/atlascreators/OSMAND_SQlite.java | 31 + .../program/atlascreators/OSMTracker.java | 119 ++ .../mobac/program/atlascreators/OruxMaps.java | 318 +++++ .../program/atlascreators/OruxMapsSqlite.java | 233 ++++ .../mobac/program/atlascreators/Osmdroid.java | 94 ++ .../program/atlascreators/OsmdroidGEMF.java | 54 + .../program/atlascreators/OsmdroidSQLite.java | 157 +++ .../java/mobac/program/atlascreators/Ozi.java | 243 ++++ .../program/atlascreators/PNGWorldfile.java | 118 ++ .../program/atlascreators/PaperAtlas.java | 339 +++++ .../program/atlascreators/PaperAtlasPdf.java | 127 ++ .../program/atlascreators/PaperAtlasPng.java | 63 + .../mobac/program/atlascreators/PathAway.java | 88 ++ .../program/atlascreators/RMapsSQLite.java | 255 ++++ .../program/atlascreators/RunGPSAtlas.java | 161 +++ .../program/atlascreators/SportsTracker.java | 144 +++ .../mobac/program/atlascreators/TTQV.java | 125 ++ .../atlascreators/TileStoreDownload.java | 38 + .../program/atlascreators/TomTomRaster.java | 246 ++++ .../program/atlascreators/TrekBuddy.java | 334 +++++ .../program/atlascreators/TrekBuddyTared.java | 121 ++ .../program/atlascreators/TwoNavRMAP.java | 547 ++++++++ .../mobac/program/atlascreators/Ublox.java | 88 ++ .../program/atlascreators/Viewranger.java | 73 ++ .../atlascreators/impl/MapTileBuilder.java | 268 ++++ .../atlascreators/impl/MapTileWriter.java | 40 + .../impl/aqm/FlatPackCreator.java | 165 +++ .../impl/gemf/GEMFFileCreator.java | 391 ++++++ .../atlascreators/impl/rmp/BoundingRect.java | 86 ++ .../impl/rmp/ChecksumOutputStream.java | 137 ++ .../atlascreators/impl/rmp/MobacTile.java | 191 +++ .../atlascreators/impl/rmp/MultiImage.java | 127 ++ .../atlascreators/impl/rmp/RmpLayer.java | 302 +++++ .../atlascreators/impl/rmp/RmpTools.java | 162 +++ .../atlascreators/impl/rmp/RmpWriter.java | 253 ++++ .../atlascreators/impl/rmp/TileContainer.java | 150 +++ .../atlascreators/impl/rmp/Tiledata.java | 73 ++ .../impl/rmp/interfaces/RmpFileEntry.java | 50 + .../impl/rmp/rmpfile/Bmp2bit.java | 51 + .../impl/rmp/rmpfile/Bmp4bit.java | 55 + .../impl/rmp/rmpfile/GeneralRmpFileEntry.java | 63 + .../impl/rmp/rmpfile/RmpIni.java | 72 ++ .../impl/rungps/RunGPSAtlasFile.java | 434 +++++++ .../tileprovider/CacheTileProvider.java | 256 ++++ .../ConvertedRawTileProvider.java | 56 + .../tileprovider/DownloadedTileProvider.java | 68 + .../tileprovider/FilterTileProvider.java | 53 + .../FilteredMapSourceProvider.java | 62 + .../tileprovider/GpxPainterTileProvider.java | 60 + .../tileprovider/MapSourceProvider.java | 82 ++ .../tileprovider/PngTileProvider.java | 60 + .../tileprovider/TileProvider.java | 39 + .../tileprovider/TileStoreTileProvider.java | 32 + .../program/commandline/CommandLineEmpty.java | 41 + .../program/commandline/CreateAtlas.java | 93 ++ .../mobac/program/download/DownloadJob.java | 110 ++ .../download/DownloadJobProducerThread.java | 71 ++ .../program/download/TileDownLoader.java | 406 ++++++ .../mobac/program/download/UserAgent.java | 58 + .../jobenumerators/DownloadJobEnumerator.java | 106 ++ .../program/interfaces/AtlasInterface.java | 49 + .../mobac/program/interfaces/AtlasObject.java | 42 + .../interfaces/CapabilityDeletable.java | 30 + .../program/interfaces/CommandLineAction.java | 28 + .../interfaces/DownloadJobListener.java | 29 + .../interfaces/DownloadableElement.java | 46 + .../interfaces/ExceptionExtendedInfo.java | 22 + .../interfaces/FileBasedMapSource.java | 27 + .../program/interfaces/HttpMapSource.java | 63 + .../interfaces/InitializableMapSource.java | 23 + .../program/interfaces/LayerInterface.java | 33 + .../program/interfaces/MapInterface.java | 54 + .../mobac/program/interfaces/MapSource.java | 114 ++ .../program/interfaces/MapSourceListener.java | 24 + .../interfaces/MapSourceTextAttribution.java | 27 + .../mobac/program/interfaces/MapSpace.java | 122 ++ .../program/interfaces/RequiresSQLite.java | 28 + .../mobac/program/interfaces/TileFilter.java | 37 + .../interfaces/TileImageDataWriter.java | 48 + .../program/interfaces/ToolTipProvider.java | 22 + .../program/interfaces/WrappedMapSource.java | 24 + .../jaxb/AtlasOutputFormatAdapter.java | 36 + .../mobac/program/jaxb/BigDecimalAdapter.java | 40 + .../java/mobac/program/jaxb/ColorAdapter.java | 55 + .../mobac/program/jaxb/DimensionAdapter.java | 44 + .../java/mobac/program/jaxb/FontAdapter.java | 38 + .../mobac/program/jaxb/MapSourceAdapter.java | 37 + .../mobac/program/jaxb/PaperSizeAdapter.java | 62 + .../java/mobac/program/jaxb/PointAdapter.java | 45 + .../mobac/program/jaxb/PolygonAdapter.java | 54 + .../java/mobac/program/jaxb/PolygonType.java | 39 + .../program/jaxb/TileImageTypeAdapter.java | 35 + .../mobac/program/model/AnyAttributeMap.java | 60 + src/main/java/mobac/program/model/Atlas.java | 221 ++++ .../program/model/AtlasOutputFormat.java | 198 +++ .../mobac/program/model/AtlasTreeModel.java | 218 ++++ .../java/mobac/program/model/Bookmark.java | 82 ++ .../java/mobac/program/model/Coordinate.java | 54 + .../program/model/CoordinateStringFormat.java | 86 ++ .../program/model/EastNorthCoordinate.java | 71 ++ src/main/java/mobac/program/model/Layer.java | 302 +++++ src/main/java/mobac/program/model/Map.java | 329 +++++ .../java/mobac/program/model/MapPolygon.java | 234 ++++ .../mobac/program/model/MapSelection.java | 247 ++++ .../program/model/MapSourceLoaderInfo.java | 59 + .../program/model/MapSourcesListModel.java | 86 ++ .../model/MercatorPixelCoordinate.java | 93 ++ .../mobac/program/model/NumericDocument.java | 44 + .../java/mobac/program/model/PaperSize.java | 90 ++ .../java/mobac/program/model/Profile.java | 229 ++++ .../java/mobac/program/model/ProxyType.java | 45 + .../program/model/SelectedZoomLevels.java | 70 + .../java/mobac/program/model/Settings.java | 520 ++++++++ .../program/model/SettingsPaperAtlas.java | 100 ++ .../mobac/program/model/SettingsWgsGrid.java | 80 ++ .../mobac/program/model/TileImageFormat.java | 124 ++ .../program/model/TileImageParameters.java | 86 ++ .../mobac/program/model/TileImageType.java | 50 + .../java/mobac/program/model/UnitSystem.java | 90 ++ .../mobac/program/model/package-info.java | 31 + .../ImageWriterWarningListener.java | 41 + .../TileImageJpegDataWriter.java | 134 ++ .../TileImagePng4DataWriter.java | 47 + .../TileImagePng8DataWriter.java | 36 + .../TileImagePngDataWriter.java | 73 ++ .../program/tilefilter/DummyTileFilter.java | 31 + .../program/tilefilter/PolygonTileFilter.java | 50 + .../mobac/program/tilestore/TileStore.java | 137 ++ .../program/tilestore/TileStoreEntry.java | 66 + .../program/tilestore/TileStoreInfo.java | 44 + .../berkeleydb/BerkeleyDbTileStore.java | 645 ++++++++++ .../berkeleydb/DelayedInterruptThread.java | 77 ++ .../tilestore/berkeleydb/TileDbEntry.java | 139 ++ src/main/java/mobac/utilities/Charsets.java | 27 + .../mobac/utilities/ExtensionClassLoader.java | 112 ++ src/main/java/mobac/utilities/GBC.java | 183 +++ src/main/java/mobac/utilities/GBCTable.java | 97 ++ .../mobac/utilities/GUIExceptionHandler.java | 354 ++++++ src/main/java/mobac/utilities/I18nUtils.java | 118 ++ .../mobac/utilities/Juli2Log4jHandler.java | 91 ++ .../mobac/utilities/MapDataFileParser.java | 152 +++ src/main/java/mobac/utilities/MyMath.java | 179 +++ .../java/mobac/utilities/OSUtilities.java | 189 +++ src/main/java/mobac/utilities/Utilities.java | 735 +++++++++++ .../java/mobac/utilities/beanshell/Tools.java | 80 ++ .../utilities/collections/SoftHashMap.java | 93 ++ .../utilities/debug/MySocketImplFactory.java | 313 +++++ .../utilities/file/DeleteFileFilter.java | 73 ++ .../utilities/file/DirInfoFileFilter.java | 49 + .../utilities/file/DirectoryFileFilter.java | 30 + .../mobac/utilities/file/FileExtFilter.java | 34 + .../mobac/utilities/file/GpxFileFilter.java | 45 + .../utilities/file/NamePatternFileFilter.java | 41 + .../mobac/utilities/file/RegexFileFilter.java | 35 + .../utilities/geo/CoordinateDm2Format.java | 91 ++ .../utilities/geo/CoordinateDms2Format.java | 101 ++ .../utilities/geo/CoordinateTileFormat.java | 94 ++ .../java/mobac/utilities/geo/GeoUtils.java | 41 + .../utilities/imageio/Png4BitWriter.java | 201 +++ .../mobac/utilities/imageio/PngChunk.java | 48 + .../mobac/utilities/imageio/PngConstants.java | 75 ++ .../mobac/utilities/imageio/PngXxlWriter.java | 226 ++++ .../mobac/utilities/jdbc/DriverProxy.java | 85 ++ .../mobac/utilities/jdbc/SQLiteLoader.java | 91 ++ .../utilities/stream/ArrayOutputStream.java | 89 ++ .../stream/CountingOutputStream.java | 53 + .../stream/LittleEndianOutputStream.java | 82 ++ .../stream/RandomAccessFileOutputStream.java | 64 + .../utilities/stream/ThrottleSupport.java | 118 ++ .../stream/ThrottledInputStream.java | 61 + .../stream/ZipStoreOutputStream.java | 60 + .../java/mobac/utilities/tar/TarArchive.java | 140 ++ .../java/mobac/utilities/tar/TarHeader.java | 290 +++++ .../java/mobac/utilities/tar/TarIndex.java | 73 ++ .../mobac/utilities/tar/TarIndexTable.java | 49 + .../utilities/tar/TarIndexedArchive.java | 61 + .../java/mobac/utilities/tar/TarRecord.java | 86 ++ .../mobac/utilities/tar/TarTmiArchive.java | 75 ++ .../utilities/writer/NullPrintWriter.java | 27 + .../mobac/utilities/writer/NullWriter.java | 35 + src/main/resources/mobac/mobac-rev.properties | 3 + src/main/resources/mobac/mobac.properties | 6 + .../mobac/resources/cert/MapPack.cer | Bin 0 -> 441 bytes .../mobac/resources/images/Splash.jpg | Bin 0 -> 41570 bytes .../mobac/resources/images/ajax-loader.gif | Bin 0 -> 10819 bytes .../resources/images/arrow_blue_down.png | Bin 0 -> 1359 bytes .../resources/images/arrow_blue_left.png | Bin 0 -> 1702 bytes .../resources/images/arrow_blue_right.png | Bin 0 -> 1714 bytes .../mobac/resources/images/arrow_blue_up.png | Bin 0 -> 1737 bytes .../mobac/resources/images/arrow_closed.png | Bin 0 -> 616 bytes .../mobac/resources/images/arrow_open.png | Bin 0 -> 473 bytes .../mobac/resources/images/atlas.png | Bin 0 -> 1021 bytes .../mobac/resources/images/compass.png | Bin 0 -> 16875 bytes .../mobac/resources/images/error.png | Bin 0 -> 5668 bytes .../mobac/resources/images/hourglass.png | Bin 0 -> 9096 bytes .../mobac/resources/images/layer.png | Bin 0 -> 551 bytes .../resources/mobac/resources/images/map.png | Bin 0 -> 1210 bytes .../mobac/resources/images/minus.png | Bin 0 -> 171 bytes .../mobac/resources/images/mobac16.png | Bin 0 -> 941 bytes .../mobac/resources/images/mobac32.png | Bin 0 -> 2702 bytes .../mobac/resources/images/mobac48.png | Bin 0 -> 5035 bytes .../resources/mobac/resources/images/plus.png | Bin 0 -> 225 bytes .../mobac/resources/images/refresh.png | Bin 0 -> 1357 bytes .../mobac/resources/images/trash.png | Bin 0 -> 872 bytes .../resources/mobac/resources/text/agpl.txt | 619 +++++++++ .../mobac/resources/text/apache-2.0.txt | 202 +++ .../resources/mobac/resources/text/gpl.txt | 339 +++++ .../mobac/resources/text/help_dialog.html | 53 + .../mobac/resources/text/help_dialog_fr.html | 53 + .../mobac/resources/text/help_dialog_ja.html | 53 + .../mobac/resources/text/help_dialog_zh.html | 53 + .../mobac/resources/text/lgpl-3.0.txt | 165 +++ .../mobac/resources/text/license-dbd-je.txt | 75 ++ .../mobac/resources/text/localize.properties | 662 ++++++++++ .../resources/text/localize_fr_FR.properties | 657 ++++++++++ .../resources/text/localize_ja_JP.properties | 653 ++++++++++ .../resources/text/localize_zh_CN.properties | 662 ++++++++++ .../resources/text/localize_zh_TW.properties | 662 ++++++++++ .../mobac/resources/xsl/gpx10to11.xsl | 75 ++ .../java/featuretests/SimpleHttpClient.java | 74 ++ src/test/java/featuretests/SimpleServer.java | 102 ++ .../java/featuretests/TileCoverageTest.java | 76 ++ .../java/mapsources/MapSourceTestCase.java | 108 ++ .../MapSourceTestFailedException.java | 88 ++ .../java/mapsources/MapSourcesTestSuite.java | 97 ++ src/test/java/mobac/tools/Cities.java | 100 ++ .../mobac/tools/MapPackUploadSelector.java | 72 ++ .../tools/MapSourceCapabilityDetector.java | 338 +++++ .../mobac/tools/MapSourceCapabilityGUI.java | 112 ++ .../java/mobac/tools/MapUpdateTypeLister.java | 43 + .../tools/testtileserver/TestTileServer.java | 184 +++ .../AbstractTileGeneratorServlet.java | 67 + .../servlets/AbstractTileServlet.java | 113 ++ .../servlets/JpgTileGeneratorServlet.java | 69 + .../servlets/PngFileTileServlet.java | 73 ++ .../servlets/PngTileGeneratorServlet.java | 59 + .../servlets/ShutdownServlet.java | 44 + .../AbstractAtlasCreatorTestCase.java | 156 +++ .../java/unittests/AtlasDownloadTestCase.java | 41 + .../java/unittests/CordinateTestCase.java | 86 ++ src/test/java/unittests/KMZTestCase.java | 42 + src/test/java/unittests/SQLiteTestCase.java | 32 + .../unittests/helper/DummyAtlasCreator.java | 43 + .../java/unittests/helper/DummyTileStore.java | 104 ++ .../helper/TestMapSourcesManager.java | 90 ++ .../java/unittests/methods/MyMathTests.java | 52 + .../unittests/methods/UtilitiesTests.java | 39 + .../testtileserver/servlets/images/cross.png | Bin 0 -> 2582 bytes .../servlets/images/gradient.png | Bin 0 -> 4163 bytes .../testtileserver/servlets/images/tile.png | Bin 0 -> 80765 bytes start.sh | 9 + 512 files changed, 61596 insertions(+), 251 deletions(-) create mode 100644 CHANGES.md delete mode 100644 LICENSE create mode 100644 Mobile Atlas Creator.exe create mode 100644 build.xml create mode 100644 gpl.txt create mode 100644 misc/launch4j.xml create mode 100644 misc/mobac.ico create mode 100644 settings.xml create mode 100644 src/main/java/mobac/Main.java create mode 100644 src/main/java/mobac/StartMOBAC.java create mode 100644 src/main/java/mobac/data/gpx/GPXUtils.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/BoundsType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/CopyrightType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/EmailType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/ExtensionsType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/Gpx.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/LinkType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/MetadataType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/PersonType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/PtType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/PtsegType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/RteType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/TrkType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/TrksegType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/WptType.java create mode 100644 src/main/java/mobac/data/gpx/gpx11/package-info.java create mode 100644 src/main/java/mobac/data/gpx/interfaces/GpxPoint.java create mode 100644 src/main/java/mobac/exceptions/AtlasTestException.java create mode 100644 src/main/java/mobac/exceptions/DownloadFailedException.java create mode 100644 src/main/java/mobac/exceptions/InvalidNameException.java create mode 100644 src/main/java/mobac/exceptions/MOBACOutOfMemoryException.java create mode 100644 src/main/java/mobac/exceptions/MapCreationException.java create mode 100644 src/main/java/mobac/exceptions/MapDownloadSkippedException.java create mode 100644 src/main/java/mobac/exceptions/MapSourceCreateException.java create mode 100644 src/main/java/mobac/exceptions/MapSourceInitializationException.java create mode 100644 src/main/java/mobac/exceptions/MapSourcesUpdateException.java create mode 100644 src/main/java/mobac/exceptions/StopAllDownloadsException.java create mode 100644 src/main/java/mobac/exceptions/TileException.java create mode 100644 src/main/java/mobac/exceptions/TileStoreException.java create mode 100644 src/main/java/mobac/exceptions/UnrecoverableDownloadException.java create mode 100644 src/main/java/mobac/exceptions/UpdateFailedException.java create mode 100644 src/main/java/mobac/externaltools/ExternalToolDef.java create mode 100644 src/main/java/mobac/externaltools/ExternalToolsLoader.java create mode 100644 src/main/java/mobac/externaltools/ToolParameters.java create mode 100644 src/main/java/mobac/gui/AtlasProgress.java create mode 100644 src/main/java/mobac/gui/MainGUI.java create mode 100644 src/main/java/mobac/gui/SplashFrame.java create mode 100644 src/main/java/mobac/gui/actions/AddGpxTrackAreaPolygonMap.java create mode 100644 src/main/java/mobac/gui/actions/AddGpxTrackPolygonMap.java create mode 100644 src/main/java/mobac/gui/actions/AddMapLayer.java create mode 100644 src/main/java/mobac/gui/actions/AddPolygonMapLayer.java create mode 100644 src/main/java/mobac/gui/actions/AddRectangleMapAutocut.java create mode 100644 src/main/java/mobac/gui/actions/AtlasConvert.java create mode 100644 src/main/java/mobac/gui/actions/AtlasCreate.java create mode 100644 src/main/java/mobac/gui/actions/AtlasNew.java create mode 100644 src/main/java/mobac/gui/actions/BookmarkAdd.java create mode 100644 src/main/java/mobac/gui/actions/BookmarkManage.java create mode 100644 src/main/java/mobac/gui/actions/DebugSetLogLevel.java create mode 100644 src/main/java/mobac/gui/actions/DebugShowLogFile.java create mode 100644 src/main/java/mobac/gui/actions/DebugShowMapSourceNames.java create mode 100644 src/main/java/mobac/gui/actions/DebugShowMapTileGrid.java create mode 100644 src/main/java/mobac/gui/actions/DebugShowReport.java create mode 100644 src/main/java/mobac/gui/actions/GpxAddPoint.java create mode 100644 src/main/java/mobac/gui/actions/GpxClear.java create mode 100644 src/main/java/mobac/gui/actions/GpxEditor.java create mode 100644 src/main/java/mobac/gui/actions/GpxElementListener.java create mode 100644 src/main/java/mobac/gui/actions/GpxLoad.java create mode 100644 src/main/java/mobac/gui/actions/GpxNew.java create mode 100644 src/main/java/mobac/gui/actions/GpxSave.java create mode 100644 src/main/java/mobac/gui/actions/HelpLicenses.java create mode 100644 src/main/java/mobac/gui/actions/OpenInWebbrowser.java create mode 100644 src/main/java/mobac/gui/actions/PanelShowHide.java create mode 100644 src/main/java/mobac/gui/actions/RefreshCustomMapsources.java create mode 100644 src/main/java/mobac/gui/actions/SelectionModeCircle.java create mode 100644 src/main/java/mobac/gui/actions/SelectionModePolygon.java create mode 100644 src/main/java/mobac/gui/actions/SelectionModeRectangle.java create mode 100644 src/main/java/mobac/gui/actions/ShowAboutDialog.java create mode 100644 src/main/java/mobac/gui/actions/ShowHelpAction.java create mode 100644 src/main/java/mobac/gui/actions/ShowReadme.java create mode 100644 src/main/java/mobac/gui/atlastree/DragDropController.java create mode 100644 src/main/java/mobac/gui/atlastree/JAtlasTree.java create mode 100644 src/main/java/mobac/gui/atlastree/MouseController.java create mode 100644 src/main/java/mobac/gui/atlastree/NodeEditor.java create mode 100644 src/main/java/mobac/gui/atlastree/NodeRenderer.java create mode 100644 src/main/java/mobac/gui/atlastree/NodeTransferWrapper.java create mode 100644 src/main/java/mobac/gui/components/FilledLayeredPane.java create mode 100644 src/main/java/mobac/gui/components/JAtlasNameField.java create mode 100644 src/main/java/mobac/gui/components/JBookmarkMenuItem.java create mode 100644 src/main/java/mobac/gui/components/JCollapsiblePanel.java create mode 100644 src/main/java/mobac/gui/components/JCoordinateField.java create mode 100644 src/main/java/mobac/gui/components/JDirectoryChooser.java create mode 100644 src/main/java/mobac/gui/components/JDistanceSlider.java create mode 100644 src/main/java/mobac/gui/components/JDropDownButton.java create mode 100644 src/main/java/mobac/gui/components/JIntCombo.java create mode 100644 src/main/java/mobac/gui/components/JIntField.java create mode 100644 src/main/java/mobac/gui/components/JMapSizeCombo.java create mode 100644 src/main/java/mobac/gui/components/JMenuItem2.java create mode 100644 src/main/java/mobac/gui/components/JObjectCheckBox.java create mode 100644 src/main/java/mobac/gui/components/JProfilesComboBox.java create mode 100644 src/main/java/mobac/gui/components/JRegexTextField.java create mode 100644 src/main/java/mobac/gui/components/JTileSizeCombo.java create mode 100644 src/main/java/mobac/gui/components/JTimeSlider.java create mode 100644 src/main/java/mobac/gui/components/JZoomCheckBox.java create mode 100644 src/main/java/mobac/gui/dialogs/AboutDialog.java create mode 100644 src/main/java/mobac/gui/dialogs/FontChooser.java create mode 100644 src/main/java/mobac/gui/dialogs/Help.java create mode 100644 src/main/java/mobac/gui/dialogs/LicensesDialog.java create mode 100644 src/main/java/mobac/gui/dialogs/ManageBookmarks.java create mode 100644 src/main/java/mobac/gui/dialogs/MessageDialogs.java create mode 100644 src/main/java/mobac/gui/dialogs/WorkinprogressDialog.java create mode 100644 src/main/java/mobac/gui/gpxtree/GpxEntry.java create mode 100644 src/main/java/mobac/gui/gpxtree/GpxRootEntry.java create mode 100644 src/main/java/mobac/gui/gpxtree/GpxTreeListener.java create mode 100644 src/main/java/mobac/gui/gpxtree/RteEntry.java create mode 100644 src/main/java/mobac/gui/gpxtree/TrkEntry.java create mode 100644 src/main/java/mobac/gui/gpxtree/TrksegEntry.java create mode 100644 src/main/java/mobac/gui/gpxtree/WptEntry.java create mode 100644 src/main/java/mobac/gui/listeners/AtlasModelListener.java create mode 100644 src/main/java/mobac/gui/mapview/GridZoom.java create mode 100644 src/main/java/mobac/gui/mapview/JMapViewer.java create mode 100644 src/main/java/mobac/gui/mapview/JobDispatcher.java create mode 100644 src/main/java/mobac/gui/mapview/MemoryTileCache.java create mode 100644 src/main/java/mobac/gui/mapview/PreviewMap.java create mode 100644 src/main/java/mobac/gui/mapview/ScaleBar.java create mode 100644 src/main/java/mobac/gui/mapview/Tile.java create mode 100644 src/main/java/mobac/gui/mapview/TileLoader.java create mode 100644 src/main/java/mobac/gui/mapview/WgsGrid.java create mode 100644 src/main/java/mobac/gui/mapview/controller/AbstractPolygonSelectionMapController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/DefaultMapController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/GpxMapController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/JMapController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/MapKeyboardController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/PolygonCircleSelectionMapController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/PolygonSelectionMapController.java create mode 100644 src/main/java/mobac/gui/mapview/controller/RectangleSelectionMapController.java create mode 100644 src/main/java/mobac/gui/mapview/interfaces/MapEventListener.java create mode 100644 src/main/java/mobac/gui/mapview/interfaces/MapLayer.java create mode 100644 src/main/java/mobac/gui/mapview/interfaces/MapTileLayer.java create mode 100644 src/main/java/mobac/gui/mapview/interfaces/TileLoaderListener.java create mode 100644 src/main/java/mobac/gui/mapview/layer/DefaultMapTileLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/GpxLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/MapAreaHighlightingLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/MapGridLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/OverlayMapTileLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/PolygonSelectionLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/RectangleSelectionLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/ShapeLayer.java create mode 100644 src/main/java/mobac/gui/mapview/layer/TileStoreCoverageLayer.java create mode 100644 src/main/java/mobac/gui/panels/JCoordinatesPanel.java create mode 100644 src/main/java/mobac/gui/panels/JGpxPanel.java create mode 100644 src/main/java/mobac/gui/panels/JProfilesPanel.java create mode 100644 src/main/java/mobac/gui/panels/JTileImageParametersPanel.java create mode 100644 src/main/java/mobac/gui/panels/JTileStoreCoveragePanel.java create mode 100644 src/main/java/mobac/gui/settings/SettingsGUI.java create mode 100644 src/main/java/mobac/gui/settings/SettingsGUIPaper.java create mode 100644 src/main/java/mobac/gui/settings/SettingsGUITileStore.java create mode 100644 src/main/java/mobac/gui/settings/SettingsGUIWgsGrid.java create mode 100644 src/main/java/mobac/mapsources/AbstractHttpMapSource.java create mode 100644 src/main/java/mobac/mapsources/AbstractMultiLayerMapSource.java create mode 100644 src/main/java/mobac/mapsources/DefaultMapSourcesManager.java create mode 100644 src/main/java/mobac/mapsources/MapSourceTools.java create mode 100644 src/main/java/mobac/mapsources/MapSourceUrlUpdater.java create mode 100644 src/main/java/mobac/mapsources/MapSourcesManager.java create mode 100644 src/main/java/mobac/mapsources/MapSourcesPropertiesManager.java create mode 100644 src/main/java/mobac/mapsources/custom/BeanShellHttpMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomCloudMade.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomLocalImageFileMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomLocalTileFilesMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomLocalTileSQliteMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomLocalTileZipMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomMapSourceType.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomMultiLayerMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/CustomWmsMapSource.java create mode 100644 src/main/java/mobac/mapsources/custom/StandardMapSourceLayer.java create mode 100644 src/main/java/mobac/mapsources/custom/package-info.java create mode 100644 src/main/java/mobac/mapsources/impl/DebugLocalMapSource.java create mode 100644 src/main/java/mobac/mapsources/impl/DebugMapSource.java create mode 100644 src/main/java/mobac/mapsources/impl/DebugRandomLocalMapSource.java create mode 100644 src/main/java/mobac/mapsources/impl/DebugTransparentLocalMapSource.java create mode 100644 src/main/java/mobac/mapsources/impl/LocalhostTestSource.java create mode 100644 src/main/java/mobac/mapsources/impl/SimpleMapSource.java create mode 100644 src/main/java/mobac/mapsources/loader/BeanShellMapSourceLoader.java create mode 100644 src/main/java/mobac/mapsources/loader/CustomMapSourceLoader.java create mode 100644 src/main/java/mobac/mapsources/loader/EclipseMapPackLoader.java create mode 100644 src/main/java/mobac/mapsources/loader/MapPackClassLoader.java create mode 100644 src/main/java/mobac/mapsources/loader/MapPackManager.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/AbstractOsmMapSource.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/CloudMade.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/Hikebikemap.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/MapQuest.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/Mapnik.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenPisteMap.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenSeaMap.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmPublicTransport.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmStandard.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/WanderreitkarteAbo.java create mode 100644 src/main/java/mobac/mapsources/mappacks/openstreetmap/mapsources.list create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRA.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRH.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRL.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleSEC.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleTAC.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleWAC.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapBase.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImagery.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImageryTopo.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapRelief.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapTopo.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_america_north/mapsources.list create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakia.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaCycling.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaHiking.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/MoldovaPointMd.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopo.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopoContours.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/Turaterkep.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/UmpWawPl.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_europe_east/mapsources.list create mode 100644 src/main/java/mobac/mapsources/mappacks/region_oceania/NzTopoMaps.java create mode 100644 src/main/java/mobac/mapsources/mappacks/region_oceania/mapsources.list create mode 100644 src/main/java/mobac/mapsources/mapspace/MapSpaceFactory.java create mode 100644 src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpace.java create mode 100644 src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpaceEllipsoidal.java create mode 100644 src/main/java/mobac/optional/JavaAdvancedImaging.java create mode 100644 src/main/java/mobac/program/AtlasThread.java create mode 100644 src/main/java/mobac/program/DirectoryManager.java create mode 100644 src/main/java/mobac/program/EnvironmentSetup.java create mode 100644 src/main/java/mobac/program/JobDispatcher.java create mode 100644 src/main/java/mobac/program/Logging.java create mode 100644 src/main/java/mobac/program/PauseResumeHandler.java create mode 100644 src/main/java/mobac/program/ProgramInfo.java create mode 100644 src/main/java/mobac/program/annotations/AtlasCreatorName.java create mode 100644 src/main/java/mobac/program/annotations/SupportedParameters.java create mode 100644 src/main/java/mobac/program/atlascreators/AFTrack.java create mode 100644 src/main/java/mobac/program/atlascreators/AbstractPlainImage.java create mode 100644 src/main/java/mobac/program/atlascreators/AlpineQuestMap.java create mode 100644 src/main/java/mobac/program/atlascreators/AndNav.java create mode 100644 src/main/java/mobac/program/atlascreators/AtlasCreator.java create mode 100644 src/main/java/mobac/program/atlascreators/BackCountryNavigator.java create mode 100644 src/main/java/mobac/program/atlascreators/BigPlanetTracks.java create mode 100644 src/main/java/mobac/program/atlascreators/CacheBox.java create mode 100644 src/main/java/mobac/program/atlascreators/CacheWolf.java create mode 100644 src/main/java/mobac/program/atlascreators/GCLive.java create mode 100644 src/main/java/mobac/program/atlascreators/Galileo.java create mode 100644 src/main/java/mobac/program/atlascreators/GarminCustom.java create mode 100644 src/main/java/mobac/program/atlascreators/Glopus.java create mode 100644 src/main/java/mobac/program/atlascreators/GlopusMapFile.java create mode 100644 src/main/java/mobac/program/atlascreators/GoogleEarthOverlay.java create mode 100644 src/main/java/mobac/program/atlascreators/GpsSportsTracker.java create mode 100644 src/main/java/mobac/program/atlascreators/IPhone3MapTiles5.java create mode 100644 src/main/java/mobac/program/atlascreators/MBTiles.java create mode 100644 src/main/java/mobac/program/atlascreators/MGMaps.java create mode 100644 src/main/java/mobac/program/atlascreators/MagellanRmp.java create mode 100644 src/main/java/mobac/program/atlascreators/Maplorer.java create mode 100644 src/main/java/mobac/program/atlascreators/Maverick.java create mode 100644 src/main/java/mobac/program/atlascreators/MobileTrailExplorer.java create mode 100644 src/main/java/mobac/program/atlascreators/MobileTrailExplorerCache.java create mode 100644 src/main/java/mobac/program/atlascreators/NFComPass.java create mode 100644 src/main/java/mobac/program/atlascreators/NaviComputer.java create mode 100644 src/main/java/mobac/program/atlascreators/OSMAND.java create mode 100644 src/main/java/mobac/program/atlascreators/OSMAND_SQlite.java create mode 100644 src/main/java/mobac/program/atlascreators/OSMTracker.java create mode 100644 src/main/java/mobac/program/atlascreators/OruxMaps.java create mode 100644 src/main/java/mobac/program/atlascreators/OruxMapsSqlite.java create mode 100644 src/main/java/mobac/program/atlascreators/Osmdroid.java create mode 100644 src/main/java/mobac/program/atlascreators/OsmdroidGEMF.java create mode 100644 src/main/java/mobac/program/atlascreators/OsmdroidSQLite.java create mode 100644 src/main/java/mobac/program/atlascreators/Ozi.java create mode 100644 src/main/java/mobac/program/atlascreators/PNGWorldfile.java create mode 100644 src/main/java/mobac/program/atlascreators/PaperAtlas.java create mode 100644 src/main/java/mobac/program/atlascreators/PaperAtlasPdf.java create mode 100644 src/main/java/mobac/program/atlascreators/PaperAtlasPng.java create mode 100644 src/main/java/mobac/program/atlascreators/PathAway.java create mode 100644 src/main/java/mobac/program/atlascreators/RMapsSQLite.java create mode 100644 src/main/java/mobac/program/atlascreators/RunGPSAtlas.java create mode 100644 src/main/java/mobac/program/atlascreators/SportsTracker.java create mode 100644 src/main/java/mobac/program/atlascreators/TTQV.java create mode 100644 src/main/java/mobac/program/atlascreators/TileStoreDownload.java create mode 100644 src/main/java/mobac/program/atlascreators/TomTomRaster.java create mode 100644 src/main/java/mobac/program/atlascreators/TrekBuddy.java create mode 100644 src/main/java/mobac/program/atlascreators/TrekBuddyTared.java create mode 100644 src/main/java/mobac/program/atlascreators/TwoNavRMAP.java create mode 100644 src/main/java/mobac/program/atlascreators/Ublox.java create mode 100644 src/main/java/mobac/program/atlascreators/Viewranger.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/MapTileBuilder.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/MapTileWriter.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/aqm/FlatPackCreator.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/gemf/GEMFFileCreator.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/BoundingRect.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/ChecksumOutputStream.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/MobacTile.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/MultiImage.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/RmpLayer.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/RmpTools.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/RmpWriter.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/TileContainer.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/Tiledata.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/interfaces/RmpFileEntry.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp2bit.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp4bit.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/GeneralRmpFileEntry.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/RmpIni.java create mode 100644 src/main/java/mobac/program/atlascreators/impl/rungps/RunGPSAtlasFile.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/CacheTileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/ConvertedRawTileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/DownloadedTileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/FilterTileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/FilteredMapSourceProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/GpxPainterTileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/MapSourceProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/PngTileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/TileProvider.java create mode 100644 src/main/java/mobac/program/atlascreators/tileprovider/TileStoreTileProvider.java create mode 100644 src/main/java/mobac/program/commandline/CommandLineEmpty.java create mode 100644 src/main/java/mobac/program/commandline/CreateAtlas.java create mode 100644 src/main/java/mobac/program/download/DownloadJob.java create mode 100644 src/main/java/mobac/program/download/DownloadJobProducerThread.java create mode 100644 src/main/java/mobac/program/download/TileDownLoader.java create mode 100644 src/main/java/mobac/program/download/UserAgent.java create mode 100644 src/main/java/mobac/program/download/jobenumerators/DownloadJobEnumerator.java create mode 100644 src/main/java/mobac/program/interfaces/AtlasInterface.java create mode 100644 src/main/java/mobac/program/interfaces/AtlasObject.java create mode 100644 src/main/java/mobac/program/interfaces/CapabilityDeletable.java create mode 100644 src/main/java/mobac/program/interfaces/CommandLineAction.java create mode 100644 src/main/java/mobac/program/interfaces/DownloadJobListener.java create mode 100644 src/main/java/mobac/program/interfaces/DownloadableElement.java create mode 100644 src/main/java/mobac/program/interfaces/ExceptionExtendedInfo.java create mode 100644 src/main/java/mobac/program/interfaces/FileBasedMapSource.java create mode 100644 src/main/java/mobac/program/interfaces/HttpMapSource.java create mode 100644 src/main/java/mobac/program/interfaces/InitializableMapSource.java create mode 100644 src/main/java/mobac/program/interfaces/LayerInterface.java create mode 100644 src/main/java/mobac/program/interfaces/MapInterface.java create mode 100644 src/main/java/mobac/program/interfaces/MapSource.java create mode 100644 src/main/java/mobac/program/interfaces/MapSourceListener.java create mode 100644 src/main/java/mobac/program/interfaces/MapSourceTextAttribution.java create mode 100644 src/main/java/mobac/program/interfaces/MapSpace.java create mode 100644 src/main/java/mobac/program/interfaces/RequiresSQLite.java create mode 100644 src/main/java/mobac/program/interfaces/TileFilter.java create mode 100644 src/main/java/mobac/program/interfaces/TileImageDataWriter.java create mode 100644 src/main/java/mobac/program/interfaces/ToolTipProvider.java create mode 100644 src/main/java/mobac/program/interfaces/WrappedMapSource.java create mode 100644 src/main/java/mobac/program/jaxb/AtlasOutputFormatAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/BigDecimalAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/ColorAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/DimensionAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/FontAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/MapSourceAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/PaperSizeAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/PointAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/PolygonAdapter.java create mode 100644 src/main/java/mobac/program/jaxb/PolygonType.java create mode 100644 src/main/java/mobac/program/jaxb/TileImageTypeAdapter.java create mode 100644 src/main/java/mobac/program/model/AnyAttributeMap.java create mode 100644 src/main/java/mobac/program/model/Atlas.java create mode 100644 src/main/java/mobac/program/model/AtlasOutputFormat.java create mode 100644 src/main/java/mobac/program/model/AtlasTreeModel.java create mode 100644 src/main/java/mobac/program/model/Bookmark.java create mode 100644 src/main/java/mobac/program/model/Coordinate.java create mode 100644 src/main/java/mobac/program/model/CoordinateStringFormat.java create mode 100644 src/main/java/mobac/program/model/EastNorthCoordinate.java create mode 100644 src/main/java/mobac/program/model/Layer.java create mode 100644 src/main/java/mobac/program/model/Map.java create mode 100644 src/main/java/mobac/program/model/MapPolygon.java create mode 100644 src/main/java/mobac/program/model/MapSelection.java create mode 100644 src/main/java/mobac/program/model/MapSourceLoaderInfo.java create mode 100644 src/main/java/mobac/program/model/MapSourcesListModel.java create mode 100644 src/main/java/mobac/program/model/MercatorPixelCoordinate.java create mode 100644 src/main/java/mobac/program/model/NumericDocument.java create mode 100644 src/main/java/mobac/program/model/PaperSize.java create mode 100644 src/main/java/mobac/program/model/Profile.java create mode 100644 src/main/java/mobac/program/model/ProxyType.java create mode 100644 src/main/java/mobac/program/model/SelectedZoomLevels.java create mode 100644 src/main/java/mobac/program/model/Settings.java create mode 100644 src/main/java/mobac/program/model/SettingsPaperAtlas.java create mode 100644 src/main/java/mobac/program/model/SettingsWgsGrid.java create mode 100644 src/main/java/mobac/program/model/TileImageFormat.java create mode 100644 src/main/java/mobac/program/model/TileImageParameters.java create mode 100644 src/main/java/mobac/program/model/TileImageType.java create mode 100644 src/main/java/mobac/program/model/UnitSystem.java create mode 100644 src/main/java/mobac/program/model/package-info.java create mode 100644 src/main/java/mobac/program/tiledatawriter/ImageWriterWarningListener.java create mode 100644 src/main/java/mobac/program/tiledatawriter/TileImageJpegDataWriter.java create mode 100644 src/main/java/mobac/program/tiledatawriter/TileImagePng4DataWriter.java create mode 100644 src/main/java/mobac/program/tiledatawriter/TileImagePng8DataWriter.java create mode 100644 src/main/java/mobac/program/tiledatawriter/TileImagePngDataWriter.java create mode 100644 src/main/java/mobac/program/tilefilter/DummyTileFilter.java create mode 100644 src/main/java/mobac/program/tilefilter/PolygonTileFilter.java create mode 100644 src/main/java/mobac/program/tilestore/TileStore.java create mode 100644 src/main/java/mobac/program/tilestore/TileStoreEntry.java create mode 100644 src/main/java/mobac/program/tilestore/TileStoreInfo.java create mode 100644 src/main/java/mobac/program/tilestore/berkeleydb/BerkeleyDbTileStore.java create mode 100644 src/main/java/mobac/program/tilestore/berkeleydb/DelayedInterruptThread.java create mode 100644 src/main/java/mobac/program/tilestore/berkeleydb/TileDbEntry.java create mode 100644 src/main/java/mobac/utilities/Charsets.java create mode 100644 src/main/java/mobac/utilities/ExtensionClassLoader.java create mode 100644 src/main/java/mobac/utilities/GBC.java create mode 100644 src/main/java/mobac/utilities/GBCTable.java create mode 100644 src/main/java/mobac/utilities/GUIExceptionHandler.java create mode 100644 src/main/java/mobac/utilities/I18nUtils.java create mode 100644 src/main/java/mobac/utilities/Juli2Log4jHandler.java create mode 100644 src/main/java/mobac/utilities/MapDataFileParser.java create mode 100644 src/main/java/mobac/utilities/MyMath.java create mode 100644 src/main/java/mobac/utilities/OSUtilities.java create mode 100644 src/main/java/mobac/utilities/Utilities.java create mode 100644 src/main/java/mobac/utilities/beanshell/Tools.java create mode 100644 src/main/java/mobac/utilities/collections/SoftHashMap.java create mode 100644 src/main/java/mobac/utilities/debug/MySocketImplFactory.java create mode 100644 src/main/java/mobac/utilities/file/DeleteFileFilter.java create mode 100644 src/main/java/mobac/utilities/file/DirInfoFileFilter.java create mode 100644 src/main/java/mobac/utilities/file/DirectoryFileFilter.java create mode 100644 src/main/java/mobac/utilities/file/FileExtFilter.java create mode 100644 src/main/java/mobac/utilities/file/GpxFileFilter.java create mode 100644 src/main/java/mobac/utilities/file/NamePatternFileFilter.java create mode 100644 src/main/java/mobac/utilities/file/RegexFileFilter.java create mode 100644 src/main/java/mobac/utilities/geo/CoordinateDm2Format.java create mode 100644 src/main/java/mobac/utilities/geo/CoordinateDms2Format.java create mode 100644 src/main/java/mobac/utilities/geo/CoordinateTileFormat.java create mode 100644 src/main/java/mobac/utilities/geo/GeoUtils.java create mode 100644 src/main/java/mobac/utilities/imageio/Png4BitWriter.java create mode 100644 src/main/java/mobac/utilities/imageio/PngChunk.java create mode 100644 src/main/java/mobac/utilities/imageio/PngConstants.java create mode 100644 src/main/java/mobac/utilities/imageio/PngXxlWriter.java create mode 100644 src/main/java/mobac/utilities/jdbc/DriverProxy.java create mode 100644 src/main/java/mobac/utilities/jdbc/SQLiteLoader.java create mode 100644 src/main/java/mobac/utilities/stream/ArrayOutputStream.java create mode 100644 src/main/java/mobac/utilities/stream/CountingOutputStream.java create mode 100644 src/main/java/mobac/utilities/stream/LittleEndianOutputStream.java create mode 100644 src/main/java/mobac/utilities/stream/RandomAccessFileOutputStream.java create mode 100644 src/main/java/mobac/utilities/stream/ThrottleSupport.java create mode 100644 src/main/java/mobac/utilities/stream/ThrottledInputStream.java create mode 100644 src/main/java/mobac/utilities/stream/ZipStoreOutputStream.java create mode 100644 src/main/java/mobac/utilities/tar/TarArchive.java create mode 100644 src/main/java/mobac/utilities/tar/TarHeader.java create mode 100644 src/main/java/mobac/utilities/tar/TarIndex.java create mode 100644 src/main/java/mobac/utilities/tar/TarIndexTable.java create mode 100644 src/main/java/mobac/utilities/tar/TarIndexedArchive.java create mode 100644 src/main/java/mobac/utilities/tar/TarRecord.java create mode 100644 src/main/java/mobac/utilities/tar/TarTmiArchive.java create mode 100644 src/main/java/mobac/utilities/writer/NullPrintWriter.java create mode 100644 src/main/java/mobac/utilities/writer/NullWriter.java create mode 100644 src/main/resources/mobac/mobac-rev.properties create mode 100644 src/main/resources/mobac/mobac.properties create mode 100644 src/main/resources/mobac/resources/cert/MapPack.cer create mode 100644 src/main/resources/mobac/resources/images/Splash.jpg create mode 100644 src/main/resources/mobac/resources/images/ajax-loader.gif create mode 100644 src/main/resources/mobac/resources/images/arrow_blue_down.png create mode 100644 src/main/resources/mobac/resources/images/arrow_blue_left.png create mode 100644 src/main/resources/mobac/resources/images/arrow_blue_right.png create mode 100644 src/main/resources/mobac/resources/images/arrow_blue_up.png create mode 100644 src/main/resources/mobac/resources/images/arrow_closed.png create mode 100644 src/main/resources/mobac/resources/images/arrow_open.png create mode 100644 src/main/resources/mobac/resources/images/atlas.png create mode 100644 src/main/resources/mobac/resources/images/compass.png create mode 100644 src/main/resources/mobac/resources/images/error.png create mode 100644 src/main/resources/mobac/resources/images/hourglass.png create mode 100644 src/main/resources/mobac/resources/images/layer.png create mode 100644 src/main/resources/mobac/resources/images/map.png create mode 100644 src/main/resources/mobac/resources/images/minus.png create mode 100644 src/main/resources/mobac/resources/images/mobac16.png create mode 100644 src/main/resources/mobac/resources/images/mobac32.png create mode 100644 src/main/resources/mobac/resources/images/mobac48.png create mode 100644 src/main/resources/mobac/resources/images/plus.png create mode 100644 src/main/resources/mobac/resources/images/refresh.png create mode 100644 src/main/resources/mobac/resources/images/trash.png create mode 100644 src/main/resources/mobac/resources/text/agpl.txt create mode 100644 src/main/resources/mobac/resources/text/apache-2.0.txt create mode 100644 src/main/resources/mobac/resources/text/gpl.txt create mode 100644 src/main/resources/mobac/resources/text/help_dialog.html create mode 100644 src/main/resources/mobac/resources/text/help_dialog_fr.html create mode 100644 src/main/resources/mobac/resources/text/help_dialog_ja.html create mode 100644 src/main/resources/mobac/resources/text/help_dialog_zh.html create mode 100644 src/main/resources/mobac/resources/text/lgpl-3.0.txt create mode 100644 src/main/resources/mobac/resources/text/license-dbd-je.txt create mode 100644 src/main/resources/mobac/resources/text/localize.properties create mode 100644 src/main/resources/mobac/resources/text/localize_fr_FR.properties create mode 100644 src/main/resources/mobac/resources/text/localize_ja_JP.properties create mode 100644 src/main/resources/mobac/resources/text/localize_zh_CN.properties create mode 100644 src/main/resources/mobac/resources/text/localize_zh_TW.properties create mode 100644 src/main/resources/mobac/resources/xsl/gpx10to11.xsl create mode 100644 src/test/java/featuretests/SimpleHttpClient.java create mode 100644 src/test/java/featuretests/SimpleServer.java create mode 100644 src/test/java/featuretests/TileCoverageTest.java create mode 100644 src/test/java/mapsources/MapSourceTestCase.java create mode 100644 src/test/java/mapsources/MapSourceTestFailedException.java create mode 100644 src/test/java/mapsources/MapSourcesTestSuite.java create mode 100644 src/test/java/mobac/tools/Cities.java create mode 100644 src/test/java/mobac/tools/MapPackUploadSelector.java create mode 100644 src/test/java/mobac/tools/MapSourceCapabilityDetector.java create mode 100644 src/test/java/mobac/tools/MapSourceCapabilityGUI.java create mode 100644 src/test/java/mobac/tools/MapUpdateTypeLister.java create mode 100644 src/test/java/mobac/tools/testtileserver/TestTileServer.java create mode 100644 src/test/java/mobac/tools/testtileserver/servlets/AbstractTileGeneratorServlet.java create mode 100644 src/test/java/mobac/tools/testtileserver/servlets/AbstractTileServlet.java create mode 100644 src/test/java/mobac/tools/testtileserver/servlets/JpgTileGeneratorServlet.java create mode 100644 src/test/java/mobac/tools/testtileserver/servlets/PngFileTileServlet.java create mode 100644 src/test/java/mobac/tools/testtileserver/servlets/PngTileGeneratorServlet.java create mode 100644 src/test/java/mobac/tools/testtileserver/servlets/ShutdownServlet.java create mode 100644 src/test/java/unittests/AbstractAtlasCreatorTestCase.java create mode 100644 src/test/java/unittests/AtlasDownloadTestCase.java create mode 100644 src/test/java/unittests/CordinateTestCase.java create mode 100644 src/test/java/unittests/KMZTestCase.java create mode 100644 src/test/java/unittests/SQLiteTestCase.java create mode 100644 src/test/java/unittests/helper/DummyAtlasCreator.java create mode 100644 src/test/java/unittests/helper/DummyTileStore.java create mode 100644 src/test/java/unittests/helper/TestMapSourcesManager.java create mode 100644 src/test/java/unittests/methods/MyMathTests.java create mode 100644 src/test/java/unittests/methods/UtilitiesTests.java create mode 100644 src/test/resources/mobac/tools/testtileserver/servlets/images/cross.png create mode 100644 src/test/resources/mobac/tools/testtileserver/servlets/images/gradient.png create mode 100644 src/test/resources/mobac/tools/testtileserver/servlets/images/tile.png create mode 100644 start.sh diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..a09252c --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,4 @@ +increased tile limit to 1 million +added the default open street map server +included sqlite driver by default +removed some broken mapsources diff --git a/LICENSE b/LICENSE deleted file mode 100644 index fb9886c..0000000 --- a/LICENSE +++ /dev/null @@ -1,251 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - -Version 1, February 1989 - -Copyright (C) 1989 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, -Boston, MA 02110-1301 USA - -Everyone is permitted to copy and distribute verbatim copies of this license -document, but changing it is not allowed. - -Preamble - -The license agreements of most software companies try to keep users at the -mercy of those companies. By contrast, our General Public License is intended -to guarantee your freedom to share and change free software--to make sure -the software is free for all its users. The General Public License applies -to the Free Software Foundation's software and to any other program whose -authors commit to using it. You can use it for your programs, too. - -When we speak of free software, we are referring to freedom, not price. Specifically, -the General Public License is designed to make sure that you have the freedom -to give away or sell copies of free software, that you receive source code -or can get it if you want it, that you can change the software or use pieces -of it in new free programs; and that you know you can do these things. - -To protect your rights, we need to make restrictions that forbid anyone to -deny you these rights or to ask you to surrender the rights. These restrictions -translate to certain responsibilities for you if you distribute copies of -the software, or if you modify it. - -For example, if you distribute copies of a such a program, whether gratis -or for a fee, you must give the recipients all the rights that you have. You -must make sure that they, too, receive or can get the source code. And you -must tell them their rights. - -We protect your rights with two steps: (1) copyright the software, and (2) -offer you this license which gives you legal permission to copy, distribute -and/or modify the software. - -Also, for each author's protection and ours, we want to make certain that -everyone understands that there is no warranty for this free software. If -the software is modified by someone else and passed on, we want its recipients -to know that what they have is not the original, so that any problems introduced -by others will not reflect on the original authors' reputations. - -The precise terms and conditions for copying, distribution and modification -follow. - -GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION -AND MODIFICATION - - - -0. This License Agreement applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed under -the terms of this General Public License. The "Program", below, refers to -any such program or work, and a "work based on the Program" means either the -Program or any work containing the Program or a portion of it, either verbatim -or with modifications. Each licensee is addressed as "you". - - - -1. You may copy and distribute verbatim copies of the Program's source code -as you receive it, in any medium, provided that you conspicuously and appropriately -publish on each copy an appropriate copyright notice and disclaimer of warranty; -keep intact all the notices that refer to this General Public License and -to the absence of any warranty; and give any other recipients of the Program -a copy of this General Public License along with the Program. You may charge -a fee for the physical act of transferring a copy. - - - -2. You may modify your copy or copies of the Program or any portion of it, -and copy and distribute such modifications under the terms of Paragraph 1 -above, provided that you also do the following: - -a) cause the modified files to carry prominent notices stating that you changed -the files and the date of any change; and - -b) cause the whole of any work that you distribute or publish, that in whole -or in part contains the Program or any part thereof, either with or without -modifications, to be licensed at no charge to all third parties under the -terms of this General Public License (except that you may choose to grant -warranty protection to some or all third parties, at your option). - -c) If the modified program normally reads commands interactively when run, -you must cause it, when started running for such interactive use in the simplest -and most usual way, to print or display an announcement including an appropriate -copyright notice and a notice that there is no warranty (or else, saying that -you provide a warranty) and that users may redistribute the program under -these conditions, and telling the user how to view a copy of this General -Public License. - -d) You may charge a fee for the physical act of transferring a copy, and you -may at your option offer warranty protection in exchange for a fee. - -Mere aggregation of another independent work with the Program (or its derivative) -on a volume of a storage or distribution medium does not bring the other work -under the scope of these terms. - - - -3. You may copy and distribute the Program (or a portion or derivative of -it, under Paragraph 2) in object code or executable form under the terms of -Paragraphs 1 and 2 above provided that you also do one of the following: - -a) accompany it with the complete corresponding machine-readable source code, -which must be distributed under the terms of Paragraphs 1 and 2 above; or, - -b) accompany it with a written offer, valid for at least three years, to give -any third party free (except for a nominal charge for the cost of distribution) -a complete machine-readable copy of the corresponding source code, to be distributed -under the terms of Paragraphs 1 and 2 above; or, - -c) accompany it with the information you received as to where the corresponding -source code may be obtained. (This alternative is allowed only for noncommercial -distribution and only if you received the program in object code or executable -form alone.) - -Source code for a work means the preferred form of the work for making modifications -to it. For an executable file, complete source code means all the source code -for all modules it contains; but, as a special exception, it need not include -source code for modules which are standard libraries that accompany the operating -system on which the executable file runs, or for standard header files or -definitions files that accompany that operating system. - - - -4. You may not copy, modify, sublicense, distribute or transfer the Program -except as expressly provided under this General Public License. Any attempt -otherwise to copy, modify, sublicense, distribute or transfer the Program -is void, and will automatically terminate your rights to use the Program under -this License. However, parties who have received copies, or rights to use -copies, from you under this General Public License will not have their licenses -terminated so long as such parties remain in full compliance. - - - -5. By copying, distributing or modifying the Program (or any work based on -the Program) you indicate your acceptance of this license to do so, and all -its terms and conditions. - - - -6. Each time you redistribute the Program (or any work based on the Program), -the recipient automatically receives a license from the original licensor -to copy, distribute or modify the Program subject to these terms and conditions. -You may not impose any further restrictions on the recipients' exercise of -the rights granted herein. - - - -7. The Free Software Foundation may publish revised and/or new versions of -the General Public License from time to time. Such new versions will be similar -in spirit to the present version, but may differ in detail to address new -problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies -a version number of the license which applies to it and "any later version", -you have the option of following the terms and conditions either of that version -or of any later version published by the Free Software Foundation. If the -Program does not specify a version number of the license, you may choose any -version ever published by the Free Software Foundation. - - - -8. If you wish to incorporate parts of the Program into other free programs -whose distribution conditions are different, write to the author to ask for -permission. For software which is copyrighted by the Free Software Foundation, -write to the Free Software Foundation; we sometimes make exceptions for this. -Our decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing and reuse -of software generally. - - - - NO WARRANTY - -9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR -THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE -STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM -"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE -OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - - -10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE -OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA -OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES -OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH -HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -END OF TERMS AND CONDITIONS - -Appendix: How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible -use to humanity, the best way to achieve this is to make it free software -which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach -them to the start of each source file to most effectively convey the exclusion -of warranty; and each file should have at least the "copyright" line and a -pointer to where the full notice is found. - - Copyright -(C) 19yy - -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 1, 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, write to the Free Software Foundation, Inc., 675 Mass -Ave, Cambridge, MA 02139, USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this when -it starts in an interactive mode: - -Gnomovision version 69, Copyright (C) 19xx name of author Gnomovision comes -with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, -and you are welcome to redistribute it under certain conditions; type `show -c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may be -called something other than `show w' and `show c'; they could even be mouse-clicks -or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your school, -if any, to sign a "copyright disclaimer" for the program, if necessary. Here -a sample; alter the names: - -Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' -(a program to direct compilers to make passes at assemblers) written by James -Hacker. - -, 1 April 1989 Ty Coon, President of Vice - -That's all there is to it! diff --git a/Mobile Atlas Creator.exe b/Mobile Atlas Creator.exe new file mode 100644 index 0000000000000000000000000000000000000000..0c8104731e9ab69161c503df2dc338adf76c1bee GIT binary patch literal 40960 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -libraryjars "${rtlibraryjar}" + -injars + "${jar.name}" + -outjars + build/temp.jar + -keepattributes *Annotation* + -keep class ${main.class} { + public static void main(java.lang.String[]); } + -keep class + mobac.program.* { public *; } + -keep class mobac.program.interfaces.* { + public *; } + -keep class mobac.program.model.package-info + -keep class + mobac.program.model.* { public *; } + -keep class mobac.program.jaxb.* { + public *; } + -keep class mobac.program.interfaces.* { public *; } + -keep + class mobac.program.download.UserAgent { public *; } + -keep class + mobac.exceptions.* { public *; } + -keep class + mobac.program.tilestore.berkeleydb.* { *; } + -keep class + mobac.program.tilestore.TileStoreEntry { public *; } + -keep class + mobac.program.atlascreators.AtlasCreator { public *; } + -keep class + mobac.utilities.beanshell.* { public *; } + -keep class + mobac.utilities.tar.TarIndex { public *; } + -keep class + mobac.utilities.tar.TarIndexedArchive { public *; } + -keep class + mobac.data.gpx.gpx11.* { public *; } + -keep class + mobac.mapsources.MapSourceTools { public *; } + -keep class + mobac.mapsources.MapSourceUrlUpdater { public *; } + -keep class + mobac.mapsources.custom.* { *; } + -keep class org.apache.log4j.Layout { + public *; } + -keep class org.apache.log4j.Level { public *; } + -keep + class org.apache.commons.lang3.StringEscapeUtils { public *; } + -keep + class org.apache.log4j.Category { public *; } + -keep class + org.apache.log4j.Priority { public *; } + -keep class + org.apache.log4j.spi.LoggingEvent { public *; } + -keep class + org.apache.log4j.spi.ThrowableInformation { public *; } + -keep class + org.apache.log4j.spi.LocationInfo { public *; } + -keep class + org.apache.log4j.PatternLayout { public *; } + -keep class + org.apache.log4j.Appender { public *; } + -keep class + org.apache.log4j.FileAppender { public *; } + -keep class + com.sixlegs.png.PngImage { public *; } + -keep class bsh.This { public + *; } + -keep class bsh.XThis { public *; } + -keep class + com.sleepycat.persist.PrimaryIndex { public *; } + -keepclassmembers + enum * { + public static **[] values(); + public static ** + valueOf(java.lang.String); + } + -keepclassmembers class * + implements + java.io.Serializable { + static final long serialVersionUID; + private void + writeObject(java.io.ObjectOutputStream); + private void + readObject(java.io.ObjectInputStream); + java.lang.Object + writeReplace(); + java.lang.Object readResolve(); + } + -keepclassmembers + class * implements + com.sleepycat.persist.evolve.Conversion { + boolean + equals(java.lang.Object); + } + -dontnote java.** + -dontnote javax.** + -dontnote com.sun.** + -dontnote sun.** + -dontwarn + -dontoptimize + -dontobfuscate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gpl.txt b/gpl.txt new file mode 100644 index 0000000..fb6319b --- /dev/null +++ b/gpl.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/misc/launch4j.xml b/misc/launch4j.xml new file mode 100644 index 0000000..061fbe9 --- /dev/null +++ b/misc/launch4j.xml @@ -0,0 +1,35 @@ + + true + gui + Mobile_Atlas_Creator.jar + ..\Mobile Atlas Creator.exe + + + + normal + http://java.com/download + + false + false + + mobac.ico + + + 1.6.0_14 + + preferJre + 1024 + + + 1.0.0.0 + - + Mobile Atlas Creator Windows Launcher + MOBAC developers + 1.0.0.0 + - + Mobile Atlas Creator + + Mobile Atlas Creator 1024MB max Heap + Mobile Atlas Creator.exe + + \ No newline at end of file diff --git a/misc/mobac.ico b/misc/mobac.ico new file mode 100644 index 0000000000000000000000000000000000000000..f946ccf5434909fec03ddac31f3742783de1a927 GIT binary patch literal 13806 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 literal 0 HcmV?d00001 diff --git a/settings.xml b/settings.xml new file mode 100644 index 0000000..43ea6d7 --- /dev/null +++ b/settings.xml @@ -0,0 +1,116 @@ + + + + 100 + + 0 + DEG_LOCAL + + + + + false + false + + 1 + 4 + Layer + en + + 10 + 10 + false + + en + + 1900/990 + 1024/54 + true + true + true + + Coordinates + + + 0 + + FreemapSlovakia + FreemapSlovakiaCyclo + FreemapSlovakiaHiking + MoldovaPointMd + OSMapaTopo + OSMapaTopoContours + OpenPisteMapBCL + MapQuest + Mapnik + WanderreitkarteAbo + Turaterkep256 + UMP-pcPL + New Zealand Topographic Maps + + + OSMPublicTransport + OpenSeaMap + OpenStreetMap Hikebikemap.de + OpenStreetMap Standard Tile Layer + USGS National Map Topo + cb-enra + cb-enrh + cb-enrl + cb-sec + cb-tac + cb-wac + USGS National Map Imagery + USGS National Map Imagery Topo + + + "0-52575d5ec7c80" + + + -1 + USGS National Map Topo + 217710592/422510592 + 217710592/422510592 + 2 + 65536 + + + 6 + 15 + 96 + 22.6 + 22.6 + 22.6 + 22.6 + 28.2 + true + true + true + true + A4 + SECOND_1 + + + CUSTOM + + 11 + 12 + + 2543562845 + PNG + 33367857650 + 451300934 + 256/256 + true + Metric + MOBAC/1.9.16 + 1.9.16 + + #0000ff + false + SECOND_1 + false + SansSerif-PLAIN-12 + 1.0 + + diff --git a/src/main/java/mobac/Main.java b/src/main/java/mobac/Main.java new file mode 100644 index 0000000..a906ab5 --- /dev/null +++ b/src/main/java/mobac/Main.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac; + +import javax.imageio.ImageIO; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import mobac.gui.MainGUI; +import mobac.gui.SplashFrame; +import mobac.mapsources.DefaultMapSourcesManager; +import mobac.program.DirectoryManager; +import mobac.program.EnvironmentSetup; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.commandline.CommandLineEmpty; +import mobac.program.commandline.CreateAtlas; +import mobac.program.interfaces.CommandLineAction; +import mobac.program.model.Settings; +import mobac.program.tilestore.TileStore; +import mobac.utilities.GUIExceptionHandler; + +/** + * Java 6 version of the main starter class + */ +public class Main { + + protected CommandLineAction cmdAction = new CommandLineEmpty(); + + public Main() { + try { + parseCommandLine(); + if (cmdAction.showSplashScreen()) + SplashFrame.showFrame(); + + DirectoryManager.initialize(); + Logging.configureLogging(); + + // MySocketImplFactory.install(); + ProgramInfo.initialize(); // Load revision info + Logging.logSystemInfo(); + GUIExceptionHandler.installToolkitEventQueueProxy(); + // Logging.logSystemProperties(); + ImageIO.setUseCache(false); + + EnvironmentSetup.checkFileSetup(); + Settings.loadOrQuit(); + EnvironmentSetup.checkMemory(); + + EnvironmentSetup.copyMapPacks(); + DefaultMapSourcesManager.initialize(); + EnvironmentSetup.createDefaultAtlases(); + TileStore.initialize(); + EnvironmentSetup.upgrade(); + cmdAction.runBeforeMainGUI(); + if (cmdAction.showMainGUI()) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + Logging.LOG.debug("Starting GUI"); + MainGUI.createMainGui(); + SplashFrame.hideFrame(); + cmdAction.runMainGUI(); + } + }); + } + } catch (Throwable t) { + GUIExceptionHandler.processException(t); + System.exit(1); + } + } + + protected void parseCommandLine() { + String[] args = StartMOBAC.ARGS; + if (args.length >= 2) { + if ("create".equalsIgnoreCase(args[0])) { + if (args.length > 2) + cmdAction = new CreateAtlas(args[1], args[2]); + else + cmdAction = new CreateAtlas(args[1]); + return; + } + } + } + + /** + * Start MOBAC without Java Runtime version check + */ + public static void main(String[] args) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + new Main(); + } catch (Throwable t) { + GUIExceptionHandler.processException(t); + } + } +} diff --git a/src/main/java/mobac/StartMOBAC.java b/src/main/java/mobac/StartMOBAC.java new file mode 100644 index 0000000..ac54a27 --- /dev/null +++ b/src/main/java/mobac/StartMOBAC.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac; + +import javax.swing.JOptionPane; +import javax.swing.UIManager; + +import mobac.utilities.I18nUtils; + +/** + * Main class for starting Mobile Atlas Creator. + * + * This class performs the Java Runtime version check and if the correct version is installed it creates a new instance + * of the class specified by {@link #MAIN_CLASS}. The class to be instantiated is specified by it's name intentionally + * as this allows to compile this class without any further class dependencies. + * + */ +public class StartMOBAC { + + public static final String MAIN_CLASS = "mobac.Main"; + + public static String[] ARGS; + + /** + * @param args + */ + public static void main(String[] args) { + ARGS = args; + setLookAndFeel(); + checkVersion(); + try { + Class.forName(MAIN_CLASS).newInstance(); + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(null, I18nUtils.localizedStringForKey("msg_environment_unable_to_start") + e.getMessage(), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.ERROR_MESSAGE); + } + } + + public static void setLookAndFeel() { + try { + if (System.getProperty("swing.defaultlaf") != null) + return; + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + //MainGUI.setDefaultFontOfAllUIComponents(MainGUI.customFont()); + } catch (Exception e) { + } + } + + protected static void checkVersion() { + String ver = System.getProperty("java.specification.version"); + if (ver == null) + ver = "Unknown"; + String[] v = ver.split("\\."); + int major = 0; + int minor = 0; + try { + major = Integer.parseInt(v[0]); + minor = Integer.parseInt(v[1]); + } catch (Exception e) { + } + int version = (major * 1000) + minor; + // 1.5 -> 1005; 1.6 -> 1006; 1.7 -> 1007 + if (version < 1006) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_environment_jre_bellow"), ver), + I18nUtils.localizedStringForKey("msg_environment_jre_bellow_title"), + JOptionPane.ERROR_MESSAGE); + System.exit(1); + } + } + +} diff --git a/src/main/java/mobac/data/gpx/GPXUtils.java b/src/main/java/mobac/data/gpx/GPXUtils.java new file mode 100644 index 0000000..e2b65f7 --- /dev/null +++ b/src/main/java/mobac/data/gpx/GPXUtils.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.data.gpx; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.swing.JOptionPane; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.util.JAXBResult; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamSource; + +import mobac.data.gpx.gpx11.Gpx; +import mobac.program.Logging; +import mobac.utilities.Utilities; + +import org.w3c.dom.Document; + +public class GPXUtils { + + public static boolean checkJAXBVersion() { + boolean res = Utilities.checkJAXBVersion(); + if (!res) + JOptionPane.showMessageDialog(null, + "Outdated Java Runtime Environment and JAXB version", + "Mobile Atlas Creator has detected that your used " + + "Java Runtime Environment is too old.\n Please update " + + "the Java Runtime Environment to at least \nversion " + + "1.6.0_14 and restart Mobile Atlas Creator.", + JOptionPane.ERROR_MESSAGE); + return res; + } + + public static Gpx loadGpxFile(File f) throws JAXBException { + // Create GPX 1.1 JAXB context + JAXBContext context = JAXBContext.newInstance(Gpx.class); + + Unmarshaller unmarshaller = context.createUnmarshaller(); + InputStream is = null; + try { + is = new FileInputStream(f); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder loader = factory.newDocumentBuilder(); + Document document = loader.parse(is); + String namespace = document.getDocumentElement().getNamespaceURI(); + if ("http://www.topografix.com/GPX/1/1".equals(namespace)) { + return (Gpx) unmarshaller.unmarshal(document); + } + if ("http://www.topografix.com/GPX/1/0".equals(namespace)) { + Source xmlSource = new javax.xml.transform.dom.DOMSource(document); + Source xsltSource = new StreamSource(Utilities + .loadResourceAsStream("xsl/gpx10to11.xsl")); + JAXBResult result = new JAXBResult(unmarshaller); + TransformerFactory transFact = TransformerFactory.newInstance(); + Transformer trans = transFact.newTransformer(xsltSource); + trans.transform(xmlSource, result); + return (Gpx) result.getResult(); + } + throw new JAXBException("Expected GPX 1.0 or GPX1.1 namespace but found \n\"" + + namespace + "\""); + } catch (JAXBException e) { + throw e; + } catch (Exception e) { + throw new JAXBException(e); + } finally { + Utilities.closeStream(is); + } + } + + public static void saveGpxFile(Gpx gpx, File f) throws JAXBException { + // Create GPX 1.1 JAXB context + JAXBContext context = JAXBContext.newInstance(Gpx.class); + + Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + OutputStream os = null; + try { + os = new FileOutputStream(f); + marshaller.marshal(gpx, os); + } catch (FileNotFoundException e) { + throw new JAXBException(e); + } finally { + Utilities.closeStream(os); + } + } + + public static void main(String[] args) { + Logging.configureConsoleLogging(); + try { + loadGpxFile(new File("misc/samples/gpx/gpx11 wpt.gpx")); + loadGpxFile(new File("misc/samples/gpx/gpx10 wpt.gpx")); + } catch (JAXBException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/mobac/data/gpx/gpx11/BoundsType.java b/src/main/java/mobac/data/gpx/gpx11/BoundsType.java new file mode 100644 index 0000000..0643be5 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/BoundsType.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.data.gpx.gpx11; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + +/** + * + * Two lat/lon pairs defining the extent of an element. + * + * + *

+ * Java class for boundsType complex type. + * + *

+ * The following schema fragment specifies the expected content contained within + * this class. + * + *

+ * <complexType name="boundsType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <attribute name="minlat" use="required" type="{http://www.topografix.com/GPX/1/1}latitudeType" />
+ *       <attribute name="minlon" use="required" type="{http://www.topografix.com/GPX/1/1}longitudeType" />
+ *       <attribute name="maxlat" use="required" type="{http://www.topografix.com/GPX/1/1}latitudeType" />
+ *       <attribute name="maxlon" use="required" type="{http://www.topografix.com/GPX/1/1}longitudeType" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "boundsType") +public class BoundsType { + + @XmlAttribute(required = true) + protected BigDecimal minlat; + @XmlAttribute(required = true) + protected BigDecimal minlon; + @XmlAttribute(required = true) + protected BigDecimal maxlat; + @XmlAttribute(required = true) + protected BigDecimal maxlon; + + /** + * Gets the value of the minlat property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getMinlat() { + return minlat; + } + + /** + * Sets the value of the minlat property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setMinlat(BigDecimal value) { + this.minlat = value; + } + + /** + * Gets the value of the minlon property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getMinlon() { + return minlon; + } + + /** + * Sets the value of the minlon property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setMinlon(BigDecimal value) { + this.minlon = value; + } + + /** + * Gets the value of the maxlat property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getMaxlat() { + return maxlat; + } + + /** + * Sets the value of the maxlat property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setMaxlat(BigDecimal value) { + this.maxlat = value; + } + + /** + * Gets the value of the maxlon property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getMaxlon() { + return maxlon; + } + + /** + * Sets the value of the maxlon property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setMaxlon(BigDecimal value) { + this.maxlon = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/CopyrightType.java b/src/main/java/mobac/data/gpx/gpx11/CopyrightType.java new file mode 100644 index 0000000..22e8080 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/CopyrightType.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.datatype.XMLGregorianCalendar; + + +/** + * + * Information about the copyright holder and any license governing use of this file. By linking to an appropriate license, + * you may place your data into the public domain or grant additional usage rights. + * + * + *

Java class for copyrightType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="copyrightType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="year" type="{http://www.w3.org/2001/XMLSchema}gYear" minOccurs="0"/>
+ *         <element name="license" type="{http://www.w3.org/2001/XMLSchema}anyURI" minOccurs="0"/>
+ *       </sequence>
+ *       <attribute name="author" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "copyrightType", propOrder = { + "year", + "license" +}) +public class CopyrightType { + + @XmlSchemaType(name = "gYear") + protected XMLGregorianCalendar year; + @XmlSchemaType(name = "anyURI") + protected String license; + @XmlAttribute(required = true) + protected String author; + + /** + * Gets the value of the year property. + * + * @return + * possible object is + * {@link XMLGregorianCalendar } + * + */ + public XMLGregorianCalendar getYear() { + return year; + } + + /** + * Sets the value of the year property. + * + * @param value + * allowed object is + * {@link XMLGregorianCalendar } + * + */ + public void setYear(XMLGregorianCalendar value) { + this.year = value; + } + + /** + * Gets the value of the license property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getLicense() { + return license; + } + + /** + * Sets the value of the license property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setLicense(String value) { + this.license = value; + } + + /** + * Gets the value of the author property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getAuthor() { + return author; + } + + /** + * Sets the value of the author property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setAuthor(String value) { + this.author = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/EmailType.java b/src/main/java/mobac/data/gpx/gpx11/EmailType.java new file mode 100644 index 0000000..63beb34 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/EmailType.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * An email address. Broken into two parts (id and domain) to help prevent email harvesting. + * + * + *

Java class for emailType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="emailType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <attribute name="id" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
+ *       <attribute name="domain" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "emailType") +public class EmailType { + + @XmlAttribute(required = true) + protected String id; + @XmlAttribute(required = true) + protected String domain; + + /** + * Gets the value of the id property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getId() { + return id; + } + + /** + * Sets the value of the id property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setId(String value) { + this.id = value; + } + + /** + * Gets the value of the domain property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getDomain() { + return domain; + } + + /** + * Sets the value of the domain property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setDomain(String value) { + this.domain = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/ExtensionsType.java b/src/main/java/mobac/data/gpx/gpx11/ExtensionsType.java new file mode 100644 index 0000000..6d1b76e --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/ExtensionsType.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAnyElement; +import javax.xml.bind.annotation.XmlType; +import org.w3c.dom.Element; + + +/** + * + * You can add extend GPX by adding your own elements from another schema here. + * + * + *

Java class for extensionsType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="extensionsType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <any/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "extensionsType", propOrder = { + "any" +}) +public class ExtensionsType { + + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the any property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getAny().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + * + * + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/Gpx.java b/src/main/java/mobac/data/gpx/gpx11/Gpx.java new file mode 100644 index 0000000..900db04 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/Gpx.java @@ -0,0 +1,272 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + +package mobac.data.gpx.gpx11; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +import mobac.program.ProgramInfo; + + +/** + * + * GPX documents contain a metadata header, followed by waypoints, routes, and + * tracks. You can add your own elements to the extensions section of the GPX + * document. + * + * + *

+ * Java class for anonymous complex type. + * + *

+ * The following schema fragment specifies the expected content contained within + * this class. + * + *

+ * <complexType>
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="metadata" type="{http://www.topografix.com/GPX/1/1}metadataType" minOccurs="0"/>
+ *         <element name="wpt" type="{http://www.topografix.com/GPX/1/1}wptType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="rte" type="{http://www.topografix.com/GPX/1/1}rteType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="trk" type="{http://www.topografix.com/GPX/1/1}trkType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="extensions" type="{http://www.topografix.com/GPX/1/1}extensionsType" minOccurs="0"/>
+ *       </sequence>
+ *       <attribute name="version" use="required" type="{http://www.w3.org/2001/XMLSchema}string" fixed="1.1" />
+ *       <attribute name="creator" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { "metadata", "wpt", "rte", "trk", "extensions" }) +@XmlRootElement(name = "gpx") +public class Gpx { + + protected MetadataType metadata; + protected List wpt; + protected List rte; + protected List trk; + protected ExtensionsType extensions; + @XmlAttribute(required = true) + protected String version; + @XmlAttribute(required = true) + protected String creator; + + public static Gpx createGpx() { + Gpx gpx = new Gpx(); + gpx.setVersion("1.1"); + gpx.setCreator(ProgramInfo.getVersionTitle()); + return gpx; + } + + protected Gpx() { + } + + /** + * Gets the value of the metadata property. + * + * @return possible object is {@link MetadataType } + * + */ + public MetadataType getMetadata() { + return metadata; + } + + /** + * Sets the value of the metadata property. + * + * @param value + * allowed object is {@link MetadataType } + * + */ + public void setMetadata(MetadataType value) { + this.metadata = value; + } + + /** + * Gets the value of the wpt property. + * + *

+ * This accessor method returns a reference to the live list, not a + * snapshot. Therefore any modification you make to the returned list will + * be present inside the JAXB object. This is why there is not a + * set method for the wpt property. + * + *

+ * For example, to add a new item, do as follows: + * + *

+	 * getWpt().add(newItem);
+	 * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list {@link WptType } + * + * + */ + public List getWpt() { + if (wpt == null) { + wpt = new ArrayList(); + } + return this.wpt; + } + + /** + * Gets the value of the rte property. + * + *

+ * This accessor method returns a reference to the live list, not a + * snapshot. Therefore any modification you make to the returned list will + * be present inside the JAXB object. This is why there is not a + * set method for the rte property. + * + *

+ * For example, to add a new item, do as follows: + * + *

+	 * getRte().add(newItem);
+	 * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list {@link RteType } + * + * + */ + public List getRte() { + if (rte == null) { + rte = new ArrayList(); + } + return this.rte; + } + + /** + * Gets the value of the trk property. + * + *

+ * This accessor method returns a reference to the live list, not a + * snapshot. Therefore any modification you make to the returned list will + * be present inside the JAXB object. This is why there is not a + * set method for the trk property. + * + *

+ * For example, to add a new item, do as follows: + * + *

+	 * getTrk().add(newItem);
+	 * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list {@link TrkType } + * + * + */ + public List getTrk() { + if (trk == null) { + trk = new ArrayList(); + } + return this.trk; + } + + /** + * Gets the value of the extensions property. + * + * @return possible object is {@link ExtensionsType } + * + */ + public ExtensionsType getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value + * allowed object is {@link ExtensionsType } + * + */ + public void setExtensions(ExtensionsType value) { + this.extensions = value; + } + + /** + * Gets the value of the version property. + * + * @return possible object is {@link String } + * + */ + public String getVersion() { + if (version == null) { + return "1.1"; + } else { + return version; + } + } + + /** + * Sets the value of the version property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setVersion(String value) { + this.version = value; + } + + /** + * Gets the value of the creator property. + * + * @return possible object is {@link String } + * + */ + public String getCreator() { + return creator; + } + + /** + * Sets the value of the creator property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setCreator(String value) { + this.creator = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/LinkType.java b/src/main/java/mobac/data/gpx/gpx11/LinkType.java new file mode 100644 index 0000000..41255e0 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/LinkType.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * A link to an external resource (Web page, digital photo, video clip, etc) with additional information. + * + * + *

Java class for linkType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="linkType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="text" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="type" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *       </sequence>
+ *       <attribute name="href" use="required" type="{http://www.w3.org/2001/XMLSchema}anyURI" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "linkType", propOrder = { + "text", + "type" +}) +public class LinkType { + + protected String text; + protected String type; + @XmlAttribute(required = true) + @XmlSchemaType(name = "anyURI") + protected String href; + + /** + * Gets the value of the text property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getText() { + return text; + } + + /** + * Sets the value of the text property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setText(String value) { + this.text = value; + } + + /** + * Gets the value of the type property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getType() { + return type; + } + + /** + * Sets the value of the type property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setType(String value) { + this.type = value; + } + + /** + * Gets the value of the href property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getHref() { + return href; + } + + /** + * Sets the value of the href property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setHref(String value) { + this.href = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/MetadataType.java b/src/main/java/mobac/data/gpx/gpx11/MetadataType.java new file mode 100644 index 0000000..6cf5c7f --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/MetadataType.java @@ -0,0 +1,314 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.datatype.XMLGregorianCalendar; + + + +/** + * + * Information about the GPX file, author, and copyright restrictions goes in the metadata section. Providing rich, + * meaningful information about your GPX files allows others to search for and use your GPS data. + * + * + *

Java class for metadataType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="metadataType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="name" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="desc" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="author" type="{http://www.topografix.com/GPX/1/1}personType" minOccurs="0"/>
+ *         <element name="copyright" type="{http://www.topografix.com/GPX/1/1}copyrightType" minOccurs="0"/>
+ *         <element name="link" type="{http://www.topografix.com/GPX/1/1}linkType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="time" type="{http://www.w3.org/2001/XMLSchema}dateTime" minOccurs="0"/>
+ *         <element name="keywords" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="bounds" type="{http://www.topografix.com/GPX/1/1}boundsType" minOccurs="0"/>
+ *         <element name="extensions" type="{http://www.topografix.com/GPX/1/1}extensionsType" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "metadataType", propOrder = { + "name", + "desc", + "author", + "copyright", + "link", + "time", + "keywords", + "bounds", + "extensions" +}) +public class MetadataType { + + protected String name; + protected String desc; + protected PersonType author; + protected CopyrightType copyright; + protected List link; + protected XMLGregorianCalendar time; + protected String keywords; + protected BoundsType bounds; + protected ExtensionsType extensions; + + /** + * Gets the value of the name property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the desc property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getDesc() { + return desc; + } + + /** + * Sets the value of the desc property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setDesc(String value) { + this.desc = value; + } + + /** + * Gets the value of the author property. + * + * @return + * possible object is + * {@link PersonType } + * + */ + public PersonType getAuthor() { + return author; + } + + /** + * Sets the value of the author property. + * + * @param value + * allowed object is + * {@link PersonType } + * + */ + public void setAuthor(PersonType value) { + this.author = value; + } + + /** + * Gets the value of the copyright property. + * + * @return + * possible object is + * {@link CopyrightType } + * + */ + public CopyrightType getCopyright() { + return copyright; + } + + /** + * Sets the value of the copyright property. + * + * @param value + * allowed object is + * {@link CopyrightType } + * + */ + public void setCopyright(CopyrightType value) { + this.copyright = value; + } + + /** + * Gets the value of the link property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the link property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getLink().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link LinkType } + * + * + */ + public List getLink() { + if (link == null) { + link = new ArrayList(); + } + return this.link; + } + + /** + * Gets the value of the time property. + * + * @return + * possible object is + * {@link XMLGregorianCalendar } + * + */ + public XMLGregorianCalendar getTime() { + return time; + } + + /** + * Sets the value of the time property. + * + * @param value + * allowed object is + * {@link XMLGregorianCalendar } + * + */ + public void setTime(XMLGregorianCalendar value) { + this.time = value; + } + + /** + * Gets the value of the keywords property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getKeywords() { + return keywords; + } + + /** + * Sets the value of the keywords property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setKeywords(String value) { + this.keywords = value; + } + + /** + * Gets the value of the bounds property. + * + * @return + * possible object is + * {@link BoundsType } + * + */ + public BoundsType getBounds() { + return bounds; + } + + /** + * Sets the value of the bounds property. + * + * @param value + * allowed object is + * {@link BoundsType } + * + */ + public void setBounds(BoundsType value) { + this.bounds = value; + } + + /** + * Gets the value of the extensions property. + * + * @return + * possible object is + * {@link ExtensionsType } + * + */ + public ExtensionsType getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value + * allowed object is + * {@link ExtensionsType } + * + */ + public void setExtensions(ExtensionsType value) { + this.extensions = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/PersonType.java b/src/main/java/mobac/data/gpx/gpx11/PersonType.java new file mode 100644 index 0000000..206cd74 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/PersonType.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * A person or organization. + * + * + *

Java class for personType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="personType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="name" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="email" type="{http://www.topografix.com/GPX/1/1}emailType" minOccurs="0"/>
+ *         <element name="link" type="{http://www.topografix.com/GPX/1/1}linkType" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "personType", propOrder = { + "name", + "email", + "link" +}) +public class PersonType { + + protected String name; + protected EmailType email; + protected LinkType link; + + /** + * Gets the value of the name property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the email property. + * + * @return + * possible object is + * {@link EmailType } + * + */ + public EmailType getEmail() { + return email; + } + + /** + * Sets the value of the email property. + * + * @param value + * allowed object is + * {@link EmailType } + * + */ + public void setEmail(EmailType value) { + this.email = value; + } + + /** + * Gets the value of the link property. + * + * @return + * possible object is + * {@link LinkType } + * + */ + public LinkType getLink() { + return link; + } + + /** + * Sets the value of the link property. + * + * @param value + * allowed object is + * {@link LinkType } + * + */ + public void setLink(LinkType value) { + this.link = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/PtType.java b/src/main/java/mobac/data/gpx/gpx11/PtType.java new file mode 100644 index 0000000..2f27b36 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/PtType.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + +package mobac.data.gpx.gpx11; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; +import javax.xml.datatype.XMLGregorianCalendar; + +import mobac.data.gpx.interfaces.GpxPoint; + +/** + * + * A geographic point with optional elevation and time. Available for use by + * other schemas. + * + * + *

+ * Java class for ptType complex type. + * + *

+ * The following schema fragment specifies the expected content contained within + * this class. + * + *

+ * <complexType name="ptType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="ele" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="time" type="{http://www.w3.org/2001/XMLSchema}dateTime" minOccurs="0"/>
+ *       </sequence>
+ *       <attribute name="lat" use="required" type="{http://www.topografix.com/GPX/1/1}latitudeType" />
+ *       <attribute name="lon" use="required" type="{http://www.topografix.com/GPX/1/1}longitudeType" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ptType", propOrder = { "ele", "time" }) +public class PtType implements GpxPoint { + + protected BigDecimal ele; + protected XMLGregorianCalendar time; + @XmlAttribute(required = true) + protected BigDecimal lat; + @XmlAttribute(required = true) + protected BigDecimal lon; + + /** + * Gets the value of the ele property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getEle() { + return ele; + } + + /** + * Sets the value of the ele property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setEle(BigDecimal value) { + this.ele = value; + } + + /** + * Gets the value of the time property. + * + * @return possible object is {@link XMLGregorianCalendar } + * + */ + public XMLGregorianCalendar getTime() { + return time; + } + + /** + * Sets the value of the time property. + * + * @param value + * allowed object is {@link XMLGregorianCalendar } + * + */ + public void setTime(XMLGregorianCalendar value) { + this.time = value; + } + + /** + * Gets the value of the lat property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getLat() { + return lat; + } + + /** + * Sets the value of the lat property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setLat(BigDecimal value) { + this.lat = value; + } + + /** + * Gets the value of the lon property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getLon() { + return lon; + } + + /** + * Sets the value of the lon property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setLon(BigDecimal value) { + this.lon = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/PtsegType.java b/src/main/java/mobac/data/gpx/gpx11/PtsegType.java new file mode 100644 index 0000000..b4e52bf --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/PtsegType.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * An ordered sequence of points. (for polygons or polylines, e.g.) + * + * + *

Java class for ptsegType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="ptsegType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="pt" type="{http://www.topografix.com/GPX/1/1}ptType" maxOccurs="unbounded" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ptsegType", propOrder = { + "pt" +}) +public class PtsegType { + + protected List pt; + + /** + * Gets the value of the pt property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the pt property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getPt().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link PtType } + * + * + */ + public List getPt() { + if (pt == null) { + pt = new ArrayList(); + } + return this.pt; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/RteType.java b/src/main/java/mobac/data/gpx/gpx11/RteType.java new file mode 100644 index 0000000..fab2adb --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/RteType.java @@ -0,0 +1,318 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination. + * + * + *

Java class for rteType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="rteType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="name" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="cmt" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="desc" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="src" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="link" type="{http://www.topografix.com/GPX/1/1}linkType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="number" type="{http://www.w3.org/2001/XMLSchema}nonNegativeInteger" minOccurs="0"/>
+ *         <element name="type" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="extensions" type="{http://www.topografix.com/GPX/1/1}extensionsType" minOccurs="0"/>
+ *         <element name="rtept" type="{http://www.topografix.com/GPX/1/1}wptType" maxOccurs="unbounded" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "rteType", propOrder = { + "name", + "cmt", + "desc", + "src", + "link", + "number", + "type", + "extensions", + "rtept" +}) +public class RteType { + + protected String name; + protected String cmt; + protected String desc; + protected String src; + protected List link; + @XmlSchemaType(name = "nonNegativeInteger") + protected BigInteger number; + protected String type; + protected ExtensionsType extensions; + protected List rtept; + + /** + * Gets the value of the name property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the cmt property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getCmt() { + return cmt; + } + + /** + * Sets the value of the cmt property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setCmt(String value) { + this.cmt = value; + } + + /** + * Gets the value of the desc property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getDesc() { + return desc; + } + + /** + * Sets the value of the desc property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setDesc(String value) { + this.desc = value; + } + + /** + * Gets the value of the src property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getSrc() { + return src; + } + + /** + * Sets the value of the src property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setSrc(String value) { + this.src = value; + } + + /** + * Gets the value of the link property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the link property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getLink().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link LinkType } + * + * + */ + public List getLink() { + if (link == null) { + link = new ArrayList(); + } + return this.link; + } + + /** + * Gets the value of the number property. + * + * @return + * possible object is + * {@link BigInteger } + * + */ + public BigInteger getNumber() { + return number; + } + + /** + * Sets the value of the number property. + * + * @param value + * allowed object is + * {@link BigInteger } + * + */ + public void setNumber(BigInteger value) { + this.number = value; + } + + /** + * Gets the value of the type property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getType() { + return type; + } + + /** + * Sets the value of the type property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setType(String value) { + this.type = value; + } + + /** + * Gets the value of the extensions property. + * + * @return + * possible object is + * {@link ExtensionsType } + * + */ + public ExtensionsType getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value + * allowed object is + * {@link ExtensionsType } + * + */ + public void setExtensions(ExtensionsType value) { + this.extensions = value; + } + + /** + * Gets the value of the rtept property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the rtept property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getRtept().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link WptType } + * + * + */ + public List getRtept() { + if (rtept == null) { + rtept = new ArrayList(); + } + return this.rtept; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/TrkType.java b/src/main/java/mobac/data/gpx/gpx11/TrkType.java new file mode 100644 index 0000000..b50dec2 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/TrkType.java @@ -0,0 +1,318 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * trk represents a track - an ordered list of points describing a path. + * + * + *

Java class for trkType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="trkType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="name" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="cmt" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="desc" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="src" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="link" type="{http://www.topografix.com/GPX/1/1}linkType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="number" type="{http://www.w3.org/2001/XMLSchema}nonNegativeInteger" minOccurs="0"/>
+ *         <element name="type" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="extensions" type="{http://www.topografix.com/GPX/1/1}extensionsType" minOccurs="0"/>
+ *         <element name="trkseg" type="{http://www.topografix.com/GPX/1/1}trksegType" maxOccurs="unbounded" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "trkType", propOrder = { + "name", + "cmt", + "desc", + "src", + "link", + "number", + "type", + "extensions", + "trkseg" +}) +public class TrkType { + + protected String name; + protected String cmt; + protected String desc; + protected String src; + protected List link; + @XmlSchemaType(name = "nonNegativeInteger") + protected BigInteger number; + protected String type; + protected ExtensionsType extensions; + protected List trkseg; + + /** + * Gets the value of the name property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the cmt property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getCmt() { + return cmt; + } + + /** + * Sets the value of the cmt property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setCmt(String value) { + this.cmt = value; + } + + /** + * Gets the value of the desc property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getDesc() { + return desc; + } + + /** + * Sets the value of the desc property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setDesc(String value) { + this.desc = value; + } + + /** + * Gets the value of the src property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getSrc() { + return src; + } + + /** + * Sets the value of the src property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setSrc(String value) { + this.src = value; + } + + /** + * Gets the value of the link property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the link property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getLink().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link LinkType } + * + * + */ + public List getLink() { + if (link == null) { + link = new ArrayList(); + } + return this.link; + } + + /** + * Gets the value of the number property. + * + * @return + * possible object is + * {@link BigInteger } + * + */ + public BigInteger getNumber() { + return number; + } + + /** + * Sets the value of the number property. + * + * @param value + * allowed object is + * {@link BigInteger } + * + */ + public void setNumber(BigInteger value) { + this.number = value; + } + + /** + * Gets the value of the type property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getType() { + return type; + } + + /** + * Sets the value of the type property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setType(String value) { + this.type = value; + } + + /** + * Gets the value of the extensions property. + * + * @return + * possible object is + * {@link ExtensionsType } + * + */ + public ExtensionsType getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value + * allowed object is + * {@link ExtensionsType } + * + */ + public void setExtensions(ExtensionsType value) { + this.extensions = value; + } + + /** + * Gets the value of the trkseg property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the trkseg property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getTrkseg().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link TrksegType } + * + * + */ + public List getTrkseg() { + if (trkseg == null) { + trkseg = new ArrayList(); + } + return this.trkseg; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/TrksegType.java b/src/main/java/mobac/data/gpx/gpx11/TrksegType.java new file mode 100644 index 0000000..6884053 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/TrksegType.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + + +package mobac.data.gpx.gpx11; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + + +/** + * + * A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. + * + * + *

Java class for trksegType complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType name="trksegType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="trkpt" type="{http://www.topografix.com/GPX/1/1}wptType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="extensions" type="{http://www.topografix.com/GPX/1/1}extensionsType" minOccurs="0"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "trksegType", propOrder = { + "trkpt", + "extensions" +}) +public class TrksegType { + + protected List trkpt; + protected ExtensionsType extensions; + + /** + * Gets the value of the trkpt property. + * + *

+ * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the trkpt property. + * + *

+ * For example, to add a new item, do as follows: + *

+     *    getTrkpt().add(newItem);
+     * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list + * {@link WptType } + * + * + */ + public List getTrkpt() { + if (trkpt == null) { + trkpt = new ArrayList(); + } + return this.trkpt; + } + + /** + * Gets the value of the extensions property. + * + * @return + * possible object is + * {@link ExtensionsType } + * + */ + public ExtensionsType getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value + * allowed object is + * {@link ExtensionsType } + * + */ + public void setExtensions(ExtensionsType value) { + this.extensions = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/WptType.java b/src/main/java/mobac/data/gpx/gpx11/WptType.java new file mode 100644 index 0000000..8ed9f07 --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/WptType.java @@ -0,0 +1,566 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + +package mobac.data.gpx.gpx11; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.datatype.XMLGregorianCalendar; + +import mobac.data.gpx.interfaces.GpxPoint; + +/** + * + * wpt represents a waypoint, point of interest, or named feature on a map. + * + * + *

+ * Java class for wptType complex type. + * + *

+ * The following schema fragment specifies the expected content contained within + * this class. + * + *

+ * <complexType name="wptType">
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element name="ele" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="time" type="{http://www.w3.org/2001/XMLSchema}dateTime" minOccurs="0"/>
+ *         <element name="magvar" type="{http://www.topografix.com/GPX/1/1}degreesType" minOccurs="0"/>
+ *         <element name="geoidheight" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="name" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="cmt" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="desc" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="src" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="link" type="{http://www.topografix.com/GPX/1/1}linkType" maxOccurs="unbounded" minOccurs="0"/>
+ *         <element name="sym" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="type" type="{http://www.w3.org/2001/XMLSchema}string" minOccurs="0"/>
+ *         <element name="fix" type="{http://www.topografix.com/GPX/1/1}fixType" minOccurs="0"/>
+ *         <element name="sat" type="{http://www.w3.org/2001/XMLSchema}nonNegativeInteger" minOccurs="0"/>
+ *         <element name="hdop" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="vdop" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="pdop" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="ageofdgpsdata" type="{http://www.w3.org/2001/XMLSchema}decimal" minOccurs="0"/>
+ *         <element name="dgpsid" type="{http://www.topografix.com/GPX/1/1}dgpsStationType" minOccurs="0"/>
+ *         <element name="extensions" type="{http://www.topografix.com/GPX/1/1}extensionsType" minOccurs="0"/>
+ *       </sequence>
+ *       <attribute name="lat" use="required" type="{http://www.topografix.com/GPX/1/1}latitudeType" />
+ *       <attribute name="lon" use="required" type="{http://www.topografix.com/GPX/1/1}longitudeType" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "wptType", propOrder = { "ele", "time", "magvar", "geoidheight", "name", "cmt", + "desc", "src", "link", "sym", "type", "fix", "sat", "hdop", "vdop", "pdop", + "ageofdgpsdata", "dgpsid", "extensions" }) +public class WptType implements GpxPoint { + + protected BigDecimal ele; + protected XMLGregorianCalendar time; + protected BigDecimal magvar; + protected BigDecimal geoidheight; + protected String name; + protected String cmt; + protected String desc; + protected String src; + protected List link; + protected String sym; + protected String type; + protected String fix; + @XmlSchemaType(name = "nonNegativeInteger") + protected BigInteger sat; + protected BigDecimal hdop; + protected BigDecimal vdop; + protected BigDecimal pdop; + protected BigDecimal ageofdgpsdata; + protected Integer dgpsid; + protected ExtensionsType extensions; + @XmlAttribute(required = true) + protected BigDecimal lat; + @XmlAttribute(required = true) + protected BigDecimal lon; + + /** + * Gets the value of the ele property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getEle() { + return ele; + } + + /** + * Sets the value of the ele property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setEle(BigDecimal value) { + this.ele = value; + } + + /** + * Gets the value of the time property. + * + * @return possible object is {@link XMLGregorianCalendar } + * + */ + public XMLGregorianCalendar getTime() { + return time; + } + + /** + * Sets the value of the time property. + * + * @param value + * allowed object is {@link XMLGregorianCalendar } + * + */ + public void setTime(XMLGregorianCalendar value) { + this.time = value; + } + + /** + * Gets the value of the magvar property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getMagvar() { + return magvar; + } + + /** + * Sets the value of the magvar property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setMagvar(BigDecimal value) { + this.magvar = value; + } + + /** + * Gets the value of the geoidheight property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getGeoidheight() { + return geoidheight; + } + + /** + * Sets the value of the geoidheight property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setGeoidheight(BigDecimal value) { + this.geoidheight = value; + } + + /** + * Gets the value of the name property. + * + * @return possible object is {@link String } + * + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the cmt property. + * + * @return possible object is {@link String } + * + */ + public String getCmt() { + return cmt; + } + + /** + * Sets the value of the cmt property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setCmt(String value) { + this.cmt = value; + } + + /** + * Gets the value of the desc property. + * + * @return possible object is {@link String } + * + */ + public String getDesc() { + return desc; + } + + /** + * Sets the value of the desc property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setDesc(String value) { + this.desc = value; + } + + /** + * Gets the value of the src property. + * + * @return possible object is {@link String } + * + */ + public String getSrc() { + return src; + } + + /** + * Sets the value of the src property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setSrc(String value) { + this.src = value; + } + + /** + * Gets the value of the link property. + * + *

+ * This accessor method returns a reference to the live list, not a + * snapshot. Therefore any modification you make to the returned list will + * be present inside the JAXB object. This is why there is not a + * set method for the link property. + * + *

+ * For example, to add a new item, do as follows: + * + *

+	 * getLink().add(newItem);
+	 * 
+ * + * + *

+ * Objects of the following type(s) are allowed in the list {@link LinkType } + * + * + */ + public List getLink() { + if (link == null) { + link = new ArrayList(); + } + return this.link; + } + + /** + * Gets the value of the sym property. + * + * @return possible object is {@link String } + * + */ + public String getSym() { + return sym; + } + + /** + * Sets the value of the sym property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setSym(String value) { + this.sym = value; + } + + /** + * Gets the value of the type property. + * + * @return possible object is {@link String } + * + */ + public String getType() { + return type; + } + + /** + * Sets the value of the type property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setType(String value) { + this.type = value; + } + + /** + * Gets the value of the fix property. + * + * @return possible object is {@link String } + * + */ + public String getFix() { + return fix; + } + + /** + * Sets the value of the fix property. + * + * @param value + * allowed object is {@link String } + * + */ + public void setFix(String value) { + this.fix = value; + } + + /** + * Gets the value of the sat property. + * + * @return possible object is {@link BigInteger } + * + */ + public BigInteger getSat() { + return sat; + } + + /** + * Sets the value of the sat property. + * + * @param value + * allowed object is {@link BigInteger } + * + */ + public void setSat(BigInteger value) { + this.sat = value; + } + + /** + * Gets the value of the hdop property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getHdop() { + return hdop; + } + + /** + * Sets the value of the hdop property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setHdop(BigDecimal value) { + this.hdop = value; + } + + /** + * Gets the value of the vdop property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getVdop() { + return vdop; + } + + /** + * Sets the value of the vdop property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setVdop(BigDecimal value) { + this.vdop = value; + } + + /** + * Gets the value of the pdop property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getPdop() { + return pdop; + } + + /** + * Sets the value of the pdop property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setPdop(BigDecimal value) { + this.pdop = value; + } + + /** + * Gets the value of the ageofdgpsdata property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getAgeofdgpsdata() { + return ageofdgpsdata; + } + + /** + * Sets the value of the ageofdgpsdata property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setAgeofdgpsdata(BigDecimal value) { + this.ageofdgpsdata = value; + } + + /** + * Gets the value of the dgpsid property. + * + * @return possible object is {@link Integer } + * + */ + public Integer getDgpsid() { + return dgpsid; + } + + /** + * Sets the value of the dgpsid property. + * + * @param value + * allowed object is {@link Integer } + * + */ + public void setDgpsid(Integer value) { + this.dgpsid = value; + } + + /** + * Gets the value of the extensions property. + * + * @return possible object is {@link ExtensionsType } + * + */ + public ExtensionsType getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value + * allowed object is {@link ExtensionsType } + * + */ + public void setExtensions(ExtensionsType value) { + this.extensions = value; + } + + /** + * Gets the value of the lat property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getLat() { + return lat; + } + + /** + * Sets the value of the lat property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setLat(BigDecimal value) { + this.lat = value; + } + + /** + * Gets the value of the lon property. + * + * @return possible object is {@link BigDecimal } + * + */ + public BigDecimal getLon() { + return lon; + } + + /** + * Sets the value of the lon property. + * + * @param value + * allowed object is {@link BigDecimal } + * + */ + public void setLon(BigDecimal value) { + this.lon = value; + } + +} diff --git a/src/main/java/mobac/data/gpx/gpx11/package-info.java b/src/main/java/mobac/data/gpx/gpx11/package-info.java new file mode 100644 index 0000000..08823fd --- /dev/null +++ b/src/main/java/mobac/data/gpx/gpx11/package-info.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// +// This file was generated by the JavaTM Architecture for XML Binding(JAXB) Reference Implementation, vJAXB 2.1.3 in JDK 1.6 +// See http://java.sun.com/xml/jaxb +// Any modifications to this file will be lost upon recompilation of the source schema. +// Generated on: 2009.08.04 at 03:45:03 PM MESZ +// + +@javax.xml.bind.annotation.XmlSchema(namespace = "http://www.topografix.com/GPX/1/1", elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) +@XmlJavaTypeAdapters({ @XmlJavaTypeAdapter(value = BigDecimalAdapter.class, type = BigDecimal.class) }) +package mobac.data.gpx.gpx11; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters; + +import mobac.program.jaxb.BigDecimalAdapter; + diff --git a/src/main/java/mobac/data/gpx/interfaces/GpxPoint.java b/src/main/java/mobac/data/gpx/interfaces/GpxPoint.java new file mode 100644 index 0000000..b1b65b8 --- /dev/null +++ b/src/main/java/mobac/data/gpx/interfaces/GpxPoint.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.data.gpx.interfaces; + +import java.math.BigDecimal; + +public interface GpxPoint { + + public BigDecimal getLat(); + + public BigDecimal getLon(); + + public BigDecimal getEle(); +} diff --git a/src/main/java/mobac/exceptions/AtlasTestException.java b/src/main/java/mobac/exceptions/AtlasTestException.java new file mode 100644 index 0000000..bc148b3 --- /dev/null +++ b/src/main/java/mobac/exceptions/AtlasTestException.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; + +public class AtlasTestException extends Exception { + + public AtlasTestException(String message, MapInterface map) { + super(message + "\nError caused by map \"" + map.getName() + "\" on layer \"" + + map.getLayer().getName() + "\""); + } + + public AtlasTestException(String message, LayerInterface layer) { + super(message + "\nError caused by layer \"" + layer.getName() + "\""); + } + + public AtlasTestException(String message) { + super(message); + } + + public AtlasTestException(Throwable cause) { + super(cause); + } + + public AtlasTestException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/mobac/exceptions/DownloadFailedException.java b/src/main/java/mobac/exceptions/DownloadFailedException.java new file mode 100644 index 0000000..2280d86 --- /dev/null +++ b/src/main/java/mobac/exceptions/DownloadFailedException.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import java.io.IOException; +import java.net.HttpURLConnection; + +public class DownloadFailedException extends IOException { + + private final int httpResponseCode; + private HttpURLConnection connection; + + public DownloadFailedException(HttpURLConnection connection, int httpResponseCode) + throws IOException { + super("Invaild HTTP response: " + httpResponseCode); + this.connection = connection; + this.httpResponseCode = httpResponseCode; + } + + public int getHttpResponseCode() { + return httpResponseCode; + } + + @Override + public String getMessage() { + return super.getMessage() + "\n" + connection.getURL(); + } + +} diff --git a/src/main/java/mobac/exceptions/InvalidNameException.java b/src/main/java/mobac/exceptions/InvalidNameException.java new file mode 100644 index 0000000..5f18648 --- /dev/null +++ b/src/main/java/mobac/exceptions/InvalidNameException.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +public class InvalidNameException extends Exception { + + private static final long serialVersionUID = 1L; + + public InvalidNameException(String message) { + super(message); + } + +} diff --git a/src/main/java/mobac/exceptions/MOBACOutOfMemoryException.java b/src/main/java/mobac/exceptions/MOBACOutOfMemoryException.java new file mode 100644 index 0000000..93631f6 --- /dev/null +++ b/src/main/java/mobac/exceptions/MOBACOutOfMemoryException.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import mobac.utilities.Utilities; + +public class MOBACOutOfMemoryException extends RuntimeException { + + long requiredMemory; + long heapAvailable; + + public MOBACOutOfMemoryException(long requiredMemory, String message) { + super(message); + Runtime r = Runtime.getRuntime(); + heapAvailable = r.maxMemory() - r.totalMemory() + r.freeMemory(); + this.requiredMemory = requiredMemory; + } + + @Override + public String getMessage() { + return super.getMessage() + "\nRequired memory: " + getFormattedRequiredMemory() + "\nAvailable free memory: " + + Utilities.formatBytes(heapAvailable); + } + + public long getRequiredMemory() { + return requiredMemory; + } + + public String getFormattedRequiredMemory() { + return Utilities.formatBytes(requiredMemory); + } + +} diff --git a/src/main/java/mobac/exceptions/MapCreationException.java b/src/main/java/mobac/exceptions/MapCreationException.java new file mode 100644 index 0000000..ea9715b --- /dev/null +++ b/src/main/java/mobac/exceptions/MapCreationException.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import java.io.StringWriter; + +import mobac.program.interfaces.ExceptionExtendedInfo; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.MapSourceLoaderInfo; + +public class MapCreationException extends Exception implements ExceptionExtendedInfo { + + private static final long serialVersionUID = 1L; + private MapInterface map; + + public MapCreationException(String message, MapInterface map, Throwable cause) { + super(message, cause); + this.map = map; + } + + public MapCreationException(String message, MapInterface map) { + super(message); + this.map = map; + } + + public MapCreationException(MapInterface map, Throwable cause) { + super(cause); + this.map = map; + } + + public String getExtendedInfo() { + StringWriter sw = new StringWriter(); + if (map != null) { + sw.append(map.getInfoText()); + MapSource mapSource = map.getMapSource(); + if (mapSource != null) { + MapSourceLoaderInfo loaderInfo = map.getMapSource().getLoaderInfo(); + if (loaderInfo != null) { + sw.append("\nMap type: " + loaderInfo.getLoaderType()); + if (loaderInfo.getSourceFile() != null) + sw.append("\nMap implementation: " + loaderInfo.getSourceFile().getName()); + sw.append("\nMap revision: " + loaderInfo.getRevision()); + } + } + } + return sw.toString(); + } + + public MapInterface getMap() { + return map; + } + +} diff --git a/src/main/java/mobac/exceptions/MapDownloadSkippedException.java b/src/main/java/mobac/exceptions/MapDownloadSkippedException.java new file mode 100644 index 0000000..4ab8585 --- /dev/null +++ b/src/main/java/mobac/exceptions/MapDownloadSkippedException.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import mobac.program.AtlasThread; + +/** + * Thrown in {@link AtlasThread#createMap(mobac.program.interfaces.MapInterface)} + * if the user chose to skip that map because of download problems. + */ +public class MapDownloadSkippedException extends Exception { + + private static final long serialVersionUID = 1L; + + public MapDownloadSkippedException() { + super(); + } + +} diff --git a/src/main/java/mobac/exceptions/MapSourceCreateException.java b/src/main/java/mobac/exceptions/MapSourceCreateException.java new file mode 100644 index 0000000..42acafc --- /dev/null +++ b/src/main/java/mobac/exceptions/MapSourceCreateException.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +public class MapSourceCreateException extends Exception { + + public MapSourceCreateException(String message) { + super(message); + } + + public MapSourceCreateException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/mobac/exceptions/MapSourceInitializationException.java b/src/main/java/mobac/exceptions/MapSourceInitializationException.java new file mode 100644 index 0000000..414db81 --- /dev/null +++ b/src/main/java/mobac/exceptions/MapSourceInitializationException.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +public class MapSourceInitializationException extends Exception { + + public MapSourceInitializationException() { + } + + public MapSourceInitializationException(String message) { + super(message); + } + + public MapSourceInitializationException(Throwable cause) { + super(cause); + } + + public MapSourceInitializationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/mobac/exceptions/MapSourcesUpdateException.java b/src/main/java/mobac/exceptions/MapSourcesUpdateException.java new file mode 100644 index 0000000..0c538d1 --- /dev/null +++ b/src/main/java/mobac/exceptions/MapSourcesUpdateException.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import mobac.mapsources.MapSourcesManager; + +/** + * Encapsulates several other exceptions that may occur while performing an + * mapsources online update. + * + * @see MapSourcesManager#mapsourcesOnlineUpdate() + */ +public class MapSourcesUpdateException extends Exception { + + private static final long serialVersionUID = 1L; + + public MapSourcesUpdateException(String message) { + super(message); + } + + public MapSourcesUpdateException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/mobac/exceptions/StopAllDownloadsException.java b/src/main/java/mobac/exceptions/StopAllDownloadsException.java new file mode 100644 index 0000000..fecf16d --- /dev/null +++ b/src/main/java/mobac/exceptions/StopAllDownloadsException.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +public class StopAllDownloadsException extends TileException { + + public StopAllDownloadsException(String message, Throwable cause) { + super(message, cause); + } + + public StopAllDownloadsException(String message) { + super(message); + } + +} diff --git a/src/main/java/mobac/exceptions/TileException.java b/src/main/java/mobac/exceptions/TileException.java new file mode 100644 index 0000000..347fdf8 --- /dev/null +++ b/src/main/java/mobac/exceptions/TileException.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +public class TileException extends Exception { + + public TileException(String message, Throwable cause) { + super(message, cause); + } + + public TileException(String message) { + super(message); + } + +} diff --git a/src/main/java/mobac/exceptions/TileStoreException.java b/src/main/java/mobac/exceptions/TileStoreException.java new file mode 100644 index 0000000..a5c0b9e --- /dev/null +++ b/src/main/java/mobac/exceptions/TileStoreException.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +import com.sleepycat.je.DatabaseException; + +public class TileStoreException extends DatabaseException { + + public TileStoreException(Throwable t) { + super(t); + } + + public TileStoreException(String message) { + super(message); + } + + public TileStoreException(String message, Throwable t) { + super(message, t); + } + +} diff --git a/src/main/java/mobac/exceptions/UnrecoverableDownloadException.java b/src/main/java/mobac/exceptions/UnrecoverableDownloadException.java new file mode 100644 index 0000000..3079d5c --- /dev/null +++ b/src/main/java/mobac/exceptions/UnrecoverableDownloadException.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +/** + * An {@link UnrecoverableDownloadException} indicates that there has been a + * problem on client side that made it impossible to download a certain file + * (usually a map tile image). Therefore the error is independent of the network + * connection between client and server and the server itself. + */ +public class UnrecoverableDownloadException extends TileException { + + private static final long serialVersionUID = 1L; + + public static int ERROR_CODE_NORMAL = 0; + public static int ERROR_CODE_CONTENT_TYPE = 1; + + private int errorCode = ERROR_CODE_NORMAL; + + public UnrecoverableDownloadException(String message) { + super(message); + errorCode = ERROR_CODE_NORMAL; + } + + public UnrecoverableDownloadException(String message, int errorCode) { + super(message); + this.errorCode = errorCode; + } + + public int getErrorCode() + { + return errorCode; + } + +} diff --git a/src/main/java/mobac/exceptions/UpdateFailedException.java b/src/main/java/mobac/exceptions/UpdateFailedException.java new file mode 100644 index 0000000..abcfe4e --- /dev/null +++ b/src/main/java/mobac/exceptions/UpdateFailedException.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.exceptions; + +public class UpdateFailedException extends Exception { + + public UpdateFailedException(String message) { + super(message); + } + + public UpdateFailedException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/mobac/externaltools/ExternalToolDef.java b/src/main/java/mobac/externaltools/ExternalToolDef.java new file mode 100644 index 0000000..753f3cd --- /dev/null +++ b/src/main/java/mobac/externaltools/ExternalToolDef.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.externaltools; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JOptionPane; +import javax.xml.bind.annotation.XmlList; +import javax.xml.bind.annotation.XmlRootElement; + +import mobac.gui.MainGUI; +import mobac.program.model.MapSelection; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +@XmlRootElement(name = "ExternalTool") +public class ExternalToolDef implements ActionListener { + + private static final Logger log = Logger.getLogger(ExternalToolDef.class); + + /** + * Name used for the menu entry in MOBAC + */ + public String name; + + /** + * For starting a commandline-script on Windows use cmd /c start mybatch.cmd + */ + public String command; + + public boolean debug = false; + + @XmlList + public List parameters = new ArrayList(); + + private boolean mapSelNull(MapSelection mapSel) { + if (mapSel != null) + return false; + JOptionPane.showMessageDialog(MainGUI.getMainGUI(), + I18nUtils.localizedStringForKey("msg_tools_exec_error_selected_area"), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + return true; + + } + + public void actionPerformed(ActionEvent e) { + try { + + String executeCommand = command; + MainGUI gui = MainGUI.getMainGUI(); + MapSelection mapSel = gui.getMapSelectionCoordinates(); + int[] zooms = gui.getSelectedZoomLevels().getZoomLevels(); + for (ToolParameters param : parameters) { + String add = ""; + switch (param) { + case MAX_LAT: + if (mapSelNull(mapSel)) + return; + add = Double.toString(mapSel.getMax().lat); + break; + case MIN_LAT: + if (mapSelNull(mapSel)) + return; + add = Double.toString(mapSel.getMin().lat); + break; + case MAX_LON: + if (mapSelNull(mapSel)) + return; + add = Double.toString(mapSel.getMax().lon); + break; + case MIN_LON: + if (mapSelNull(mapSel)) + return; + add = Double.toString(mapSel.getMin().lon); + break; + case MAX_ZOOM: + if (zooms.length == 0) { + JOptionPane.showMessageDialog(gui, + I18nUtils.localizedStringForKey("msg_no_zoom_level_selected"), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + return; + } + add = Integer.toString(zooms[zooms.length - 1]); + break; + case MIN_ZOOM: + if (zooms.length == 0) { + JOptionPane.showMessageDialog(gui, + I18nUtils.localizedStringForKey("msg_no_zoom_level_selected"), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + return; + } + add = Integer.toString(zooms[0]); + break; + case MAPSOURCE_NAME: + add = gui.previewMap.getMapSource().getName(); + break; + case MAPSOURCE_DISPLAYNAME: + add = gui.previewMap.getMapSource().toString(); + break; + case NAME_EDITBOX: + add = gui.getUserText(); + break; + default: + throw new RuntimeException("Unsupported parameter type: " + param); + } + if (add.indexOf(' ') >= 0) + add = "\"" + add + "\""; + executeCommand += " " + add; + } + if (debug) { + int r = JOptionPane.showConfirmDialog(gui, + String.format(I18nUtils.localizedStringForKey("msg_tools_exec_command_ask"), executeCommand), + I18nUtils.localizedStringForKey("msg_tools_exec_command_ask_title"), + JOptionPane.OK_CANCEL_OPTION); + if (r != JOptionPane.OK_OPTION) + return; + } + log.debug("Executing " + executeCommand); + Runtime.getRuntime().exec(executeCommand); + } catch (Exception e1) { + GUIExceptionHandler.processException(e1); + } + } + +} diff --git a/src/main/java/mobac/externaltools/ExternalToolsLoader.java b/src/main/java/mobac/externaltools/ExternalToolsLoader.java new file mode 100644 index 0000000..5aba164 --- /dev/null +++ b/src/main/java/mobac/externaltools/ExternalToolsLoader.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.externaltools; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import mobac.program.DirectoryManager; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.Utilities; +import mobac.utilities.file.FileExtFilter; + +import org.w3c.dom.Document; + +public class ExternalToolsLoader { + + public static List tools = null; + + static { + load(); + } + + public static boolean load() { + try { + File dir = DirectoryManager.toolsDir; + if (!dir.isDirectory()) + return false; + File[] files = dir.listFiles(new FileExtFilter(".xml")); + tools = new LinkedList(); + for (File f : files) { + tools.add(loadFile(f)); + } + return true; + } catch (Exception e) { + GUIExceptionHandler.showExceptionDialog("Failed to load external tools definition", e); + return false; + } + } + + public static ExternalToolDef loadFile(File f) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(ExternalToolDef.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + InputStream is = null; + try { + is = new FileInputStream(f); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder loader = factory.newDocumentBuilder(); + Document document = loader.parse(is); + return (ExternalToolDef) unmarshaller.unmarshal(document); + } catch (JAXBException e) { + throw e; + } catch (Exception e) { + throw new JAXBException(e); + } finally { + Utilities.closeStream(is); + } + } + + public static void save(ExternalToolDef toolsDef, File f) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(ExternalToolDef.class); + Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + OutputStream os = null; + try { + os = new FileOutputStream(f); + marshaller.marshal(toolsDef, os); + } catch (FileNotFoundException e) { + throw new JAXBException(e); + } finally { + Utilities.closeStream(os); + } + } + + public static final void main(String[] args) { + try { + ExternalToolDef t = new ExternalToolDef(); + t.command = "command"; + t.name = "test"; + t.parameters.add(ToolParameters.MAX_LAT); + t.parameters.add(ToolParameters.MAX_LON); + t.parameters.add(ToolParameters.MIN_LAT); + t.parameters.add(ToolParameters.MIN_LON); + save(t, new File("tools/test.xml")); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/mobac/externaltools/ToolParameters.java b/src/main/java/mobac/externaltools/ToolParameters.java new file mode 100644 index 0000000..501ab84 --- /dev/null +++ b/src/main/java/mobac/externaltools/ToolParameters.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.externaltools; + +public enum ToolParameters { + + MAX_LAT, MIN_LAT, MAX_LON, MIN_LON, // + MIN_ZOOM, MAX_ZOOM, // minimum and maximum of selected zoom levels check-boxes + MAPSOURCE_NAME, MAPSOURCE_DISPLAYNAME, // Currently select map source internal or display name + NAME_EDITBOX // content of the edit box "Name" in the side panel "Atlas Content" + +} diff --git a/src/main/java/mobac/gui/AtlasProgress.java b/src/main/java/mobac/gui/AtlasProgress.java new file mode 100644 index 0000000..8b97586 --- /dev/null +++ b/src/main/java/mobac/gui/AtlasProgress.java @@ -0,0 +1,751 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui; + +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagLayout; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; + +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import javax.swing.ToolTipManager; +import javax.swing.UIManager; + +import mobac.program.AtlasThread; +import mobac.program.Logging; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSourceListener; +import mobac.program.model.AtlasOutputFormat; +import mobac.program.model.Settings; +import mobac.utilities.GBC; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.OSUtilities; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +/** + * A window showing the progress while {@link AtlasThread} downloads and processes the map tiles. + * + */ +public class AtlasProgress extends JFrame implements ActionListener, MapSourceListener { + + private static Logger log = Logger.getLogger(AtlasProgress.class); + + private static final long serialVersionUID = -1L; + + private static final Timer TIMER = new Timer(true); + + private JProgressBar atlasProgressBar; + private JProgressBar mapDownloadProgressBar; + private JProgressBar mapCreationProgressBar; + + private Container background; + + private long initialTotalTime; + private long initialMapDownloadTime; + + private static class Data { + AtlasInterface atlasInterface; + MapInterface map; + MapInfo mapInfo; + long numberOfDownloadedBytes = 0; + long numberOfBytesLoadedFromCache = 0; + int totalNumberOfTiles = 0; + int totalNumberOfMaps = 0; + int totalProgress = 0; + int totalProgressTenthPercent = -1; + int currentMapNumber = 0; + int mapDownloadProgress = 0; + int mapDownloadNumberOfTiles = 0; + int mapCreationProgress = 0; + int mapCreationMax = 0; + int mapRetryErrors = 0; + int mapPermanentErrors = 0; + int prevMapsRetryErrors = 0; + int prevMapsPermanentErrors = 0; + boolean paused = false; + } + + private final Data data = new Data(); + + private boolean aborted = false; + private boolean finished = false; + + private JLabel windowTitle; + + private JLabel title; + private JLabel mapInfoLabel; + private JLabel mapDownloadTitle; + private JLabel atlasPercent; + private JLabel mapDownloadPercent; + private JLabel atlasMapsDone; + private JLabel mapDownloadElementsDone; + private JLabel atlasTimeLeft; + private JLabel mapDownloadTimeLeft; + private JLabel mapCreation; + private JLabel nrOfDownloadedBytes; + private JLabel nrOfDownloadedBytesValue; + private JLabel nrOfDownloadedBytesPerSecond; + private JLabel nrOfDownloadedBytesPerSecondValue; + private JLabel nrOfCacheBytes; + private JLabel nrOfCacheBytesValue; + private JLabel activeDownloads; + private JLabel activeDownloadsValue; + private JLabel retryableDownloadErrors; + private JLabel retryableDownloadErrorsValue; + private JLabel permanentDownloadErrors; + private JLabel permanentDownloadErrorsValue; + private JLabel totalDownloadTime; + private JLabel totalDownloadTimeValue; + + private JCheckBox ignoreDlErrors; + private JLabel statusLabel; + + private JButton dismissWindowButton; + private JButton openProgramFolderButton; + private JButton abortAtlasCreationButton; + private JButton pauseResumeDownloadButton; + + private AtlasCreationController downloadControlListener = null; + + private UpdateTask updateTask = null; + private GUIUpdater guiUpdater = null; + + private AtlasThread atlasThread; + + private ArrayList mapInfos = null; + + private static String TEXT_MAP_DOWNLOAD = I18nUtils.localizedStringForKey("dlg_download_zoom_level_progress"); + private static String TEXT_PERCENT = I18nUtils.localizedStringForKey("dlg_download_done_percent"); + private static String TEXT_TENTHPERCENT = I18nUtils.localizedStringForKey("dlg_download_done_tenthpercent"); + + public AtlasProgress(AtlasThread atlasThread) { + super(I18nUtils.localizedStringForKey("dlg_download_title")); + this.atlasThread = atlasThread; + ToolTipManager.sharedInstance().setDismissDelay(12000); + if (MainGUI.getMainGUI() == null) // Atlas creation started via command-line, no MainGUi available + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + else + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + + setIconImages(MainGUI.MOBAC_ICONS); + setLayout(new GridBagLayout()); + updateTask = new UpdateTask(); + guiUpdater = new GUIUpdater(); + + createComponents(); + // Initialize the layout in respect to the layout (font size ...) + pack(); + + guiUpdater.run(); + + // The layout is now initialized - we disable it because we don't want + // want to the labels to jump around if the content changes. + background.setLayout(null); + setResizable(false); + + Dimension dScreen = Toolkit.getDefaultToolkit().getScreenSize(); + Dimension dContent = getSize(); + setLocation((dScreen.width - dContent.width) / 2, (dScreen.height - dContent.height) / 2); + + initialTotalTime = System.currentTimeMillis(); + initialMapDownloadTime = System.currentTimeMillis(); + + addWindowListener(new CloseListener()); + } + + private void createComponents() { + background = new JPanel(new GridBagLayout()); + + windowTitle = new JLabel(I18nUtils.localizedStringForKey("dlg_download_window_title")); + + title = new JLabel(I18nUtils.localizedStringForKey("dlg_download_map_progress")); + + mapInfoLabel = new JLabel(); + + atlasMapsDone = new JLabel(I18nUtils.localizedStringForKey("dlg_download_map_done_count_default")); + atlasPercent = new JLabel(String.format(TEXT_TENTHPERCENT, 100.0)); + atlasTimeLeft = new JLabel(I18nUtils.localizedStringForKey("dlg_download_remain_time_default"), JLabel.RIGHT); + atlasProgressBar = new JProgressBar(); + + mapDownloadTitle = new JLabel(TEXT_MAP_DOWNLOAD + "000"); + mapDownloadElementsDone = new JLabel(I18nUtils.localizedStringForKey("dlg_download_tile_done_count_default")); + mapDownloadPercent = new JLabel(String.format(TEXT_PERCENT, 100)); + mapDownloadTimeLeft = new JLabel(I18nUtils.localizedStringForKey("dlg_download_remain_time_default"), + JLabel.RIGHT); + mapDownloadProgressBar = new JProgressBar(); + + mapCreation = new JLabel(I18nUtils.localizedStringForKey("dlg_download_map_create_title")); + mapCreationProgressBar = new JProgressBar(); + + nrOfDownloadedBytesPerSecond = new JLabel(I18nUtils.localizedStringForKey("dlg_download_avg_speed")); + nrOfDownloadedBytesPerSecondValue = new JLabel(); + nrOfDownloadedBytes = new JLabel(I18nUtils.localizedStringForKey("dlg_download_total_bytes")); + nrOfDownloadedBytesValue = new JLabel(); + nrOfCacheBytes = new JLabel(I18nUtils.localizedStringForKey("dlg_download_bytes_from_cache")); + nrOfCacheBytesValue = new JLabel(); + + activeDownloads = new JLabel(I18nUtils.localizedStringForKey("dlg_download_thread_count")); + activeDownloadsValue = new JLabel(); + retryableDownloadErrors = new JLabel(I18nUtils.localizedStringForKey("dlg_download_retry_count")); + retryableDownloadErrors.setToolTipText(I18nUtils.localizedStringForKey("dlg_download_error_tips")); + retryableDownloadErrorsValue = new JLabel(); + retryableDownloadErrorsValue.setToolTipText(retryableDownloadErrors.getToolTipText()); + permanentDownloadErrors = new JLabel(I18nUtils.localizedStringForKey("dlg_download_failed_count")); + permanentDownloadErrors.setToolTipText(I18nUtils.localizedStringForKey("dlg_download_error_tips")); + permanentDownloadErrorsValue = new JLabel(); + permanentDownloadErrorsValue.setToolTipText(permanentDownloadErrors.getToolTipText()); + totalDownloadTime = new JLabel(I18nUtils.localizedStringForKey("dlg_download_total_time")); + totalDownloadTimeValue = new JLabel(); + + ignoreDlErrors = new JCheckBox(I18nUtils.localizedStringForKey("dlg_download_checkbox_ignore_error"), + Settings.getInstance().ignoreDlErrors); + statusLabel = new JLabel(I18nUtils.localizedStringForKey("dlg_download_status_title")); + Font f = statusLabel.getFont(); + statusLabel.setFont(f.deriveFont(Font.BOLD)); + abortAtlasCreationButton = new JButton(I18nUtils.localizedStringForKey("dlg_download_btn_abort")); + abortAtlasCreationButton.setToolTipText(I18nUtils.localizedStringForKey("dlg_download_btn_abort_tips")); + dismissWindowButton = new JButton(I18nUtils.localizedStringForKey("dlg_download_btn_close_win")); + dismissWindowButton.setToolTipText(I18nUtils.localizedStringForKey("dlg_download_btn_close_win_tips_disable")); + dismissWindowButton.setVisible(false); + openProgramFolderButton = new JButton(I18nUtils.localizedStringForKey("dlg_download_btn_open_folder")); + openProgramFolderButton.setToolTipText(I18nUtils + .localizedStringForKey("dlg_download_btn_open_folder_tips_disabled")); + openProgramFolderButton.setEnabled(false); + pauseResumeDownloadButton = new JButton(I18nUtils.localizedStringForKey("dlg_download_btn_pause_resume")); + + GBC gbcRIF = GBC.std().insets(0, 0, 20, 0).fill(GBC.HORIZONTAL); + GBC gbcEol = GBC.eol(); + GBC gbcEolFill = GBC.eol().fill(GBC.HORIZONTAL); + GBC gbcEolFillI = GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 0); + + // background.add(windowTitle, gbcEolFill); + // background.add(Box.createVerticalStrut(10), gbcEol); + + background.add(mapInfoLabel, gbcEolFill); + background.add(Box.createVerticalStrut(20), gbcEol); + + background.add(title, gbcRIF); + background.add(atlasMapsDone, gbcRIF); + background.add(atlasPercent, gbcRIF); + background.add(atlasTimeLeft, gbcEolFill); + background.add(atlasProgressBar, gbcEolFillI); + background.add(Box.createVerticalStrut(20), gbcEol); + + background.add(mapDownloadTitle, gbcRIF); + background.add(mapDownloadElementsDone, gbcRIF); + background.add(mapDownloadPercent, gbcRIF); + background.add(mapDownloadTimeLeft, gbcEolFill); + background.add(mapDownloadProgressBar, gbcEolFillI); + background.add(Box.createVerticalStrut(20), gbcEol); + + background.add(mapCreation, gbcEol); + background.add(mapCreationProgressBar, gbcEolFillI); + background.add(Box.createVerticalStrut(10), gbcEol); + + JPanel infoPanel = new JPanel(new GridBagLayout()); + GBC gbci = GBC.std().insets(0, 3, 3, 3); + infoPanel.add(nrOfDownloadedBytes, gbci); + infoPanel.add(nrOfDownloadedBytesValue, gbci.toggleEol()); + infoPanel.add(nrOfCacheBytes, gbci.toggleEol()); + infoPanel.add(nrOfCacheBytesValue, gbci.toggleEol()); + infoPanel.add(nrOfDownloadedBytesPerSecond, gbci.toggleEol()); + infoPanel.add(nrOfDownloadedBytesPerSecondValue, gbci.toggleEol()); + infoPanel.add(activeDownloads, gbci.toggleEol()); + infoPanel.add(activeDownloadsValue, gbci.toggleEol()); + infoPanel.add(retryableDownloadErrors, gbci.toggleEol()); + infoPanel.add(retryableDownloadErrorsValue, gbci.toggleEol()); + infoPanel.add(permanentDownloadErrors, gbci.toggleEol()); + infoPanel.add(permanentDownloadErrorsValue, gbci.toggleEol()); + infoPanel.add(totalDownloadTime, gbci.toggleEol()); + infoPanel.add(totalDownloadTimeValue, gbci.toggleEol()); + + JPanel bottomPanel = new JPanel(new GridBagLayout()); + bottomPanel.add(infoPanel, GBC.std().gridheight(2).fillH()); + bottomPanel.add(ignoreDlErrors, GBC.eol().anchor(GBC.EAST)); + + bottomPanel.add(statusLabel, GBC.eol().anchor(GBC.CENTER)); + + GBC gbcRight = GBC.std().anchor(GBC.SOUTHEAST).insets(5, 0, 0, 0); + bottomPanel.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL)); + bottomPanel.add(abortAtlasCreationButton, gbcRight); + bottomPanel.add(dismissWindowButton, gbcRight); + bottomPanel.add(pauseResumeDownloadButton, gbcRight); + bottomPanel.add(openProgramFolderButton, gbcRight); + + background.add(bottomPanel, gbcEolFillI); + + JPanel borderPanel = new JPanel(new GridBagLayout()); + borderPanel.add(background, GBC.std().insets(10, 10, 10, 10).fill()); + + add(borderPanel, GBC.std().fill()); + + abortAtlasCreationButton.addActionListener(this); + dismissWindowButton.addActionListener(this); + openProgramFolderButton.addActionListener(this); + pauseResumeDownloadButton.addActionListener(this); + } + + public void initAtlas(AtlasInterface atlasInterface) { + data.atlasInterface = atlasInterface; + if (atlasInterface.getOutputFormat().equals(AtlasOutputFormat.TILESTORE)) + data.totalNumberOfTiles = (int) atlasInterface.calculateTilesToDownload(); + else + data.totalNumberOfTiles = (int) atlasInterface.calculateTilesToDownload() * 2; + int mapCount = 0; + int tileCount = 0; + mapInfos = new ArrayList(100); + for (LayerInterface layer : atlasInterface) { + mapCount += layer.getMapCount(); + for (MapInterface map : layer) { + int before = tileCount; + int mapTiles = (int) map.calculateTilesToDownload(); + tileCount += mapTiles + mapTiles; + mapInfos.add(new MapInfo(map, before, tileCount)); + } + } + mapInfos.trimToSize(); + data.totalNumberOfMaps = mapCount; + + initialTotalTime = System.currentTimeMillis(); + initialMapDownloadTime = -1; + updateGUI(); + setVisible(true); + TIMER.schedule(updateTask, 0, 500); + } + + public void initMapDownload(MapInterface map) { + int index = mapInfos.indexOf(new MapInfo(map, 0, 0)); + data.mapInfo = mapInfos.get(index); + data.totalProgress = data.mapInfo.tileCountOnStart; + data.map = map; + data.mapDownloadNumberOfTiles = (int) map.calculateTilesToDownload(); + initialMapDownloadTime = System.currentTimeMillis(); + data.prevMapsPermanentErrors += data.mapPermanentErrors; + data.prevMapsRetryErrors += data.mapRetryErrors; + data.mapCreationProgress = 0; + data.mapDownloadProgress = 0; + data.currentMapNumber = index + 1; + updateGUI(); + } + + /** + * Initialize the GUI progress bars + * + * @param maxTilesToProcess + */ + public void initMapCreation(int maxTilesToProcess) { + data.mapCreationProgress = 0; + data.mapCreationMax = maxTilesToProcess; + initialMapDownloadTime = -1; + updateGUI(); + } + + public void setErrorCounter(int retryErrors, int permanentErrors) { + data.mapRetryErrors = retryErrors; + data.mapPermanentErrors = permanentErrors; + updateGUI(); + } + + public void incMapDownloadProgress() { + data.mapDownloadProgress++; + data.totalProgress++; + updateGUI(); + } + + public void incMapCreationProgress() { + setMapCreationProgress(data.mapCreationProgress + 1); + } + + public void incMapCreationProgress(int stepSize) { + setMapCreationProgress(data.mapCreationProgress + stepSize); + } + + public void setMapCreationProgress(int progress) { + data.mapCreationProgress = progress; + data.totalProgress = data.mapInfo.tileCountOnStart + data.mapInfo.mapTiles + + (int) (((long) data.mapInfo.mapTiles) * data.mapCreationProgress / data.mapCreationMax); + updateGUI(); + } + + public boolean ignoreDownloadErrors() { + return ignoreDlErrors.isSelected(); + } + + public void tileDownloaded(int size) { + synchronized (data) { + data.numberOfDownloadedBytes += size; + } + updateGUI(); + } + + public void tileLoadedFromCache(int size) { + synchronized (data) { + data.numberOfBytesLoadedFromCache += size; + } + updateGUI(); + } + + private String formatTime(long longSeconds) { + String timeString = ""; + + if (longSeconds < 0) { + timeString = I18nUtils.localizedStringForKey("dlg_download_time_unknown"); + } else { + int minutes = (int) (longSeconds / 60); + int seconds = (int) (longSeconds % 60); + if (minutes > 0) + timeString += Integer.toString(minutes) + + " " + + (minutes == 1 ? I18nUtils.localizedStringForKey("minute") : I18nUtils + .localizedStringForKey("minutes")) + " "; + timeString += Integer.toString(seconds) + + " " + + (seconds == 1 ? I18nUtils.localizedStringForKey("second") : I18nUtils + .localizedStringForKey("seconds")); + } + return timeString; + } + + public void setZoomLevel(int theZoomLevel) { + mapDownloadTitle.setText(TEXT_MAP_DOWNLOAD + Integer.toString(theZoomLevel)); + } + + public void atlasCreationFinished() { + finished = true; + stopUpdateTask(); + forceUpdateGUI(); + downloadControlListener = null; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + abortAtlasCreationButton.setEnabled(false); + + if (aborted) { + windowTitle.setText(I18nUtils.localizedStringForKey("dlg_download_abort_window_title")); + setTitle(I18nUtils.localizedStringForKey("dlg_download_abort_title")); + } else { + windowTitle.setText(I18nUtils.localizedStringForKey("dlg_download_succeed_window_title")); + setTitle(I18nUtils.localizedStringForKey("dlg_download_succeed_title")); + } + // mapInfoLabel.setText(""); + atlasMapsDone.setText(String.format(I18nUtils.localizedStringForKey("dlg_download_map_done_count"), + data.currentMapNumber, data.totalNumberOfMaps)); + + abortAtlasCreationButton.setVisible(false); + + dismissWindowButton.setToolTipText(I18nUtils + .localizedStringForKey("dlg_download_btn_close_win_tips_enable")); + dismissWindowButton.setVisible(true); + + if (!aborted) { + openProgramFolderButton.setToolTipText(I18nUtils + .localizedStringForKey("dlg_download_btn_open_folder_tips_enabled")); + openProgramFolderButton.setEnabled(true); + } + } + }); + } + + private synchronized void stopUpdateTask() { + try { + updateTask.cancel(); + updateTask = null; + } catch (Exception e) { + } + } + + public void closeWindow() { + try { + stopUpdateTask(); + downloadControlListener = null; + setVisible(false); + } finally { + dispose(); + } + } + + public AtlasCreationController getDownloadControlListener() { + return downloadControlListener; + } + + public void setDownloadControlerListener(AtlasCreationController threadControlListener) { + this.downloadControlListener = threadControlListener; + } + + public void actionPerformed(ActionEvent event) { + Object source = event.getSource(); + File atlasFolder = Settings.getInstance().getAtlasOutputDirectory(); + if (openProgramFolderButton.equals(source)) { + try { + OSUtilities.openFolderBrowser(atlasFolder); + } catch (Exception e) { + log.error("", e); + } + } else if (dismissWindowButton.equals(source)) { + downloadControlListener = null; + closeWindow(); + } else if (abortAtlasCreationButton.equals(source)) { + aborted = true; + stopUpdateTask(); + if (downloadControlListener != null) + downloadControlListener.abortAtlasCreation(); + else + closeWindow(); + } else if (pauseResumeDownloadButton.equals(source)) { + if (downloadControlListener != null) + downloadControlListener.pauseResumeAtlasCreation(); + } + } + + public void updateGUI() { + guiUpdater.updateAsynchronously(); + } + + public void forceUpdateGUI() { + SwingUtilities.invokeLater(guiUpdater); + } + + private class GUIUpdater implements Runnable { + + int scheduledCounter = 0; + + public void updateAsynchronously() { + // If there is still at least one scheduled update request to be + // executed we don't have add another one as this can result in an + // to overloaded swing invocation queue. + synchronized (this) { + if (scheduledCounter > 0) + return; + scheduledCounter++; + } + SwingUtilities.invokeLater(this); + } + + public void run() { + synchronized (this) { + scheduledCounter--; + } + + if (data.map != null) { + String text = String.format(I18nUtils.localizedStringForKey("dlg_download_map_info_label"), + data.map.getName(), data.map.getLayer().getName(), data.map.getMapSource().toString()); + mapInfoLabel.setText(text); + } + + // atlas progress + atlasProgressBar.setMaximum(data.totalNumberOfTiles); + atlasProgressBar.setValue(data.totalProgress); + + int newTenthPercent = (int) (data.totalProgress * 1000d / (double) data.totalNumberOfTiles); + try { + boolean pauseState = atlasThread.isPaused(); + String statusText = I18nUtils.localizedStringForKey("dlg_download_status_running"); + if (aborted) + statusText = I18nUtils.localizedStringForKey("dlg_download_status_aborted"); + else if (finished) + statusText = I18nUtils.localizedStringForKey("dlg_download_status_finished"); + else if (pauseState) + statusText = I18nUtils.localizedStringForKey("dlg_download_status_paused"); + else + statusText = I18nUtils.localizedStringForKey("dlg_download_status_running"); + statusLabel.setText(I18nUtils.localizedStringForKey("dlg_download_status_title") + " " + statusText); + + if (data.totalProgressTenthPercent != newTenthPercent || pauseState != data.paused) { + data.totalProgressTenthPercent = newTenthPercent; + atlasPercent.setText(String.format(TEXT_TENTHPERCENT, data.totalProgressTenthPercent / 10.0)); + if (data.atlasInterface != null) { + String text = String.format(I18nUtils.localizedStringForKey("dlg_download_atlas_progress"), + data.totalProgressTenthPercent / 10, data.atlasInterface.getName(), + data.atlasInterface.getOutputFormat()); + if (pauseState) + text += " [" + I18nUtils.localizedStringForKey("dlg_download_status_paused") + "]"; + AtlasProgress.this.setTitle(text); + } + } + data.paused = pauseState; + } catch (NullPointerException e) { + } + + long seconds = -1; + int totalProgress = data.totalProgress; + if (totalProgress != 0) { + // Avoid for a possible division by zero + int totalTilesRemaining = data.totalNumberOfTiles - totalProgress; + long totalElapsedTime = System.currentTimeMillis() - initialTotalTime; + seconds = (totalElapsedTime * totalTilesRemaining / (1000L * totalProgress)); + } + atlasTimeLeft.setText(String.format(I18nUtils.localizedStringForKey("dlg_download_remain_time"), + formatTime(seconds))); + + // layer progress + mapDownloadProgressBar.setMaximum(data.mapDownloadNumberOfTiles); + mapDownloadProgressBar.setValue(data.mapDownloadProgress); + + mapDownloadPercent.setText(String.format(TEXT_PERCENT, + (int) (mapDownloadProgressBar.getPercentComplete() * 100))); + + mapDownloadElementsDone.setText(String.format( + I18nUtils.localizedStringForKey("dlg_download_tile_done_count"), data.mapDownloadProgress, + data.mapDownloadNumberOfTiles)); + + seconds = -1; + int mapDlProgress = data.mapDownloadProgress; + if (mapDlProgress != 0 && initialMapDownloadTime > 0) + seconds = ((System.currentTimeMillis() - initialMapDownloadTime) + * (data.mapDownloadNumberOfTiles - mapDlProgress) / (1000L * mapDlProgress)); + mapDownloadTimeLeft.setText(String.format(I18nUtils.localizedStringForKey("dlg_download_remain_time"), + formatTime(seconds))); + + // map progress + mapCreation.setText(I18nUtils.localizedStringForKey("dlg_download_map_create_title")); + mapCreationProgressBar.setValue(data.mapCreationProgress); + mapCreationProgressBar.setMaximum(data.mapCreationMax); + atlasMapsDone.setText(String.format(I18nUtils.localizedStringForKey("dlg_download_map_done_count"), + (data.currentMapNumber - 1), data.totalNumberOfMaps)); + + // bytes per second + long rate = data.numberOfDownloadedBytes * 1000; + long time = System.currentTimeMillis() - initialMapDownloadTime; + if (data.mapCreationProgress == 0 && initialMapDownloadTime > 0) { + if (time == 0) { + nrOfDownloadedBytesPerSecondValue.setText("??"); + } else { + rate = rate / time; + nrOfDownloadedBytesPerSecondValue.setText(String.format( + I18nUtils.localizedStringForKey("dlg_download_avg_speed_value"), + Utilities.formatBytes(rate))); + } + } + + // downloaded bytes + nrOfDownloadedBytesValue.setText(": " + Utilities.formatBytes(data.numberOfDownloadedBytes)); + nrOfCacheBytesValue.setText(": " + Utilities.formatBytes(data.numberOfBytesLoadedFromCache)); + + // total creation time + long totalSeconds = (System.currentTimeMillis() - initialTotalTime) / 1000; + totalDownloadTimeValue.setText(": " + formatTime(totalSeconds)); + totalDownloadTimeValue.repaint(); + + // active downloads + int activeDownloads = (atlasThread == null) ? 0 : atlasThread.getActiveDownloads(); + activeDownloadsValue.setText(": " + activeDownloads); + activeDownloadsValue.repaint(); + + int totalRetryableErrors = data.prevMapsRetryErrors + data.mapRetryErrors; + retryableDownloadErrorsValue.setText(String.format( + I18nUtils.localizedStringForKey("dlg_download_retry_count_value"), data.mapRetryErrors, + totalRetryableErrors)); + retryableDownloadErrorsValue.repaint(); + int totalPermanentErrors = data.prevMapsPermanentErrors + data.mapPermanentErrors; + permanentDownloadErrorsValue.setText(String.format( + I18nUtils.localizedStringForKey("dlg_download_failed_count_value"), data.mapPermanentErrors, + totalPermanentErrors)); + permanentDownloadErrorsValue.repaint(); + } + } + + private class UpdateTask extends TimerTask { + + @Override + public void run() { + updateGUI(); + } + } + + private class CloseListener extends WindowAdapter { + + @Override + public void windowClosing(WindowEvent e) { + log.debug("Closing event detected for atlas progress window"); + AtlasCreationController listener = AtlasProgress.this.downloadControlListener; + if (listener != null) + listener.abortAtlasCreation(); + } + + } + + protected static class MapInfo { + + final MapInterface map; + final int tileCountOnStart; + final int tileCountOnEnd; + final int mapTiles; + + public MapInfo(MapInterface map, int tileCountOnStart, int tileCountOnEnd) { + super(); + this.map = map; + this.tileCountOnStart = tileCountOnStart; + this.tileCountOnEnd = tileCountOnEnd; + this.mapTiles = (int) map.calculateTilesToDownload(); + } + + @Override + public int hashCode() { + return map.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MapInfo)) + return false; + return map.equals(((MapInfo) obj).map); + } + } + + public static interface AtlasCreationController { + + public void abortAtlasCreation(); + + public void pauseResumeAtlasCreation(); + + public boolean isPaused(); + + } + + public static void main(String[] args) { + Logging.configureLogging(); + GUIExceptionHandler.installToolkitEventQueueProxy(); + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + log.error("The selection of look and feel failed!", e); + } + AtlasProgress ap = new AtlasProgress(null); + ap.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + ap.setVisible(true); + } +} diff --git a/src/main/java/mobac/gui/MainGUI.java b/src/main/java/mobac/gui/MainGUI.java new file mode 100644 index 0000000..ab18e51 --- /dev/null +++ b/src/main/java/mobac/gui/MainGUI.java @@ -0,0 +1,1128 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Frame; +import java.awt.GraphicsEnvironment; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JLayeredPane; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButtonMenuItem; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import javax.xml.bind.JAXBException; + +import mobac.externaltools.ExternalToolDef; +import mobac.externaltools.ExternalToolsLoader; +import mobac.gui.actions.AddGpxTrackAreaPolygonMap; +import mobac.gui.actions.AddGpxTrackPolygonMap; +import mobac.gui.actions.AddMapLayer; +import mobac.gui.actions.AtlasConvert; +import mobac.gui.actions.AtlasCreate; +import mobac.gui.actions.AtlasNew; +import mobac.gui.actions.BookmarkAdd; +import mobac.gui.actions.BookmarkManage; +import mobac.gui.actions.DebugSetLogLevel; +import mobac.gui.actions.DebugShowLogFile; +import mobac.gui.actions.DebugShowMapSourceNames; +import mobac.gui.actions.DebugShowMapTileGrid; +import mobac.gui.actions.DebugShowReport; +import mobac.gui.actions.HelpLicenses; +import mobac.gui.actions.PanelShowHide; +import mobac.gui.actions.RefreshCustomMapsources; +import mobac.gui.actions.SelectionModeCircle; +import mobac.gui.actions.SelectionModePolygon; +import mobac.gui.actions.SelectionModeRectangle; +import mobac.gui.actions.ShowAboutDialog; +import mobac.gui.actions.ShowHelpAction; +import mobac.gui.actions.ShowReadme; +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.components.FilledLayeredPane; +import mobac.gui.components.JAtlasNameField; +import mobac.gui.components.JBookmarkMenuItem; +import mobac.gui.components.JCollapsiblePanel; +import mobac.gui.components.JMenuItem2; +import mobac.gui.components.JZoomCheckBox; +import mobac.gui.listeners.AtlasModelListener; +import mobac.gui.mapview.GridZoom; +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.WgsGrid.WgsDensity; +import mobac.gui.mapview.controller.JMapController; +import mobac.gui.mapview.controller.PolygonCircleSelectionMapController; +import mobac.gui.mapview.controller.PolygonSelectionMapController; +import mobac.gui.mapview.controller.RectangleSelectionMapController; +import mobac.gui.mapview.interfaces.MapEventListener; +import mobac.gui.panels.JCoordinatesPanel; +import mobac.gui.panels.JGpxPanel; +import mobac.gui.panels.JProfilesPanel; +import mobac.gui.panels.JTileImageParametersPanel; +import mobac.gui.panels.JTileStoreCoveragePanel; +import mobac.gui.settings.SettingsGUI; +import mobac.mapsources.MapSourcesManager; +import mobac.program.ProgramInfo; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.InitializableMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.model.Bookmark; +import mobac.program.model.MapSelection; +import mobac.program.model.MercatorPixelCoordinate; +import mobac.program.model.Profile; +import mobac.program.model.SelectedZoomLevels; +import mobac.program.model.Settings; +import mobac.program.model.SettingsWgsGrid; +import mobac.program.model.TileImageParameters; +import mobac.utilities.GBC; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +public class MainGUI extends JFrame implements MapEventListener { + + private static final long serialVersionUID = 1L; + + private static Logger log = Logger.getLogger(MainGUI.class); + + private static Color labelBackgroundColor = new Color(0, 0, 0, 127); + private static Color checkboxBackgroundColor = new Color(0, 0, 0, 40); + private static Color labelForegroundColor = Color.WHITE; + + private static MainGUI mainGUI = null; + public static final ArrayList MOBAC_ICONS = new ArrayList(3); + + static { + MOBAC_ICONS.add(Utilities.loadResourceImageIcon("mobac48.png").getImage()); + MOBAC_ICONS.add(Utilities.loadResourceImageIcon("mobac32.png").getImage()); + MOBAC_ICONS.add(Utilities.loadResourceImageIcon("mobac16.png").getImage()); + } + + protected JMenuBar menuBar; + protected JMenu toolsMenu = null; + + private JMenu bookmarkMenu = null; + + public final PreviewMap previewMap = new PreviewMap(); + public final JAtlasTree jAtlasTree = new JAtlasTree(previewMap); + + private JCheckBox wgsGridCheckBox; + private JComboBox wgsGridCombo; + + private JLabel zoomLevelText; + private JComboBox gridZoomCombo; + private JSlider zoomSlider; + private JComboBox mapSourceCombo; + private JButton settingsButton; + private JAtlasNameField atlasNameTextField; + private JButton createAtlasButton; + private JPanel zoomLevelPanel; + private JZoomCheckBox[] cbZoom = new JZoomCheckBox[0]; + private JLabel amountOfTilesLabel; + + private AtlasCreate atlasCreateAction = new AtlasCreate(jAtlasTree); + + private JCoordinatesPanel coordinatesPanel; + private JProfilesPanel profilesPanel; + public JTileImageParametersPanel tileImageParametersPanel; + private JTileStoreCoveragePanel tileStoreCoveragePanel; + public JGpxPanel gpxPanel; + + private JPanel mapControlPanel = new JPanel(new BorderLayout()); + private JPanel leftPanel = new JPanel(new GridBagLayout()); + private JPanel leftPanelContent = null; + private JPanel rightPanel = new JPanel(new GridBagLayout()); + + public JMenu logLevelMenu; + private JMenuItem smRectangle; + private JMenuItem smPolygon; + private JMenuItem smCircle; + + private MercatorPixelCoordinate mapSelectionMax = null; + private MercatorPixelCoordinate mapSelectionMin = null; + + public static void createMainGui() { + if (mainGUI != null) + return; + + mainGUI = new MainGUI(); + mainGUI.setVisible(true); + log.trace("MainGUI now visible"); + } + + public static MainGUI getMainGUI() { + return mainGUI; + } + + // MP: get custom font + static Font sCustomFont = null; + + public static Font customFont() { + if (sCustomFont == null) { + // force to use Chinese font + sCustomFont = new Font("宋体", 9, 13); + } + return sCustomFont; + } + + // MP: update all UI components' default font to custom font + public static void setDefaultFontOfAllUIComponents(Font defaultFont) { + if (defaultFont != null) { + // register custom font to application,system font will return false + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + ge.registerFont(defaultFont); + + // update all UI's font settings + javax.swing.plaf.FontUIResource fontRes = new javax.swing.plaf.FontUIResource(defaultFont); + Enumeration keys = UIManager.getDefaults().keys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + Object value = UIManager.get(key); + if (value instanceof javax.swing.plaf.FontUIResource) { + UIManager.put(key, fontRes); + } + } + } + } + + private MainGUI() { + super(); + mainGUI = this; + setIconImages(MOBAC_ICONS); + + GUIExceptionHandler.registerForCurrentThread(); + setTitle(ProgramInfo.getCompleteTitle()); + + log.trace("Creating main dialog - " + getTitle()); + setResizable(true); + Dimension dScreen = Toolkit.getDefaultToolkit().getScreenSize(); + setMinimumSize(new Dimension(Math.min(800, dScreen.width), Math.min(590, dScreen.height))); + setSize(getMinimumSize()); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + addWindowListener(new WindowDestroyer()); + addComponentListener(new MainWindowListener()); + + previewMap.addMapEventListener(this); + + createControls(); + calculateNrOfTilesToDownload(); + setLayout(new BorderLayout()); + add(leftPanel, BorderLayout.WEST); + add(rightPanel, BorderLayout.EAST); + JLayeredPane layeredPane = new FilledLayeredPane(); + layeredPane.add(previewMap, Integer.valueOf(0)); + layeredPane.add(mapControlPanel, Integer.valueOf(1)); + add(layeredPane, BorderLayout.CENTER); + + updateMapControlsPanel(); + updateLeftPanel(); + updateRightPanel(); + updateZoomLevelCheckBoxes(); + calculateNrOfTilesToDownload(); + + menuBar = new JMenuBar(); + prepareMenuBar(); + setJMenuBar(menuBar); + + loadSettings(); + profilesPanel.initialize(); + mapSourceChanged(previewMap.getMapSource()); + updateZoomLevelCheckBoxes(); + updateGridSizeCombo(); + tileImageParametersPanel.updateControlsState(); + zoomChanged(previewMap.getZoom()); + gridZoomChanged(previewMap.getGridZoom()); + previewMap.updateMapSelection(); + previewMap.grabFocus(); + } + + private void createControls() { + + // zoom slider + zoomSlider = new JSlider(JMapViewer.MIN_ZOOM, previewMap.getMapSource().getMaxZoom()); + zoomSlider.setOrientation(JSlider.HORIZONTAL); + zoomSlider.setMinimumSize(new Dimension(50, 10)); + zoomSlider.setSize(50, zoomSlider.getPreferredSize().height); + zoomSlider.addChangeListener(new ZoomSliderListener()); + zoomSlider.setOpaque(false); + + // zoom level text + zoomLevelText = new JLabel(" 00 "); + zoomLevelText.setOpaque(true); + zoomLevelText.setBackground(labelBackgroundColor); + zoomLevelText.setForeground(labelForegroundColor); + zoomLevelText.setToolTipText(I18nUtils.localizedStringForKey("map_ctrl_zoom_level_title_tips")); + + // grid zoom combo + gridZoomCombo = new JComboBox(); + gridZoomCombo.setEditable(false); + gridZoomCombo.addActionListener(new GridZoomComboListener()); + gridZoomCombo.setToolTipText(I18nUtils.localizedStringForKey("map_ctrl_zoom_grid_tips")); + + SettingsWgsGrid s = Settings.getInstance().wgsGrid; + + // WGS Grid label + wgsGridCheckBox = new JCheckBox(I18nUtils.localizedStringForKey("map_ctrl_wgs_grid_title"), s.enabled); + // wgsGridCheckBox.setOpaque(true); + wgsGridCheckBox.setOpaque(true); + wgsGridCheckBox.setBackground(checkboxBackgroundColor); + wgsGridCheckBox.setForeground(labelForegroundColor); + wgsGridCheckBox.setToolTipText(I18nUtils.localizedStringForKey("map_ctrl_wgs_grid_tips")); + wgsGridCheckBox.setMargin(new Insets(0, 0, 0, 0)); + wgsGridCheckBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + boolean enabled = wgsGridCheckBox.isSelected(); + Settings.getInstance().wgsGrid.enabled = enabled; + wgsGridCombo.setVisible(enabled); + previewMap.repaint(); + } + }); + + // WGS Grid combo + wgsGridCombo = new JComboBox(WgsDensity.values()); + wgsGridCombo.setMaximumRowCount(WgsDensity.values().length); + wgsGridCombo.setVisible(s.enabled); + wgsGridCombo.setSelectedItem(s.density); + wgsGridCombo.setToolTipText(I18nUtils.localizedStringForKey("map_ctrl_wgs_grid_density_tips")); + wgsGridCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + WgsDensity d = (WgsDensity) wgsGridCombo.getSelectedItem(); + Settings.getInstance().wgsGrid.density = d; + previewMap.repaint(); + } + }); + + // map source combo + mapSourceCombo = new JComboBox(MapSourcesManager.getInstance().getEnabledOrderedMapSources()); + mapSourceCombo.setMaximumRowCount(20); + mapSourceCombo.addActionListener(new MapSourceComboListener()); + mapSourceCombo.setToolTipText(I18nUtils.localizedStringForKey("lp_map_source_combo_tips")); + + // settings button + settingsButton = new JButton(I18nUtils.localizedStringForKey("lp_main_setting_button_title")); + settingsButton.addActionListener(new SettingsButtonListener()); + settingsButton.setToolTipText(I18nUtils.localizedStringForKey("lp_main_setting_button_tips")); + + // atlas name text field + atlasNameTextField = new JAtlasNameField(); + atlasNameTextField.setColumns(12); + atlasNameTextField.setActionCommand("atlasNameTextField"); + atlasNameTextField.setToolTipText(I18nUtils.localizedStringForKey("lp_atlas_name_field_tips")); + + // main button + createAtlasButton = new JButton(I18nUtils.localizedStringForKey("lp_mian_create_btn_title")); + createAtlasButton.addActionListener(atlasCreateAction); + createAtlasButton.setToolTipText(I18nUtils.localizedStringForKey("lp_main_create_btn_tips")); + + // zoom level check boxes + zoomLevelPanel = new JPanel(); + zoomLevelPanel.setBorder(BorderFactory.createEmptyBorder()); + zoomLevelPanel.setOpaque(false); + + // amount of tiles to download + amountOfTilesLabel = new JLabel(); + amountOfTilesLabel.setToolTipText(I18nUtils.localizedStringForKey("lp_zoom_total_tile_count_tips")); + amountOfTilesLabel.setOpaque(true); + amountOfTilesLabel.setBackground(labelBackgroundColor); + amountOfTilesLabel.setForeground(labelForegroundColor); + + coordinatesPanel = new JCoordinatesPanel(); + tileImageParametersPanel = new JTileImageParametersPanel(); + profilesPanel = new JProfilesPanel(jAtlasTree); + profilesPanel.getLoadButton().addActionListener(new LoadProfileListener()); + tileStoreCoveragePanel = new JTileStoreCoveragePanel(previewMap); + } + + private void prepareMenuBar() { + // Atlas menu + JMenu atlasMenu = new JMenu(I18nUtils.localizedStringForKey("menu_atlas")); + atlasMenu.setMnemonic(KeyEvent.VK_A); + + JMenuItem newAtlas = new JMenuItem(I18nUtils.localizedStringForKey("menu_atlas_new")); + newAtlas.setMnemonic(KeyEvent.VK_N); + newAtlas.addActionListener(new AtlasNew()); + atlasMenu.add(newAtlas); + + JMenuItem convertAtlas = new JMenuItem(I18nUtils.localizedStringForKey("menu_atlas_convert_format")); + convertAtlas.setMnemonic(KeyEvent.VK_V); + convertAtlas.addActionListener(new AtlasConvert()); + atlasMenu.add(convertAtlas); + atlasMenu.addSeparator(); + + JMenuItem createAtlas = new JMenuItem(I18nUtils.localizedStringForKey("menu_atlas_create")); + createAtlas.setMnemonic(KeyEvent.VK_C); + createAtlas.addActionListener(atlasCreateAction); + atlasMenu.add(createAtlas); + + // Maps menu + JMenu mapsMenu = new JMenu(I18nUtils.localizedStringForKey("menu_maps")); + mapsMenu.setMnemonic(KeyEvent.VK_M); + JMenu selectionModeMenu = new JMenu(I18nUtils.localizedStringForKey("menu_maps_selection")); + selectionModeMenu.setMnemonic(KeyEvent.VK_M); + mapsMenu.add(selectionModeMenu); + + smRectangle = new JRadioButtonMenuItem(I18nUtils.localizedStringForKey("menu_maps_selection_rect")); + smRectangle.addActionListener(new SelectionModeRectangle()); + smRectangle.setSelected(true); + selectionModeMenu.add(smRectangle); + + smPolygon = new JRadioButtonMenuItem(I18nUtils.localizedStringForKey("menu_maps_selection_polygon")); + smPolygon.addActionListener(new SelectionModePolygon()); + selectionModeMenu.add(smPolygon); + + smCircle = new JRadioButtonMenuItem(I18nUtils.localizedStringForKey("menu_maps_selection_circle")); + smCircle.addActionListener(new SelectionModeCircle()); + selectionModeMenu.add(smCircle); + + JMenuItem addSelection = new JMenuItem(I18nUtils.localizedStringForKey("menu_maps_selection_add")); + addSelection.addActionListener(AddMapLayer.INSTANCE); + addSelection.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK)); + addSelection.setMnemonic(KeyEvent.VK_A); + mapsMenu.add(addSelection); + + JMenuItem addGpxTrackSelection = new JMenuItem2( + I18nUtils.localizedStringForKey("menu_maps_selection_add_around_gpx"), AddGpxTrackPolygonMap.class); + mapsMenu.add(addGpxTrackSelection); + + JMenuItem addGpxTrackAreaSelection = new JMenuItem2( + I18nUtils.localizedStringForKey("menu_maps_selection_add_by_gpx"), + AddGpxTrackAreaPolygonMap.class); + mapsMenu.add(addGpxTrackAreaSelection); + + // Bookmarks menu + bookmarkMenu = new JMenu(I18nUtils.localizedStringForKey("menu_bookmark")); + bookmarkMenu.setMnemonic(KeyEvent.VK_B); + JMenuItem addBookmark = new JMenuItem(I18nUtils.localizedStringForKey("menu_bookmark_save")); + addBookmark.setMnemonic(KeyEvent.VK_S); + addBookmark.addActionListener(new BookmarkAdd(previewMap)); + bookmarkMenu.add(addBookmark); + JMenuItem manageBookmarks = new JMenuItem2(I18nUtils.localizedStringForKey("menu_bookmark_manage"), + BookmarkManage.class); + manageBookmarks.setMnemonic(KeyEvent.VK_S); + bookmarkMenu.add(addBookmark); + bookmarkMenu.add(manageBookmarks); + bookmarkMenu.addSeparator(); + + // Panels menu + JMenu panelsMenu = new JMenu(I18nUtils.localizedStringForKey("menu_panels")); + panelsMenu.setMnemonic(KeyEvent.VK_P); + JMenuItem showLeftPanel = new JMenuItem(I18nUtils.localizedStringForKey("menu_show_hide_left_panel")); + showLeftPanel.addActionListener(new PanelShowHide(leftPanel)); + JMenuItem showRightPanel = new JMenuItem(I18nUtils.localizedStringForKey("menu_show_hide_gpx_panel")); + showRightPanel.addActionListener(new PanelShowHide(rightPanel)); + panelsMenu.add(showLeftPanel); + panelsMenu.add(showRightPanel); + + menuBar.add(atlasMenu); + menuBar.add(mapsMenu); + menuBar.add(bookmarkMenu); + menuBar.add(panelsMenu); + + loadToolsMenu(); + + menuBar.add(Box.createHorizontalGlue()); + + // Debug menu + JMenu debugMenu = new JMenu(I18nUtils.localizedStringForKey("menu_debug")); + JMenuItem mapGrid = new JCheckBoxMenuItem(I18nUtils.localizedStringForKey("menu_debug_show_hide_tile_border"), + false); + mapGrid.addActionListener(new DebugShowMapTileGrid()); + debugMenu.add(mapGrid); + debugMenu.addSeparator(); + + debugMenu.setMnemonic(KeyEvent.VK_D); + JMenuItem mapSourceNames = new JMenuItem2(I18nUtils.localizedStringForKey("menu_debug_show_all_map_source"), + DebugShowMapSourceNames.class); + mapSourceNames.setMnemonic(KeyEvent.VK_N); + debugMenu.add(mapSourceNames); + debugMenu.addSeparator(); + + JMenuItem refreshCustomMapSources = new JMenuItem2( + I18nUtils.localizedStringForKey("menu_debug_refresh_map_source"), RefreshCustomMapsources.class); + debugMenu.add(refreshCustomMapSources); + debugMenu.addSeparator(); + JMenuItem showLog = new JMenuItem2(I18nUtils.localizedStringForKey("menu_debug_show_log_file"), + DebugShowLogFile.class); + showLog.setMnemonic(KeyEvent.VK_S); + debugMenu.add(showLog); + + logLevelMenu = new JMenu(I18nUtils.localizedStringForKey("menu_debug_log_level")); + logLevelMenu.setMnemonic(KeyEvent.VK_L); + Level[] list = new Level[] { Level.TRACE, Level.DEBUG, Level.INFO, Level.ERROR, Level.FATAL, Level.OFF }; + ActionListener al = new DebugSetLogLevel(); + Level rootLogLevel = Logger.getRootLogger().getLevel(); + for (Level level : list) { + String name = level.toString(); + JRadioButtonMenuItem item = new JRadioButtonMenuItem(name, (rootLogLevel.toString().equals(name))); + item.setName(name); + item.addActionListener(al); + logLevelMenu.add(item); + } + debugMenu.add(logLevelMenu); + debugMenu.addSeparator(); + JMenuItem report = new JMenuItem2(I18nUtils.localizedStringForKey("menu_debug_system_report"), + DebugShowReport.class); + report.setMnemonic(KeyEvent.VK_R); + debugMenu.add(report); + menuBar.add(debugMenu); + + // Help menu + JMenu help = new JMenu(I18nUtils.localizedStringForKey("menu_help")); + JMenuItem readme = new JMenuItem(I18nUtils.localizedStringForKey("menu_help_readme")); + JMenuItem howToMap = new JMenuItem(I18nUtils.localizedStringForKey("menu_help_how_to_preview")); + JMenuItem licenses = new JMenuItem(I18nUtils.localizedStringForKey("menu_help_licenses")); + JMenuItem about = new JMenuItem(I18nUtils.localizedStringForKey("menu_help_about")); + readme.addActionListener(new ShowReadme()); + about.addActionListener(new ShowAboutDialog()); + howToMap.addActionListener(new ShowHelpAction()); + licenses.addActionListener(new HelpLicenses()); + help.add(readme); + help.add(howToMap); + help.addSeparator(); + help.add(licenses); + help.addSeparator(); + help.add(about); + + menuBar.add(help); + } + + public void loadToolsMenu() { + if (ExternalToolsLoader.load()) { + if (toolsMenu == null) { + toolsMenu = new JMenu(I18nUtils.localizedStringForKey("menu_tool")); + toolsMenu.addMenuListener(new MenuListener() { + + public void menuSelected(MenuEvent e) { + loadToolsMenu(); + log.debug("Tools menu Loaded"); + } + + public void menuDeselected(MenuEvent e) { + } + + public void menuCanceled(MenuEvent e) { + } + }); + menuBar.add(toolsMenu); + } + toolsMenu.removeAll(); + for (ExternalToolDef t : ExternalToolsLoader.tools) { + JMenuItem m = new JMenuItem(t.name); + m.addActionListener(t); + toolsMenu.add(m); + } + } + } + + private void updateLeftPanel() { + leftPanel.removeAll(); + + coordinatesPanel.addButtonActionListener(new ApplySelectionButtonListener()); + + JCollapsiblePanel mapSourcePanel = new JCollapsiblePanel( + I18nUtils.localizedStringForKey("lp_map_source_title"), new GridBagLayout()); + mapSourcePanel.addContent(mapSourceCombo, GBC.std().insets(2, 2, 2, 2).fill()); + + JCollapsiblePanel zoomLevelsPanel = new JCollapsiblePanel(I18nUtils.localizedStringForKey("lp_zoom_title"), + new GridBagLayout()); + zoomLevelsPanel.addContent(zoomLevelPanel, GBC.eol().insets(2, 4, 2, 0)); + zoomLevelsPanel.addContent(amountOfTilesLabel, GBC.std().anchor(GBC.WEST).insets(0, 5, 0, 2)); + + GBC gbc_std = GBC.std().insets(5, 2, 5, 3); + GBC gbc_eol = GBC.eol().insets(5, 2, 5, 3); + + JCollapsiblePanel atlasContentPanel = new JCollapsiblePanel(I18nUtils.localizedStringForKey("lp_atlas_title"), + new GridBagLayout()); + JScrollPane treeScrollPane = new JScrollPane(jAtlasTree, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + jAtlasTree.getTreeModel().addTreeModelListener(new AtlasModelListener(jAtlasTree, profilesPanel)); + + treeScrollPane.setMinimumSize(new Dimension(100, 150)); + treeScrollPane.setPreferredSize(new Dimension(100, 200)); + treeScrollPane.setAutoscrolls(true); + atlasContentPanel.addContent(treeScrollPane, GBC.eol().fill().insets(0, 1, 0, 0)); + JButton clearAtlas = new JButton(I18nUtils.localizedStringForKey("lp_atlas_new_btn_title")); + atlasContentPanel.addContent(clearAtlas, GBC.std()); + clearAtlas.addActionListener(new AtlasNew()); + JButton addLayers = new JButton(I18nUtils.localizedStringForKey("lp_atlas_add_selection_btn_title")); + atlasContentPanel.addContent(addLayers, GBC.eol()); + addLayers.addActionListener(AddMapLayer.INSTANCE); + atlasContentPanel.addContent(new JLabel(I18nUtils.localizedStringForKey("lp_atlas_name_label_title")), gbc_std); + atlasContentPanel.addContent(atlasNameTextField, gbc_eol.fill(GBC.HORIZONTAL)); + + gbc_eol = GBC.eol().insets(5, 2, 5, 2).fill(GBC.HORIZONTAL); + + leftPanelContent = new JPanel(new GridBagLayout()); + leftPanelContent.add(coordinatesPanel, gbc_eol); + leftPanelContent.add(mapSourcePanel, gbc_eol); + leftPanelContent.add(zoomLevelsPanel, gbc_eol); + leftPanelContent.add(tileImageParametersPanel, gbc_eol); + leftPanelContent.add(atlasContentPanel, gbc_eol); + + leftPanelContent.add(profilesPanel, gbc_eol); + leftPanelContent.add(createAtlasButton, gbc_eol); + leftPanelContent.add(settingsButton, gbc_eol); + leftPanelContent.add(tileStoreCoveragePanel, gbc_eol); + leftPanelContent.add(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL)); + + JScrollPane scrollPane = new JScrollPane(leftPanelContent); + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setBorder(BorderFactory.createEmptyBorder()); + // Set the scroll pane width large enough so that the + // scroll bar has enough space to appear right to it + Dimension d = scrollPane.getPreferredSize(); + d.width += 5 + scrollPane.getVerticalScrollBar().getWidth(); + // scrollPane.setPreferredSize(d); + scrollPane.setMinimumSize(d); + leftPanel.add(scrollPane, GBC.std().fill()); + // leftPanel.add(leftPanelContent, GBC.std().fill()); + + } + + private void updateRightPanel() { + GBC gbc_eol = GBC.eol().insets(5, 2, 5, 2).fill(); + gpxPanel = new JGpxPanel(previewMap); + rightPanel.add(gpxPanel, gbc_eol); + } + + private JPanel updateMapControlsPanel() { + mapControlPanel.removeAll(); + mapControlPanel.setOpaque(false); + + // zoom label + JLabel zoomLabel = new JLabel(I18nUtils.localizedStringForKey("map_ctrl_zoom_level_title")); + zoomLabel.setOpaque(true); + zoomLabel.setBackground(labelBackgroundColor); + zoomLabel.setForeground(labelForegroundColor); + + // top panel + JPanel topControls = new JPanel(new GridBagLayout()); + topControls.setOpaque(false); + topControls.add(zoomLabel, GBC.std().insets(5, 5, 0, 0)); + topControls.add(zoomSlider, GBC.std().insets(0, 5, 0, 0)); + topControls.add(zoomLevelText, GBC.std().insets(0, 5, 0, 0)); + topControls.add(gridZoomCombo, GBC.std().insets(10, 5, 0, 0)); + topControls.add(wgsGridCheckBox, GBC.std().insets(10, 5, 0, 0)); + topControls.add(wgsGridCombo, GBC.std().insets(5, 5, 0, 0)); + topControls.add(Box.createHorizontalGlue(), GBC.std().fillH()); + mapControlPanel.add(topControls, BorderLayout.NORTH); + + // bottom panel + // JPanel bottomControls = new JPanel(new GridBagLayout()); + // bottomControls.setOpaque(false); + // bottomControls.add(Box.createHorizontalGlue(), + // GBC.std().fill(GBC.HORIZONTAL)); + // mapControlPanel.add(bottomControls, BorderLayout.SOUTH); + + return mapControlPanel; + } + + public void updateMapSourcesList() { + MapSource ms = (MapSource) mapSourceCombo.getSelectedItem(); + mapSourceCombo + .setModel(new DefaultComboBoxModel(MapSourcesManager.getInstance().getEnabledOrderedMapSources())); + mapSourceCombo.setSelectedItem(ms); + MapSource ms2 = (MapSource) mapSourceCombo.getSelectedItem(); + if (!ms.equals(ms2)) + previewMap.setMapSource(ms2); + } + + public void updateBookmarksMenu() { + LinkedList items = new LinkedList(); + for (int i = 0; i < bookmarkMenu.getMenuComponentCount(); i++) { + JMenuItem item = bookmarkMenu.getItem(i); + if (!(item instanceof JBookmarkMenuItem)) + items.add(item); + } + bookmarkMenu.removeAll(); + for (JMenuItem item : items) { + if (item != null) + bookmarkMenu.add(item); + else + bookmarkMenu.addSeparator(); + } + for (Bookmark b : Settings.getInstance().placeBookmarks) { + bookmarkMenu.add(new JBookmarkMenuItem(b)); + } + } + + private void loadSettings() { + if (Profile.DEFAULT.exists()) { + try { + jAtlasTree.load(Profile.DEFAULT); + } catch (Exception e) { + log.error("Failed to load atlas", e); + GUIExceptionHandler.processException(e); + new AtlasNew().actionPerformed(null); + } + } else + new AtlasNew().actionPerformed(null); + + Settings settings = Settings.getInstance(); + atlasNameTextField.setText(settings.elementName); + previewMap.settingsLoad(); + int nextZoom = 0; + List zoomList = settings.selectedZoomLevels; + if (zoomList != null) { + for (JZoomCheckBox currentZoomCb : cbZoom) { + for (int i = nextZoom; i < zoomList.size(); i++) { + int currentListZoom = zoomList.get(i); + if (currentZoomCb.getZoomLevel() == currentListZoom) { + currentZoomCb.setSelected(true); + nextZoom = 1; + break; + } + } + } + } + coordinatesPanel.setNumberFormat(settings.coordinateNumberFormat); + + tileImageParametersPanel.loadSettings(); + tileImageParametersPanel.atlasFormatChanged(jAtlasTree.getAtlas().getOutputFormat()); + // mapSourceCombo + // .setSelectedItem(MapSourcesManager.getSourceByName(settings. + // mapviewMapSource)); + + setSize(settings.mainWindow.size); + Point windowLocation = settings.mainWindow.position; + if (windowLocation.x == -1 && windowLocation.y == -1) { + setLocationRelativeTo(null); + } else { + setLocation(windowLocation); + } + if (settings.mainWindow.maximized) + setExtendedState(Frame.MAXIMIZED_BOTH); + + leftPanel.setVisible(settings.mainWindow.leftPanelVisible); + rightPanel.setVisible(settings.mainWindow.rightPanelVisible); + + if (leftPanelContent != null) { + for (Component c : leftPanelContent.getComponents()) { + if (c instanceof JCollapsiblePanel) { + JCollapsiblePanel cp = (JCollapsiblePanel) c; + String name = cp.getName(); + if (name != null && settings.mainWindow.collapsedPanels.contains(name)) + cp.setCollapsed(true); + } + } + } + + updateBookmarksMenu(); + } + + private void saveSettings() { + try { + jAtlasTree.save(Profile.DEFAULT); + + Settings s = Settings.getInstance(); + previewMap.settingsSave(); + s.mapviewMapSource = previewMap.getMapSource().getName(); + s.selectedZoomLevels = new SelectedZoomLevels(cbZoom).getZoomLevelList(); + + s.elementName = atlasNameTextField.getText(); + s.coordinateNumberFormat = coordinatesPanel.getNumberFormat(); + + tileImageParametersPanel.saveSettings(); + boolean maximized = (getExtendedState() & Frame.MAXIMIZED_BOTH) != 0; + s.mainWindow.maximized = maximized; + if (!maximized) { + s.mainWindow.size = getSize(); + s.mainWindow.position = getLocation(); + } + s.mainWindow.collapsedPanels.clear(); + if (leftPanelContent != null) { + for (Component c : leftPanelContent.getComponents()) { + if (c instanceof JCollapsiblePanel) { + JCollapsiblePanel cp = (JCollapsiblePanel) c; + if (cp.isCollapsed()) + s.mainWindow.collapsedPanels.add(cp.getName()); + } + } + } + s.mainWindow.leftPanelVisible = leftPanel.isVisible(); + s.mainWindow.rightPanelVisible = rightPanel.isVisible(); + checkAndSaveSettings(); + } catch (Exception e) { + GUIExceptionHandler.showExceptionDialog(e); + JOptionPane.showMessageDialog(null, I18nUtils.localizedStringForKey("msg_settings_write_error"), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + } + } + + public void checkAndSaveSettings() throws JAXBException { + if (Settings.checkSettingsFileModified()) { + int x = JOptionPane.showConfirmDialog(this, + I18nUtils.localizedStringForKey("msg_setting_file_is_changed_by_other"), + I18nUtils.localizedStringForKey("msg_setting_file_is_changed_by_other_title"), + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (x != JOptionPane.YES_OPTION) + return; + } + Settings.save(); + + } + + public JTileImageParametersPanel getParametersPanel() { + return tileImageParametersPanel; + } + + public String getUserText() { + return atlasNameTextField.getText(); + } + + public void refreshPreviewMap() { + previewMap.refreshMap(); + } + + private class ZoomSliderListener implements ChangeListener { + public void stateChanged(ChangeEvent e) { + previewMap.setZoom(zoomSlider.getValue()); + } + } + + private class GridZoomComboListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + if (!gridZoomCombo.isEnabled()) + return; + GridZoom g = (GridZoom) gridZoomCombo.getSelectedItem(); + if (g == null) + return; + log.debug("Selected grid zoom combo box item has changed: " + g.getZoom()); + previewMap.setGridZoom(g.getZoom()); + repaint(); + previewMap.updateMapSelection(); + } + } + + private void updateGridSizeCombo() { + int maxZoom = previewMap.getMapSource().getMaxZoom(); + int minZoom = previewMap.getMapSource().getMinZoom(); + GridZoom lastGridZoom = (GridZoom) gridZoomCombo.getSelectedItem(); + gridZoomCombo.setEnabled(false); + gridZoomCombo.removeAllItems(); + gridZoomCombo.setMaximumRowCount(maxZoom - minZoom + 2); + gridZoomCombo.addItem(new GridZoom(-1) { + + @Override + public String toString() { + return I18nUtils.localizedStringForKey("map_ctrl_zoom_grid_disable"); + } + + }); + for (int i = maxZoom; i >= minZoom; i--) { + gridZoomCombo.addItem(new GridZoom(i)); + } + if (lastGridZoom != null) + gridZoomCombo.setSelectedItem(lastGridZoom); + gridZoomCombo.setEnabled(true); + } + + private class ApplySelectionButtonListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + setSelectionByEnteredCoordinates(); + } + } + + private class MapSourceComboListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + MapSource mapSource = (MapSource) mapSourceCombo.getSelectedItem(); + if (mapSource == null) { + mapSourceCombo.setSelectedIndex(0); + mapSource = (MapSource) mapSourceCombo.getSelectedItem(); + } + if (mapSource instanceof InitializableMapSource) + // initialize the map source e.g. detect available zoom levels + ((InitializableMapSource) mapSource).initialize(); + previewMap.setMapSource(mapSource); + zoomSlider.setMinimum(previewMap.getMapSource().getMinZoom()); + zoomSlider.setMaximum(previewMap.getMapSource().getMaxZoom()); + updateGridSizeCombo(); + updateZoomLevelCheckBoxes(); + calculateNrOfTilesToDownload(); + } + } + + private class LoadProfileListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + Profile profile = profilesPanel.getSelectedProfile(); + profilesPanel.getDeleteButton().setEnabled(profile != null); + if (profile == null) + return; + + jAtlasTree.load(profile); + previewMap.repaint(); + tileImageParametersPanel.atlasFormatChanged(jAtlasTree.getAtlas().getOutputFormat()); + } + } + + private class SettingsButtonListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + SettingsGUI.showSettingsDialog(MainGUI.this); + } + } + + private void updateZoomLevelCheckBoxes() { + MapSource tileSource = previewMap.getMapSource(); + int zoomLevels = tileSource.getMaxZoom() - tileSource.getMinZoom() + 1; + zoomLevels = Math.max(zoomLevels, 0); + JCheckBox oldZoomLevelCheckBoxes[] = cbZoom; + int oldMinZoom = 0; + if (cbZoom.length > 0) + oldMinZoom = cbZoom[0].getZoomLevel(); + cbZoom = new JZoomCheckBox[zoomLevels]; + zoomLevelPanel.removeAll(); + + zoomLevelPanel.setLayout(new GridLayout(0, 10, 1, 2)); + ZoomLevelCheckBoxListener cbl = new ZoomLevelCheckBoxListener(); + + for (int i = cbZoom.length - 1; i >= 0; i--) { + int cbz = i + tileSource.getMinZoom(); + JZoomCheckBox cb = new JZoomCheckBox(cbz); + cb.setPreferredSize(new Dimension(22, 11)); + cb.setMinimumSize(cb.getPreferredSize()); + cb.setOpaque(false); + cb.setFocusable(false); + cb.setName(Integer.toString(cbz)); + int oldCbIndex = cbz - oldMinZoom; + if (oldCbIndex >= 0 && oldCbIndex < (oldZoomLevelCheckBoxes.length)) + cb.setSelected(oldZoomLevelCheckBoxes[oldCbIndex].isSelected()); + cb.addActionListener(cbl); + // cb.setToolTipText("Select zoom level " + cbz + " for atlas"); + zoomLevelPanel.add(cb); + cbZoom[i] = cb; + + JLabel l = new JLabel(Integer.toString(cbz)); + zoomLevelPanel.add(l); + } + amountOfTilesLabel.setOpaque(false); + amountOfTilesLabel.setForeground(Color.black); + } + + private class ZoomLevelCheckBoxListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + calculateNrOfTilesToDownload(); + } + } + + public void selectionChanged(MercatorPixelCoordinate max, MercatorPixelCoordinate min) { + mapSelectionMax = max; + mapSelectionMin = min; + coordinatesPanel.setSelection(max, min); + calculateNrOfTilesToDownload(); + } + + public void zoomChanged(int zoomLevel) { + zoomLevelText.setText(" " + zoomLevel + " "); + zoomSlider.setValue(zoomLevel); + } + + public void gridZoomChanged(int newGridZoomLevel) { + gridZoomCombo.setSelectedItem(new GridZoom(newGridZoomLevel)); + } + + public MapSource getSelectedMapSource() { + return (MapSource) mapSourceCombo.getSelectedItem(); + } + + public SelectedZoomLevels getSelectedZoomLevels() { + return new SelectedZoomLevels(cbZoom); + } + + public void selectNextMapSource() { + if (mapSourceCombo.getSelectedIndex() == mapSourceCombo.getItemCount() - 1) { + Toolkit.getDefaultToolkit().beep(); + } else { + mapSourceCombo.setSelectedIndex(mapSourceCombo.getSelectedIndex() + 1); + } + } + + public void selectPreviousMapSource() { + if (mapSourceCombo.getSelectedIndex() == 0) { + Toolkit.getDefaultToolkit().beep(); + } else { + mapSourceCombo.setSelectedIndex(mapSourceCombo.getSelectedIndex() - 1); + } + } + + public void mapSourceChanged(MapSource newMapSource) { + // TODO update selected area if new map source has different projectionCategory + calculateNrOfTilesToDownload(); + // if (newMapSource != null && newMapSource.equals(mapSourceCombo.getSelectedItem())) + // return; + mapSourceCombo.setSelectedItem(newMapSource); + } + + public void mapSelectionControllerChanged(JMapController newMapController) { + smPolygon.setSelected(false); + smCircle.setSelected(false); + smRectangle.setSelected(false); + if (newMapController instanceof PolygonSelectionMapController) + smPolygon.setSelected(true); + else if (newMapController instanceof PolygonCircleSelectionMapController) + smCircle.setSelected(true); + else if (newMapController instanceof RectangleSelectionMapController) + smRectangle.setSelected(true); + } + + private void setSelectionByEnteredCoordinates() { + coordinatesPanel.correctMinMax(); + MapSelection ms = coordinatesPanel.getMapSelection(previewMap.getMapSource()); + mapSelectionMax = ms.getBottomRightPixelCoordinate(); + mapSelectionMin = ms.getTopLeftPixelCoordinate(); + previewMap.setSelectionAndZoomTo(ms, false); + } + + public MapSelection getMapSelectionCoordinates() { + if (mapSelectionMax == null || mapSelectionMin == null) + return null; + return new MapSelection(previewMap.getMapSource(), mapSelectionMax, mapSelectionMin); + } + + public TileImageParameters getSelectedTileImageParameters() { + return tileImageParametersPanel.getSelectedTileImageParameters(); + } + + private void calculateNrOfTilesToDownload() { + MapSelection ms = getMapSelectionCoordinates(); + String baseText; + baseText = I18nUtils.localizedStringForKey("lp_zoom_total_tile_title"); + if (ms == null || !ms.isAreaSelected()) { + amountOfTilesLabel.setText(String.format(baseText, "0")); + amountOfTilesLabel.setToolTipText(""); + } else { + try { + SelectedZoomLevels sZL = new SelectedZoomLevels(cbZoom); + + int[] zoomLevels = sZL.getZoomLevels(); + + long totalNrOfTiles = 0; + + StringBuilder hint = new StringBuilder(1024); + hint.append(I18nUtils.localizedStringForKey("lp_zoom_total_tile_hint_head")); + for (int i = 0; i < zoomLevels.length; i++) { + int zoom = zoomLevels[i]; + long[] info = ms.calculateNrOfTilesEx(zoom); + totalNrOfTiles += info[0]; + hint.append(String.format(I18nUtils.localizedStringForKey("lp_zoom_total_tile_hint_row"), + zoomLevels[i], info[0], info[1], info[2])); + // hint.append("
Level " + zoomLevels[i] + ": " + info[0] + " (" + info[1] + "*" + info[2] + + // ")"); + } + String hintText = "" + hint.toString() + ""; + amountOfTilesLabel.setText(String.format(baseText, Long.toString(totalNrOfTiles))); + amountOfTilesLabel.setToolTipText(hintText); + } catch (Exception e) { + amountOfTilesLabel.setText(String.format(baseText, "?")); + log.error("", e); + } + } + } + + public AtlasInterface getAtlas() { + return jAtlasTree.getAtlas(); + } + + private class WindowDestroyer extends WindowAdapter { + + @Override + public void windowOpened(WindowEvent e) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + previewMap.setEnabled(true); + } + }); + } + + public void windowClosing(WindowEvent event) { + saveSettings(); + } + } + + /** + * Saves the window position and size when window is moved or resized. This is necessary because of the maximized + * state. If a window is maximized it is impossible to retrieve the window size & position of the non-maximized + * window - therefore we have to collect the information every time they change. + */ + private class MainWindowListener extends ComponentAdapter { + public void componentResized(ComponentEvent event) { + // log.debug(event.paramString()); + updateValues(); + } + + public void componentMoved(ComponentEvent event) { + // log.debug(event.paramString()); + updateValues(); + } + + private void updateValues() { + // only update old values while window is in NORMAL state + // Note(Java bug): Sometimes getExtendedState() says the window is + // not maximized but maximizing is already in progress and therefore + // the window bounds are already changed. + if ((getExtendedState() & MAXIMIZED_BOTH) != 0) + return; + Settings s = Settings.getInstance(); + s.mainWindow.size = getSize(); + s.mainWindow.position = getLocation(); + } + } + +} diff --git a/src/main/java/mobac/gui/SplashFrame.java b/src/main/java/mobac/gui/SplashFrame.java new file mode 100644 index 0000000..147f2c2 --- /dev/null +++ b/src/main/java/mobac/gui/SplashFrame.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui; + +import java.awt.Dimension; +import java.awt.HeadlessException; +import java.awt.Toolkit; + +import javax.swing.BorderFactory; +import javax.swing.JFrame; +import javax.swing.JLabel; + +import mobac.utilities.Utilities; + +public class SplashFrame extends JFrame { + + private static SplashFrame startupFrame; + + public static void showFrame() { + startupFrame = new SplashFrame(); + } + + public SplashFrame() throws HeadlessException { + //super(MainGUI.localizedStringForKey("splash_title")); + super("MOBAC"); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setUndecorated(true); + setIconImages(MainGUI.MOBAC_ICONS); + JLabel image = new JLabel(Utilities.loadResourceImageIcon("Splash.jpg")); + image.setBorder(BorderFactory.createEtchedBorder()); + add(image); + pack(); + setFocusable(false); + Dimension dScreen = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation((dScreen.width - getWidth()) / 2, (dScreen.height - getHeight()) / 2); + setSize(getMinimumSize()); + setVisible(true); + } + + public static void hideFrame() { + if (startupFrame == null) + return; + startupFrame.setVisible(false); + startupFrame.dispose(); + startupFrame = null; + } + + public static void main(String[] arg) { + new SplashFrame(); + } +} diff --git a/src/main/java/mobac/gui/actions/AddGpxTrackAreaPolygonMap.java b/src/main/java/mobac/gui/actions/AddGpxTrackAreaPolygonMap.java new file mode 100644 index 0000000..d289617 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AddGpxTrackAreaPolygonMap.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Polygon; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +import javax.swing.JOptionPane; + +import mobac.data.gpx.gpx11.TrkType; +import mobac.data.gpx.gpx11.TrksegType; +import mobac.data.gpx.interfaces.GpxPoint; +import mobac.exceptions.InvalidNameException; +import mobac.gui.MainGUI; +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.gpxtree.GpxRootEntry; +import mobac.gui.gpxtree.TrkEntry; +import mobac.gui.gpxtree.TrksegEntry; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.Layer; +import mobac.program.model.MapPolygon; +import mobac.program.model.SelectedZoomLevels; +import mobac.program.model.Settings; +import mobac.program.model.TileImageParameters; +import mobac.utilities.I18nUtils; + +public class AddGpxTrackAreaPolygonMap implements ActionListener { + + public static final AddGpxTrackAreaPolygonMap INSTANCE = new AddGpxTrackAreaPolygonMap(); + + public void actionPerformed(ActionEvent event) { + MainGUI mg = MainGUI.getMainGUI(); + GpxEntry entry = mg.gpxPanel.getSelectedEntry(); + + if (entry == null) + return; + + TrksegType trk = null; + TrkType t = null; + if (entry instanceof TrksegEntry) { + trk = ((TrksegEntry) entry).getTrkSeg(); + } else if (entry instanceof GpxRootEntry) { + GpxRootEntry re = (GpxRootEntry) entry; + List tlist = re.getLayer().getGpx().getTrk(); + if (tlist.size() > 1) { + JOptionPane + .showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_add_gpx_polygon_too_many_track")); + return; + } else if (tlist.size() == 1) + t = tlist.get(0); + } + if (entry instanceof TrkEntry) + t = ((TrkEntry) entry).getTrk(); + if (t != null) { + if (t.getTrkseg().size() > 1) { + JOptionPane.showMessageDialog(mg, + I18nUtils.localizedStringForKey("msg_add_gpx_polygon_too_many_segment")); + return; + } else if (t.getTrkseg().size() == 1) + trk = t.getTrkseg().get(0); + } + if (trk == null) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_add_gpx_polygon_no_select"), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + return; + } + + JAtlasTree jAtlasTree = mg.jAtlasTree; + final String mapNameFmt = "%s %02d"; + AtlasInterface atlasInterface = jAtlasTree.getAtlas(); + String name = mg.getUserText(); + MapSource mapSource = mg.getSelectedMapSource(); + SelectedZoomLevels sZL = mg.getSelectedZoomLevels(); + int[] zoomLevels = sZL.getZoomLevels(); + if (zoomLevels.length == 0) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_no_zoom_level_selected")); + return; + } + List points = trk.getTrkpt(); + EastNorthCoordinate[] trackPoints = new EastNorthCoordinate[points.size()]; + EastNorthCoordinate minCoordinate = new EastNorthCoordinate(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + EastNorthCoordinate maxCoordinate = new EastNorthCoordinate(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + for (int i = 0; i < trackPoints.length; i++) { + GpxPoint gpxPoint = points.get(i); + EastNorthCoordinate c = new EastNorthCoordinate(gpxPoint.getLat().doubleValue(), gpxPoint.getLon() + .doubleValue()); + minCoordinate.lat = Math.min(minCoordinate.lat, c.lat); + minCoordinate.lon = Math.min(minCoordinate.lon, c.lon); + maxCoordinate.lat = Math.max(maxCoordinate.lat, c.lat); + maxCoordinate.lon = Math.max(maxCoordinate.lon, c.lon); + trackPoints[i] = c; + } + + final int maxZoom = zoomLevels[zoomLevels.length - 1]; + final MapSpace mapSpace = mapSource.getMapSpace(); + + String layerName = name; + int c = 1; + Layer layer = null; + boolean success = false; + do { + try { + layer = new Layer(atlasInterface, layerName); + success = true; + } catch (InvalidNameException e) { + layerName = name + "_" + Integer.toString(c++); + } + } while (!success); + + TileImageParameters customTileParameters = mg.getSelectedTileImageParameters(); + + int[] xPoints = new int[trackPoints.length]; + int[] yPoints = new int[trackPoints.length]; + for (int i = 0; i < trackPoints.length; i++) { + EastNorthCoordinate coord = trackPoints[i]; + xPoints[i] = mapSpace.cLonToX(coord.lon, maxZoom); + yPoints[i] = mapSpace.cLatToY(coord.lat, maxZoom); + } + + Polygon p = new Polygon(xPoints, yPoints, xPoints.length); + MapPolygon maxZoomMap = new MapPolygon(null, "Dummy", mapSource, maxZoom, p, customTileParameters); + + int width = maxZoomMap.getMaxTileCoordinate().x - maxZoomMap.getMinTileCoordinate().x; + int height = maxZoomMap.getMaxTileCoordinate().y - maxZoomMap.getMinTileCoordinate().y; + if (Math.max(width, height) > Settings.getInstance().maxMapSize) { + String msg = I18nUtils.localizedStringForKey("msg_add_gpx_polygon_maxsize"); + int result = JOptionPane.showConfirmDialog(mg, msg, + I18nUtils.localizedStringForKey("msg_add_gpx_polygon_maxsize_title"), JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (result != JOptionPane.YES_OPTION) + return; + } + + for (int zoom : zoomLevels) { + String mapName = String.format(mapNameFmt, new Object[] { layerName, zoom }); + MapInterface map = MapPolygon.createFromMapPolygon(layer, mapName, zoom, maxZoomMap); + layer.addMap(map); + } + atlasInterface.addLayer(layer); + mg.jAtlasTree.getTreeModel().notifyNodeInsert(layer); + + } +} diff --git a/src/main/java/mobac/gui/actions/AddGpxTrackPolygonMap.java b/src/main/java/mobac/gui/actions/AddGpxTrackPolygonMap.java new file mode 100644 index 0000000..28df13c --- /dev/null +++ b/src/main/java/mobac/gui/actions/AddGpxTrackPolygonMap.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import mobac.data.gpx.gpx11.TrkType; +import mobac.data.gpx.gpx11.TrksegType; +import mobac.data.gpx.interfaces.GpxPoint; +import mobac.exceptions.InvalidNameException; +import mobac.gui.MainGUI; +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.components.JDistanceSlider; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.gpxtree.GpxRootEntry; +import mobac.gui.gpxtree.TrkEntry; +import mobac.gui.gpxtree.TrksegEntry; +import mobac.gui.mapview.layer.MapAreaHighlightingLayer; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.Layer; +import mobac.program.model.MapPolygon; +import mobac.program.model.SelectedZoomLevels; +import mobac.program.model.Settings; +import mobac.program.model.TileImageParameters; +import mobac.program.model.UnitSystem; +import mobac.utilities.I18nUtils; + +public class AddGpxTrackPolygonMap implements ActionListener { + + public static final AddGpxTrackPolygonMap INSTANCE = new AddGpxTrackPolygonMap(); + + private MapAreaHighlightingLayer msl = null; + + public void actionPerformed(ActionEvent event) { + final MainGUI mg = MainGUI.getMainGUI(); + GpxEntry entry = mg.gpxPanel.getSelectedEntry(); + + if (entry == null) + return; + + TrksegType trk = null; + TrkType t = null; + if (entry instanceof TrksegEntry) { + trk = ((TrksegEntry) entry).getTrkSeg(); + } else if (entry instanceof GpxRootEntry) { + GpxRootEntry re = (GpxRootEntry) entry; + List tlist = re.getLayer().getGpx().getTrk(); + if (tlist.size() > 1) { + JOptionPane + .showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_add_gpx_polygon_too_many_track")); + return; + } else if (tlist.size() == 1) + t = tlist.get(0); + } + if (entry instanceof TrkEntry) + t = ((TrkEntry) entry).getTrk(); + if (t != null) { + if (t.getTrkseg().size() > 1) { + JOptionPane.showMessageDialog(mg, + I18nUtils.localizedStringForKey("msg_add_gpx_polygon_too_many_segment")); + return; + } else if (t.getTrkseg().size() == 1) + trk = t.getTrkseg().get(0); + } + if (trk == null) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_add_gpx_polygon_no_select"), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + return; + } + + JAtlasTree jAtlasTree = mg.jAtlasTree; + final String mapNameFmt = "%s %02d"; + AtlasInterface atlasInterface = jAtlasTree.getAtlas(); + String name = mg.getUserText(); + final MapSource mapSource = mg.getSelectedMapSource(); + SelectedZoomLevels sZL = mg.getSelectedZoomLevels(); + int[] zoomLevels = sZL.getZoomLevels(); + if (zoomLevels.length == 0) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_no_zoom_level_selected")); + return; + } + List points = trk.getTrkpt(); + final EastNorthCoordinate[] trackPoints = new EastNorthCoordinate[points.size()]; + EastNorthCoordinate minCoordinate = new EastNorthCoordinate(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + EastNorthCoordinate maxCoordinate = new EastNorthCoordinate(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + for (int i = 0; i < trackPoints.length; i++) { + GpxPoint gpxPoint = points.get(i); + EastNorthCoordinate c = new EastNorthCoordinate(gpxPoint.getLat().doubleValue(), gpxPoint.getLon() + .doubleValue()); + minCoordinate.lat = Math.min(minCoordinate.lat, c.lat); + minCoordinate.lon = Math.min(minCoordinate.lon, c.lon); + maxCoordinate.lat = Math.max(maxCoordinate.lat, c.lat); + maxCoordinate.lon = Math.max(maxCoordinate.lon, c.lon); + trackPoints[i] = c; + } + + final int maxZoom = zoomLevels[zoomLevels.length - 1]; + final MapSpace mapSpace = mapSource.getMapSpace(); + Point p1 = maxCoordinate.toTileCoordinate(mapSpace, maxZoom); + Point p2 = minCoordinate.toTileCoordinate(mapSpace, maxZoom); + + final int centerY = p1.y + ((p1.y - p2.y) / 2); + + final UnitSystem unitSystem = Settings.getInstance().getUnitSystem(); + + final TileImageParameters customTileParameters = mg.getSelectedTileImageParameters(); + + JPanel panel = new JPanel(new BorderLayout()); + panel.setPreferredSize(new Dimension(300, 100)); + final JLabel label = new JLabel(""); + final JDistanceSlider slider = new JDistanceSlider(mapSource.getMapSpace(), maxZoom, centerY, unitSystem, 5, + 500); + ChangeListener cl = new ChangeListener() { + + public void stateChanged(ChangeEvent e) { + double d = mapSpace.horizontalDistance(maxZoom, centerY, slider.getValue()); + d *= unitSystem.earthRadius * unitSystem.unitFactor; + String unitName = unitSystem.unitSmall; + if (d > unitSystem.unitFactor) { + d /= unitSystem.unitFactor; + unitName = unitSystem.unitLarge; + } + label.setText(String.format(I18nUtils.localizedStringForKey("dlg_gpx_track_select_distance"), + ((int) d), unitName)); + } + }; + final JButton previewButton = new JButton(I18nUtils.localizedStringForKey("dlg_gpx_track_select_preview")); + previewButton.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + int distance = slider.getValue(); + MapPolygon maxZoomMap = MapPolygon.createTrackEnclosure(null, "Dummy", mapSource, maxZoom, trackPoints, + distance, customTileParameters); + if (msl != null) + msl.setObject(maxZoomMap); + msl = new MapAreaHighlightingLayer(maxZoomMap); + mg.previewMap.repaint(); + } + }); + + cl.stateChanged(null); + slider.addChangeListener(cl); + panel.add(label, BorderLayout.NORTH); + panel.add(slider, BorderLayout.CENTER); + panel.add(previewButton, BorderLayout.SOUTH); + + int result = JOptionPane.showConfirmDialog(mg, panel, + I18nUtils.localizedStringForKey("dlg_gpx_track_select_title"), JOptionPane.OK_CANCEL_OPTION); + + if (msl != null) { + mg.previewMap.mapLayers.remove(msl); + msl.setObject(null); + } + + if (result != JOptionPane.OK_OPTION) + return; + + int distance = slider.getValue(); + MapPolygon maxZoomMap = MapPolygon.createTrackEnclosure(null, "Dummy", mapSource, maxZoom, trackPoints, + distance, customTileParameters); + + String layerName = name; + int c = 1; + Layer layer = null; + boolean success = false; + do { + try { + layer = new Layer(atlasInterface, layerName); + success = true; + } catch (InvalidNameException e) { + layerName = name + "_" + Integer.toString(c++); + } + } while (!success); + + int width = maxZoomMap.getMaxTileCoordinate().x - maxZoomMap.getMinTileCoordinate().x; + int height = maxZoomMap.getMaxTileCoordinate().y - maxZoomMap.getMinTileCoordinate().y; + if (Math.max(width, height) > Settings.getInstance().maxMapSize) { + String msg = I18nUtils.localizedStringForKey("msg_add_gpx_polygon_maxsize"); + result = JOptionPane.showConfirmDialog(mg, msg, + I18nUtils.localizedStringForKey("msg_add_gpx_polygon_maxsize_title"), JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (result != JOptionPane.YES_OPTION) + return; + } + + for (int zoom : zoomLevels) { + String mapName = String.format(mapNameFmt, new Object[] { layerName, zoom }); + MapInterface map = MapPolygon.createFromMapPolygon(layer, mapName, zoom, maxZoomMap); + layer.addMap(map); + } + atlasInterface.addLayer(layer); + mg.jAtlasTree.getTreeModel().notifyNodeInsert(layer); + + } +} diff --git a/src/main/java/mobac/gui/actions/AddMapLayer.java b/src/main/java/mobac/gui/actions/AddMapLayer.java new file mode 100644 index 0000000..7a2f744 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AddMapLayer.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.controller.AbstractPolygonSelectionMapController; +import mobac.gui.mapview.controller.JMapController; +import mobac.gui.mapview.controller.RectangleSelectionMapController; + +public class AddMapLayer implements ActionListener { + + public static final AddMapLayer INSTANCE = new AddMapLayer(); + + public void actionPerformed(ActionEvent event) { + JMapController msc = MainGUI.getMainGUI().previewMap.getMapSelectionController(); + if (msc instanceof RectangleSelectionMapController) + new AddRectangleMapAutocut().actionPerformed(event); + else if (msc instanceof AbstractPolygonSelectionMapController) + new AddPolygonMapLayer().actionPerformed(event); + else + throw new RuntimeException("Unknown mapSelectionController type"); + } +} diff --git a/src/main/java/mobac/gui/actions/AddPolygonMapLayer.java b/src/main/java/mobac/gui/actions/AddPolygonMapLayer.java new file mode 100644 index 0000000..0db2428 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AddPolygonMapLayer.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Point; +import java.awt.Polygon; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +import javax.swing.JOptionPane; + +import mobac.exceptions.InvalidNameException; +import mobac.gui.MainGUI; +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.controller.AbstractPolygonSelectionMapController; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.Layer; +import mobac.program.model.MapPolygon; +import mobac.program.model.SelectedZoomLevels; +import mobac.program.model.TileImageParameters; +import mobac.utilities.I18nUtils; + +public class AddPolygonMapLayer implements ActionListener { + + public void actionPerformed(ActionEvent event) { + MainGUI mg = MainGUI.getMainGUI(); + + AbstractPolygonSelectionMapController msc = (AbstractPolygonSelectionMapController) mg.previewMap + .getMapSelectionController(); + + JAtlasTree jAtlasTree = mg.jAtlasTree; + final String mapNameFmt = "%s %02d"; + AtlasInterface atlasInterface = jAtlasTree.getAtlas(); + String name = mg.getUserText(); + MapSource mapSource = mg.getSelectedMapSource(); + MapSpace mapSpace = mapSource.getMapSpace(); + SelectedZoomLevels sZL = mg.getSelectedZoomLevels(); + + int[] zoomLevels = sZL.getZoomLevels(); + if (zoomLevels.length == 0) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_no_zoom_level_selected")); + return; + } + + String layerName = name; + Layer layer = null; + int c = 1; + boolean success = false; + do { + try { + layer = new Layer(atlasInterface, layerName); + success = true; + } catch (InvalidNameException e) { + layerName = name + "_" + Integer.toString(c++); + } + } while (!success); + List polygonPoints = msc.getPolygonPoints(); + + for (int zoom : zoomLevels) { + int xpoints[] = new int[polygonPoints.size()]; + int ypoints[] = new int[polygonPoints.size()]; + + for (int i = 0; i < xpoints.length; i++) { + Point p = mapSpace.changeZoom(polygonPoints.get(i), JMapViewer.MAX_ZOOM, zoom); + xpoints[i] = p.x; + ypoints[i] = p.y; + } + TileImageParameters customTileParameters = mg.getSelectedTileImageParameters(); + Polygon polygon = new Polygon(xpoints, ypoints, xpoints.length); + // Rectangle bounds = polygon.getBounds(); + // int maxMapSize = Settings.getInstance().maxMapSize; + // System.out.println(bounds.height + " " + bounds.width); + + String mapName = String.format(mapNameFmt, new Object[] { layerName, zoom }); + MapPolygon map = new MapPolygon(layer, mapName, mapSource, zoom, polygon, customTileParameters); + layer.addMap(map); + } + atlasInterface.addLayer(layer); + jAtlasTree.getTreeModel().notifyNodeInsert(layer); + + msc.finishPolygon(); + } + +} diff --git a/src/main/java/mobac/gui/actions/AddRectangleMapAutocut.java b/src/main/java/mobac/gui/actions/AddRectangleMapAutocut.java new file mode 100644 index 0000000..99ee627 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AddRectangleMapAutocut.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JOptionPane; + +import mobac.exceptions.InvalidNameException; +import mobac.gui.MainGUI; +import mobac.gui.atlastree.JAtlasTree; +import mobac.program.Logging; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.Layer; +import mobac.program.model.MapSelection; +import mobac.program.model.SelectedZoomLevels; +import mobac.program.model.Settings; +import mobac.program.model.TileImageParameters; +import mobac.utilities.I18nUtils; + +public class AddRectangleMapAutocut implements ActionListener { + + public void actionPerformed(ActionEvent event) { + MainGUI mg = MainGUI.getMainGUI(); + JAtlasTree jAtlasTree = mg.jAtlasTree; + final String mapNameFmt = "%s %02d"; + AtlasInterface atlasInterface = jAtlasTree.getAtlas(); + String name = mg.getUserText(); + MapSource mapSource = mg.getSelectedMapSource(); + SelectedZoomLevels sZL = mg.getSelectedZoomLevels(); + MapSelection ms = mg.getMapSelectionCoordinates(); + if (ms == null) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_no_select_area")); + return; + } + Settings settings = Settings.getInstance(); + // String errorText = mg.validateInput(); + // if (errorText.length() > 0) { + // JOptionPane.showMessageDialog(mg, errorText, "Errors", JOptionPane.ERROR_MESSAGE); + // return; + // } + + int[] zoomLevels = sZL.getZoomLevels(); + if (zoomLevels.length == 0) { + JOptionPane.showMessageDialog(mg, I18nUtils.localizedStringForKey("msg_no_zoom_level_selected")); + return; + } + + String layerName = name; + Layer layer = null; + int c = 1; + boolean success = false; + do { + try { + layer = new Layer(atlasInterface, layerName); + success = true; + } catch (InvalidNameException e) { + layerName = name + "_" + Integer.toString(c++); + } + } while (!success); + for (int zoom : zoomLevels) { + Point tl = ms.getTopLeftPixelCoordinate(zoom); + Point br = ms.getBottomRightPixelCoordinate(zoom); + TileImageParameters customTileParameters = mg.getSelectedTileImageParameters(); + try { + String mapName = String.format(mapNameFmt, new Object[] { layerName, zoom }); + layer.addMapsAutocut(mapName, mapSource, tl, br, zoom, customTileParameters, settings.maxMapSize, settings.mapOverlapTiles); + } catch (InvalidNameException e) { + Logging.LOG.error("", e); + } + } + atlasInterface.addLayer(layer); + jAtlasTree.getTreeModel().notifyNodeInsert(layer); + + } + +} diff --git a/src/main/java/mobac/gui/actions/AtlasConvert.java b/src/main/java/mobac/gui/actions/AtlasConvert.java new file mode 100644 index 0000000..94d42e9 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AtlasConvert.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; + +import mobac.gui.MainGUI; +import mobac.program.model.AtlasOutputFormat; +import mobac.utilities.I18nUtils; + +public class AtlasConvert implements ActionListener { + + public void actionPerformed(ActionEvent event) { + MainGUI mg = MainGUI.getMainGUI(); + JPanel panel = new JPanel(); + BorderLayout layout = new BorderLayout(); + layout.setVgap(4); + panel.setLayout(layout); + + JPanel formatPanel = new JPanel(new BorderLayout()); + + formatPanel.setPreferredSize(new Dimension(250, 300)); + + formatPanel.add(new JLabel(I18nUtils.localizedStringForKey("dlg_new_atlas_select_format_title")), BorderLayout.NORTH); + JList atlasFormatList = new JList(AtlasOutputFormat.getFormatsAsVector()); + atlasFormatList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + JScrollPane scroller = new JScrollPane(atlasFormatList); + scroller.setPreferredSize(new Dimension(100, 200)); + formatPanel.add(scroller, BorderLayout.CENTER); + + panel.add(formatPanel, BorderLayout.CENTER); + AtlasOutputFormat currentAOF = null; + try { + currentAOF = mg.getAtlas().getOutputFormat(); + } catch (Exception e) { + } + if (currentAOF != null) + atlasFormatList.setSelectedValue(currentAOF, true); + else + atlasFormatList.setSelectedIndex(1); + int result = JOptionPane.showConfirmDialog(MainGUI.getMainGUI(), panel, I18nUtils.localizedStringForKey("msg_convert_atlas_format"), + JOptionPane.OK_CANCEL_OPTION); + if (result != JOptionPane.OK_OPTION) + return; + + AtlasOutputFormat format = (AtlasOutputFormat) atlasFormatList.getSelectedValue(); + mg.jAtlasTree.convertAtlas(format); + mg.getParametersPanel().atlasFormatChanged(format); + } +} diff --git a/src/main/java/mobac/gui/actions/AtlasCreate.java b/src/main/java/mobac/gui/actions/AtlasCreate.java new file mode 100644 index 0000000..8c38931 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AtlasCreate.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JOptionPane; + +import mobac.exceptions.AtlasTestException; +import mobac.gui.atlastree.JAtlasTree; +import mobac.program.AtlasThread; +import mobac.program.interfaces.AtlasInterface; +import mobac.utilities.I18nUtils; + +public class AtlasCreate implements ActionListener { + + private JAtlasTree jAtlasTree; + + public AtlasCreate(JAtlasTree jAtlasTree) { + this.jAtlasTree = jAtlasTree; + } + + public void actionPerformed(ActionEvent event) { + if (!jAtlasTree.testAtlasContentValid()) + return; + try { + // We have to work on a deep clone otherwise the user would be + // able to modify settings of maps, layers and the atlas itself + // while the AtlasThread works on that atlas reference + AtlasInterface atlasToCreate = jAtlasTree.getAtlas().deepClone(); + Thread atlasThread = new AtlasThread(atlasToCreate); + atlasThread.start(); + } catch (AtlasTestException e) { + JOptionPane.showMessageDialog(null, "" + e.getMessage() + "", + I18nUtils.localizedStringForKey("msg_convert_incompatible_format"), JOptionPane.ERROR_MESSAGE); + + } + } + +} diff --git a/src/main/java/mobac/gui/actions/AtlasNew.java b/src/main/java/mobac/gui/actions/AtlasNew.java new file mode 100644 index 0000000..afc98c6 --- /dev/null +++ b/src/main/java/mobac/gui/actions/AtlasNew.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; + +import mobac.gui.MainGUI; +import mobac.program.model.AtlasOutputFormat; +import mobac.utilities.I18nUtils; + +public class AtlasNew implements ActionListener { + + public void actionPerformed(ActionEvent event) { + MainGUI mg = MainGUI.getMainGUI(); + JPanel panel = new JPanel(); + BorderLayout layout = new BorderLayout(); + layout.setVgap(4); + panel.setLayout(layout); + + JPanel formatPanel = new JPanel(new BorderLayout()); + + formatPanel.add(new JLabel(I18nUtils.localizedStringForKey("dlg_new_atlas_select_format_title")), BorderLayout.NORTH); + JList atlasFormatList = new JList(AtlasOutputFormat.getFormatsAsVector()); + atlasFormatList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + JScrollPane scroller = new JScrollPane(atlasFormatList); + scroller.setPreferredSize(new Dimension(140, 200)); + formatPanel.add(scroller, BorderLayout.CENTER); + + JPanel namePanel = new JPanel(new BorderLayout()); + namePanel.add(new JLabel(I18nUtils.localizedStringForKey("dlg_new_atlas_name_title")), BorderLayout.NORTH); + JTextField atlasName = new JTextField(I18nUtils.localizedStringForKey("dlg_new_atlas_default_atlas_name")); + namePanel.add(atlasName, BorderLayout.SOUTH); + + panel.add(namePanel, BorderLayout.NORTH); + panel.add(formatPanel, BorderLayout.CENTER); + panel.setPreferredSize(new Dimension(300, 300)); + AtlasOutputFormat currentAOF = null; + try { + currentAOF = mg.getAtlas().getOutputFormat(); + } catch (Exception e) { + } + if (currentAOF != null) + atlasFormatList.setSelectedValue(currentAOF, true); + else + atlasFormatList.setSelectedIndex(1); + int result = JOptionPane.showConfirmDialog(MainGUI.getMainGUI(), panel, I18nUtils.localizedStringForKey("dlg_new_atlas_title"), + JOptionPane.OK_CANCEL_OPTION); + if (result != JOptionPane.OK_OPTION) + return; + + AtlasOutputFormat format = (AtlasOutputFormat) atlasFormatList.getSelectedValue(); + mg.jAtlasTree.newAtlas(atlasName.getText(), format); + mg.getParametersPanel().atlasFormatChanged(format); + } +} diff --git a/src/main/java/mobac/gui/actions/BookmarkAdd.java b/src/main/java/mobac/gui/actions/BookmarkAdd.java new file mode 100644 index 0000000..712c02d --- /dev/null +++ b/src/main/java/mobac/gui/actions/BookmarkAdd.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JOptionPane; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.PreviewMap; +import mobac.program.model.Bookmark; +import mobac.program.model.Settings; +import mobac.utilities.I18nUtils; + +public class BookmarkAdd implements ActionListener { + + private final PreviewMap previewMap; + + public BookmarkAdd(PreviewMap previewMap) { + this.previewMap = previewMap; + } + + public void actionPerformed(ActionEvent arg0) { + Bookmark bm = previewMap.getPositionBookmark(); + String name = JOptionPane.showInputDialog(I18nUtils.localizedStringForKey("dlg_add_bookmark_msg"), bm.toString()); + if (name == null) + return; + bm.setName(name); + Settings.getInstance().placeBookmarks.add(bm); + MainGUI.getMainGUI().updateBookmarksMenu(); + } + +} diff --git a/src/main/java/mobac/gui/actions/BookmarkManage.java b/src/main/java/mobac/gui/actions/BookmarkManage.java new file mode 100644 index 0000000..76f0739 --- /dev/null +++ b/src/main/java/mobac/gui/actions/BookmarkManage.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.MainGUI; +import mobac.gui.dialogs.ManageBookmarks; + +public class BookmarkManage implements ActionListener { + + public void actionPerformed(ActionEvent event) { + ManageBookmarks mb = new ManageBookmarks(MainGUI.getMainGUI()); + mb.setModal(true); + mb.setVisible(true); + MainGUI.getMainGUI().updateBookmarksMenu(); + } + +} diff --git a/src/main/java/mobac/gui/actions/DebugSetLogLevel.java b/src/main/java/mobac/gui/actions/DebugSetLogLevel.java new file mode 100644 index 0000000..7507ebc --- /dev/null +++ b/src/main/java/mobac/gui/actions/DebugSetLogLevel.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenu; +import javax.swing.JMenuItem; + +import mobac.gui.MainGUI; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +public class DebugSetLogLevel implements ActionListener { + + public void actionPerformed(ActionEvent event) { + Logger log = Logger.getRootLogger(); + JMenuItem menuItem = (JMenuItem) event.getSource(); + log.setLevel(Level.toLevel(menuItem.getName())); + JMenu menu = MainGUI.getMainGUI().logLevelMenu; + Component[] c = menu.getMenuComponents(); + for (int i = 0; i < c.length; i++) { + ((JMenuItem) c[i]).setSelected(c[i] == menuItem); + } + } +} diff --git a/src/main/java/mobac/gui/actions/DebugShowLogFile.java b/src/main/java/mobac/gui/actions/DebugShowLogFile.java new file mode 100644 index 0000000..9ed2ca7 --- /dev/null +++ b/src/main/java/mobac/gui/actions/DebugShowLogFile.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; + +import javax.swing.JOptionPane; + +import mobac.gui.MainGUI; +import mobac.program.Logging; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +public class DebugShowLogFile implements ActionListener { + + public void actionPerformed(ActionEvent event) { + Logger log = Logger.getLogger(DebugShowLogFile.class); + String logFile = Logging.getLogFile(); + if (logFile == null) { + log.error("No file logger configured"); + JOptionPane.showMessageDialog(MainGUI.getMainGUI(), + I18nUtils.localizedStringForKey("msg_no_log_file_config"), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.ERROR_MESSAGE); + return; + } + File f = new File(logFile); + if (!f.isFile()) { + log.error("Log file does not exists: " + f.getAbsolutePath()); + JOptionPane.showMessageDialog(MainGUI.getMainGUI(), + String.format(I18nUtils.localizedStringForKey("msg_no_log_file"), f.getAbsolutePath()), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + return; + } + try { + Desktop.getDesktop().open(f); + } catch (IOException e) { + GUIExceptionHandler.processException(e); + } + } + +} diff --git a/src/main/java/mobac/gui/actions/DebugShowMapSourceNames.java b/src/main/java/mobac/gui/actions/DebugShowMapSourceNames.java new file mode 100644 index 0000000..92d7f1f --- /dev/null +++ b/src/main/java/mobac/gui/actions/DebugShowMapSourceNames.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; + +import mobac.gui.MainGUI; +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.utilities.I18nUtils; + +public class DebugShowMapSourceNames implements ActionListener { + + public void actionPerformed(ActionEvent e) { + ArrayList mapSources = new ArrayList(MapSourcesManager.getInstance() + .getAllAvailableMapSources()); + + Collections.sort(mapSources, new Comparator() { + + public int compare(MapSource o1, MapSource o2) { + return o1.getName().compareTo(o2.getName()); + } + + }); + JFrame dialog = new JFrame(I18nUtils.localizedStringForKey("dlg_show_source_title")); + dialog.setLocationRelativeTo(MainGUI.getMainGUI()); + dialog.setLocation(100, 40); + Dimension dScreen = Toolkit.getDefaultToolkit().getScreenSize(); + dScreen.height -= 200; + dScreen.width = Math.min(dScreen.width - 100, 700); + dialog.setSize(dScreen); + dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + JTable mapSourcesTable = new JTable(new MapSourcesTableModel(mapSources)); + JScrollPane scroller = new JScrollPane(mapSourcesTable); + + mapSourcesTable.getColumnModel().getColumn(2).setMaxWidth(100); + dialog.add(scroller); + dialog.setVisible(true); + } + + static class MapSourcesTableModel extends AbstractTableModel { + + List mapSources; + + public MapSourcesTableModel(List mapSources) { + super(); + this.mapSources = mapSources; + } + + public int getRowCount() { + return mapSources.size(); + } + + public int getColumnCount() { + return 4; + } + + @Override + public String getColumnName(int column) { + switch (column) { + case 0: + return I18nUtils.localizedStringForKey("dlg_show_source_column_name"); + case 1: + return I18nUtils.localizedStringForKey("dlg_show_source_column_display_text"); + case 2: + return I18nUtils.localizedStringForKey("dlg_show_source_column_rev"); + case 3: + return I18nUtils.localizedStringForKey("dlg_show_source_column_type"); + default: + return null; + } + } + + public Object getValueAt(int rowIndex, int columnIndex) { + MapSource ms = mapSources.get(rowIndex); + MapSourceLoaderInfo li; + switch (columnIndex) { + case 0: + return ms.getName(); + case 1: + return ms.toString(); + case 2: + li = ms.getLoaderInfo(); + if (li == null) + return null; + return li.getRevision(); + case 3: + li = ms.getLoaderInfo(); + if (li == null) + return null; + String s = ""; + File f = li.getSourceFile(); + if (f != null) + s += f.getName() + " / "; + return s + li.getLoaderType(); + default: + return null; + } + } + } + +} diff --git a/src/main/java/mobac/gui/actions/DebugShowMapTileGrid.java b/src/main/java/mobac/gui/actions/DebugShowMapTileGrid.java new file mode 100644 index 0000000..3524bc6 --- /dev/null +++ b/src/main/java/mobac/gui/actions/DebugShowMapTileGrid.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JCheckBoxMenuItem; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.PreviewMap; + +public class DebugShowMapTileGrid implements ActionListener { + + public void actionPerformed(ActionEvent e) { + PreviewMap previewMap = MainGUI.getMainGUI().previewMap; + previewMap.setTileGridVisible(!previewMap.isTileGridVisible()); + if (e.getSource() instanceof JCheckBoxMenuItem) { + JCheckBoxMenuItem m = (JCheckBoxMenuItem) e.getSource(); + m.setSelected(previewMap.isTileGridVisible()); + } + } + +} diff --git a/src/main/java/mobac/gui/actions/DebugShowReport.java b/src/main/java/mobac/gui/actions/DebugShowReport.java new file mode 100644 index 0000000..f2c26b1 --- /dev/null +++ b/src/main/java/mobac/gui/actions/DebugShowReport.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.utilities.GUIExceptionHandler; + +public class DebugShowReport implements ActionListener { + + public void actionPerformed(ActionEvent e) { + GUIExceptionHandler.processException(null, null); + // throw new RuntimeException("Test"); + } + +} diff --git a/src/main/java/mobac/gui/actions/GpxAddPoint.java b/src/main/java/mobac/gui/actions/GpxAddPoint.java new file mode 100644 index 0000000..7c80d20 --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxAddPoint.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JOptionPane; + +import mobac.gui.MainGUI; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.GpxMapController; +import mobac.gui.panels.JGpxPanel; +import mobac.utilities.I18nUtils; + + + +public class GpxAddPoint implements ActionListener { + + JGpxPanel panel; + + private GpxMapController mapController = null; + + public GpxAddPoint(JGpxPanel panel) { + super(); + this.panel = panel; + } + + public synchronized void actionPerformed(ActionEvent event) { + GpxEntry entry = panel.getSelectedEntry(); + if (entry == null) { + int answer = JOptionPane.showConfirmDialog(null, + I18nUtils.localizedStringForKey("rp_gpx_msg_ask_create_new"), + I18nUtils.localizedStringForKey("rp_gpx_msg_ask_create_new_title"), + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (answer != JOptionPane.YES_OPTION) + return; + entry = new GpxNew(panel).newGpx(); + } + + if (!entry.isWaypointParent()) { + JOptionPane.showMessageDialog(null, + I18nUtils.localizedStringForKey("rp_gpx_msg_add_point_failed"), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.INFORMATION_MESSAGE); + return; + } + + PreviewMap map = MainGUI.getMainGUI().previewMap; + map.getMapSelectionController().disable(); + if (mapController == null) + mapController = new GpxMapController(map, panel, false); + mapController.enable(); + } +} diff --git a/src/main/java/mobac/gui/actions/GpxClear.java b/src/main/java/mobac/gui/actions/GpxClear.java new file mode 100644 index 0000000..5f5330d --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxClear.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Iterator; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.gui.panels.JGpxPanel; + + + +/** + * Deletes all loaded {@link GpxLayer}s from the main map viewer. + * + */ +public class GpxClear implements ActionListener { + + JGpxPanel panel; + + public GpxClear(JGpxPanel panel) { + super(); + this.panel = panel; + } + + public void actionPerformed(ActionEvent e) { + Iterator mapLayers = MainGUI.getMainGUI().previewMap.mapLayers.iterator(); + while (mapLayers.hasNext()) { + if (mapLayers.next() instanceof GpxLayer) + mapLayers.remove(); + } + panel.resetModel(); + MainGUI.getMainGUI().previewMap.repaint(); + } + +} diff --git a/src/main/java/mobac/gui/actions/GpxEditor.java b/src/main/java/mobac/gui/actions/GpxEditor.java new file mode 100644 index 0000000..51ee769 --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxEditor.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import mobac.data.gpx.gpx11.Gpx; +import mobac.data.gpx.gpx11.RteType; +import mobac.data.gpx.gpx11.TrkType; +import mobac.data.gpx.gpx11.TrksegType; +import mobac.data.gpx.gpx11.WptType; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.gpxtree.RteEntry; +import mobac.gui.gpxtree.TrkEntry; +import mobac.gui.gpxtree.TrksegEntry; + +/** + * Encapsulates all functionality regarding edits of loaded gpx files. + * + * @author lhoeppner + * + */ +public class GpxEditor { + private static GpxEditor editor = null; + + public static GpxEditor getInstance() { + if (editor == null) { + editor = new GpxEditor(); + } + return editor; + } + + /** + * Adds a wpt to the selected route. + * + * @param entry + * @param wpt + */ + public void findRteAndAdd(RteEntry entry, WptType wpt) { + List rtes = entry.getLayer().getGpx().getRte(); + RteType rteParent = (entry).getRte(); + for (RteType rte : rtes) { + if (rte.equals(rteParent)) { + rte.getRtept().add(wpt); + } + } + } + + /** + * Adds a wpt to the selected track segment. + * + * @param entry + * @param wpt + */ + public void findTrksegAndAdd(TrksegEntry entry, WptType wpt) { + // get the track the selected track segment belongs to + DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) entry.getNode().getParent(); + TrkEntry trkParent = (TrkEntry) parentNode.getUserObject(); + + // get the selected track segment + TrksegType trksegParent = entry.getTrkSeg(); + List trksegs = trkParent.getTrk().getTrkseg(); + + for (TrksegType trkseg : trksegs) { + if (trkseg.equals(trksegParent)) { + trkseg.getTrkpt().add(wpt); + } + } + } + + /** + * Removes a waypoint from the Gpx assigned to the layer. + * + * @param wpt + * - the node to be removed + * @return - true if wpt found and deleted, false otherwise + */ + public boolean findWptAndDelete(WptType wpt, GpxEntry gpxEntry) { + Gpx gpx = gpxEntry.getLayer().getGpx(); + // wpts + List wpts = gpx.getWpt(); + for (WptType currentWpt : wpts) { + if (currentWpt.equals(wpt)) { + wpts.remove(currentWpt); + return true; + } + } + // trks + List trks = gpx.getTrk(); + for (TrkType currentTrk : trks) { + List trksegs = currentTrk.getTrkseg(); + for (TrksegType currentTrkseg : trksegs) { + wpts = currentTrkseg.getTrkpt(); + for (WptType currentWpt : wpts) { + if (currentWpt.equals(wpt)) { + wpts.remove(currentWpt); + return true; + } + } + } + } + // rtes + List rtes = gpx.getRte(); + for (RteType currentRte : rtes) { + wpts = currentRte.getRtept(); + for (WptType currentWpt : wpts) { + if (currentWpt.equals(wpt)) { + wpts.remove(currentWpt); + return true; + } + } + } + return false; // if the node wasn't found + } +} diff --git a/src/main/java/mobac/gui/actions/GpxElementListener.java b/src/main/java/mobac/gui/actions/GpxElementListener.java new file mode 100644 index 0000000..c09250d --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxElementListener.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; + +import mobac.data.gpx.gpx11.WptType; +import mobac.gui.MainGUI; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.gpxtree.GpxRootEntry; +import mobac.gui.gpxtree.RteEntry; +import mobac.gui.gpxtree.TrkEntry; +import mobac.gui.gpxtree.TrksegEntry; +import mobac.gui.gpxtree.WptEntry; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.GpxMapController; +import mobac.utilities.I18nUtils; + +/** + * Listener for the gpx editor tree elements. + * + * @author lhoeppner + * @author r_x + * + */ +public class GpxElementListener implements MouseListener { + + public static final String MENU_NAME_RENAME = I18nUtils.localizedStringForKey("rp_gpx_menu_rename"); + public static final String MENU_NAME_DELETE = I18nUtils.localizedStringForKey("rp_gpx_menu_delete"); + + private final GpxEntry gpxEntry; + + private GpxMapController mapController = null; + private GpxEditor editor = GpxEditor.getInstance(); + + public GpxElementListener(GpxEntry gpxEntry) { + this.gpxEntry = gpxEntry; + } + + public void mouseClicked(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + handleClick(e); + } + + public void mouseReleased(MouseEvent e) { + handleClick(e); + } + + private void handleClick(MouseEvent e) { + JMenuItem item = (JMenuItem) e.getSource(); + if (item == null) + return; + if (MENU_NAME_RENAME.equals(item.getName())) { + renameEntry(); + } else if (MENU_NAME_DELETE.equals(item.getName())) { + removeEntry(); + } + } + + /** + * Removes an entry (wpt, trk, trkseg, rte) from a gpx file (and the displayed layer) Currently only works for + * waypoints. + * + */ + private void removeEntry() { + int answer = JOptionPane.showConfirmDialog(null, + I18nUtils.localizedStringForKey("rp_gpx_msg_confim_delete"), + I18nUtils.localizedStringForKey("rp_gpx_msg_confim_delete_title"), + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (answer == JOptionPane.YES_OPTION) { + PreviewMap map = MainGUI.getMainGUI().previewMap; + map.getMapSelectionController().disable(); + if (mapController == null) + mapController = new GpxMapController(map, gpxEntry.getLayer().getPanel(), false); + mapController.enable(); + + if (gpxEntry.getClass().equals(RteEntry.class)) { + // RteEntry rte = (RteEntry) gpxEntry; + + } else if (gpxEntry.getClass().equals(TrkEntry.class)) { + // TrkEntry trk = (TrkEntry) gpxEntry; + + } else if (gpxEntry.getClass().equals(WptEntry.class)) { + WptEntry wptEntry = (WptEntry) gpxEntry; + WptType wpt = wptEntry.getWpt(); + editor.findWptAndDelete(wpt, gpxEntry); + wptEntry.getLayer().getPanel().removeWpt(wptEntry); + mapController.repaint(); + } else if (gpxEntry.getClass().equals(GpxRootEntry.class)) { + // GpxRootEntry root = (GpxRootEntry) gpxEntry; + + } + } else { + return; + } + } + + /** + * Renames (if possible) the entry according to user input. + * + */ + private void renameEntry() { + if (gpxEntry.getClass().equals(TrksegEntry.class)) { + JOptionPane.showMessageDialog(null, + I18nUtils.localizedStringForKey("rp_gpx_msg_can_not_rename_track"), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.INFORMATION_MESSAGE); + return; + } else { + if (gpxEntry.getClass().equals(RteEntry.class)) { + RteEntry rte = (RteEntry) gpxEntry; + String name = JOptionPane.showInputDialog(null, I18nUtils.localizedStringForKey("rp_gpx_rename_element_title"), + rte.getRte().getName()); + if (name == null) { + return; + } + rte.getRte().setName(name); + } else if (gpxEntry.getClass().equals(TrkEntry.class)) { + TrkEntry trk = (TrkEntry) gpxEntry; + String name = JOptionPane.showInputDialog(null, I18nUtils.localizedStringForKey("rp_gpx_rename_element_title"), trk.getTrk().getName()); + if (name == null) { + return; + } + trk.getTrk().setName(name); + } else if (gpxEntry.getClass().equals(WptEntry.class)) { + WptEntry wpt = (WptEntry) gpxEntry; + String name = JOptionPane.showInputDialog(null, I18nUtils.localizedStringForKey("rp_gpx_rename_element_title"), wpt.getWpt().getName()); + if (name == null) { + return; + } + wpt.getWpt().setName(name); + } else if (gpxEntry.getClass().equals(GpxRootEntry.class)) { + GpxRootEntry root = (GpxRootEntry) gpxEntry; + String initialValue = root.getMetaDataName(); + String name = JOptionPane.showInputDialog(null, I18nUtils.localizedStringForKey("rp_gpx_rename_element_title"), initialValue); + if (name == null) { + return; + } + root.setMetaDataName(name); + } + } + } +} diff --git a/src/main/java/mobac/gui/actions/GpxLoad.java b/src/main/java/mobac/gui/actions/GpxLoad.java new file mode 100644 index 0000000..5ca118a --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxLoad.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Component; +import java.awt.Cursor; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; +import javax.xml.bind.JAXBException; + +import mobac.data.gpx.GPXUtils; +import mobac.data.gpx.gpx11.Gpx; +import mobac.gui.MainGUI; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.gui.panels.JGpxPanel; +import mobac.program.model.Settings; +import mobac.utilities.I18nUtils; +import mobac.utilities.file.GpxFileFilter; + +import org.apache.log4j.Logger; + +public class GpxLoad implements ActionListener { + + private Logger log = Logger.getLogger(GpxLoad.class); + + JGpxPanel panel; + + public GpxLoad(JGpxPanel panel) { + super(); + this.panel = panel; + } + + public void actionPerformed(ActionEvent event) { + if (!GPXUtils.checkJAXBVersion()) + return; + JFileChooser fc = new JFileChooser(); + try { + File dir = new File(Settings.getInstance().gpxFileChooserDir); + fc.setCurrentDirectory(dir); // restore the saved directory + } catch (Exception e) { + } + fc.setMultiSelectionEnabled(true); + fc.addChoosableFileFilter(new GpxFileFilter(false)); + final MainGUI mainGUI = MainGUI.getMainGUI(); + int returnVal = fc.showOpenDialog(mainGUI); + if (returnVal != JFileChooser.APPROVE_OPTION) + return; + Settings.getInstance().gpxFileChooserDir = fc.getCurrentDirectory().getAbsolutePath(); + + File[] f = fc.getSelectedFiles(); + + // check already opened gpx files + boolean duplicates = false; + for (File selectedFile : f) { + duplicates = panel.isFileOpen(selectedFile.getAbsolutePath()); + if (duplicates) + break; + } + if (duplicates) { + int answer = JOptionPane + .showConfirmDialog(mainGUI, I18nUtils.localizedStringForKey("rp_gpx_msg_confirm_reopen_file"), + I18nUtils.localizedStringForKey("Warning"), JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (answer != JOptionPane.YES_OPTION) + return; + } + + // process + if (f.length > 1) { + doMultiLoad(f, mainGUI); + } else if (f.length == 1) { + doLoad(f[0], mainGUI); + } + mainGUI.previewMap.refreshMap(); + } + + /** + * @param f + */ + private void doLoad(File f, Component parent) { + try { + Gpx gpx = GPXUtils.loadGpxFile(f); + GpxLayer gpxLayer = new GpxLayer(gpx); + gpxLayer.setFile(f); + panel.addGpxLayer(gpxLayer); + } catch (JAXBException e) { + JOptionPane.showMessageDialog(parent, "Unable to load the GPX file
" + f.getAbsolutePath() + + "

Please make sure the file is a valid GPX v1.1 file.
" + + "
Internal error message:
" + e.getMessage() + "", "GPX loading failed", + JOptionPane.ERROR_MESSAGE); + throw new RuntimeException(e); + } + } + + private void doMultiLoad(final File[] files, final MainGUI mainGUI) { + final JDialog progressDialog = new JDialog(mainGUI); + // prepare progress dialog + progressDialog.setSize(400, 50); + progressDialog.setResizable(false); + progressDialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + progressDialog.setLocation( + Math.max(0, (int) (mainGUI.getLocation().getX() + mainGUI.getSize().getWidth() / 2 - 200)), + Math.max(0, (int) (mainGUI.getLocation().getY() + mainGUI.getSize().getHeight() / 2 - 25))); + final JProgressBar progressBar = new JProgressBar(0, files.length); + progressDialog.add(progressBar); + + mainGUI.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + mainGUI.setEnabled(false); + progressDialog.setVisible(true); + + Thread job = new Thread() { + + private int counter = 0; + + public void run() { + try { + // iterate over files to load + for (final File file : files) { + counter++; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progressBar.setValue(counter); + progressDialog.setTitle("Processing " + counter + " of " + files.length + " <" + + file.getName() + ">"); + } + }); + doLoad(file, progressDialog); + } + } catch (RuntimeException e) { + log.error(e.getMessage(), e); + } finally { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + // close progress dialog + mainGUI.previewMap.repaint(); + mainGUI.setCursor(Cursor.getDefaultCursor()); + if (progressDialog != null) { + progressDialog.setVisible(false); + progressDialog.dispose(); + } + mainGUI.setEnabled(true); + mainGUI.toFront(); + } + }); + } + }; + }; + + job.start(); + } +} diff --git a/src/main/java/mobac/gui/actions/GpxNew.java b/src/main/java/mobac/gui/actions/GpxNew.java new file mode 100644 index 0000000..23d961b --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxNew.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.data.gpx.GPXUtils; +import mobac.data.gpx.gpx11.Gpx; +import mobac.gui.MainGUI; +import mobac.gui.gpxtree.GpxRootEntry; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.gui.panels.JGpxPanel; +//import mobac.gui.panels.JGpxPanel.ListModelEntry; + + +public class GpxNew implements ActionListener { + + JGpxPanel panel; + + public GpxNew(JGpxPanel panel) { + super(); + this.panel = panel; + } + + public void actionPerformed(ActionEvent event) { + if (!GPXUtils.checkJAXBVersion()) + return; + newGpx(); + MainGUI.getMainGUI().previewMap.repaint(); + } + + public GpxRootEntry newGpx() { + Gpx gpx = Gpx.createGpx(); + GpxLayer gpxLayer = new GpxLayer(gpx); + return panel.addGpxLayer(gpxLayer); + } +} diff --git a/src/main/java/mobac/gui/actions/GpxSave.java b/src/main/java/mobac/gui/actions/GpxSave.java new file mode 100644 index 0000000..8ae27f9 --- /dev/null +++ b/src/main/java/mobac/gui/actions/GpxSave.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.xml.bind.JAXBException; + +import mobac.data.gpx.GPXUtils; +import mobac.data.gpx.gpx11.Gpx; +import mobac.gui.MainGUI; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.panels.JGpxPanel; +import mobac.program.model.Settings; +import mobac.utilities.I18nUtils; +import mobac.utilities.file.GpxFileFilter; + + +public class GpxSave implements ActionListener { + + private JGpxPanel panel; + private boolean saveAs; + + public GpxSave(JGpxPanel panel) { + this(panel, false); + } + + /** + * + * @param panel + * @param saveAs + * if true a file chooser dialog is displayed where the user can + * change the filename + */ + public GpxSave(JGpxPanel panel, boolean saveAs) { + super(); + this.panel = panel; + this.saveAs = saveAs; + } + + public void actionPerformed(ActionEvent event) { + + GpxEntry entry = panel.getSelectedEntry(); + if (entry == null) { + JOptionPane.showMessageDialog(null, + I18nUtils.localizedStringForKey("rp_gpx_msg_error_save_gpx_file"), + I18nUtils.localizedStringForKey("rp_gpx_msg_no_select_file"), + JOptionPane.ERROR_MESSAGE); + return; + } + if (!GPXUtils.checkJAXBVersion()) + return; + + Gpx gpx = entry.getLayer().getGpx(); + + try { + File f = entry.getLayer().getFile(); + if (saveAs || f == null) + f = selectFile(f); + if (f == null) + return; + if (!f.getName().toLowerCase().endsWith(".gpx")) + f = new File(f.getAbsolutePath() + ".gpx"); + entry.getLayer().setFile(f); + GPXUtils.saveGpxFile(gpx, f); + } catch (JAXBException e) { + throw new RuntimeException(e); + } + MainGUI.getMainGUI().previewMap.repaint(); + } + + private File selectFile(File f) { + JFileChooser fc = new JFileChooser(); + try { + File dir = new File(Settings.getInstance().gpxFileChooserDir); + if (f == null) + fc.setCurrentDirectory(dir); // restore the saved directory + else + fc.setSelectedFile(f); + } catch (Exception e) { + } + fc.addChoosableFileFilter(new GpxFileFilter(true)); + int returnVal = fc.showSaveDialog(MainGUI.getMainGUI()); + if (returnVal != JFileChooser.APPROVE_OPTION) + return null; + Settings.getInstance().gpxFileChooserDir = fc.getCurrentDirectory().getAbsolutePath(); + return fc.getSelectedFile(); + } +} diff --git a/src/main/java/mobac/gui/actions/HelpLicenses.java b/src/main/java/mobac/gui/actions/HelpLicenses.java new file mode 100644 index 0000000..c5ba9bd --- /dev/null +++ b/src/main/java/mobac/gui/actions/HelpLicenses.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.dialogs.LicensesDialog; + +public class HelpLicenses implements ActionListener { + + public void actionPerformed(ActionEvent e) { + new LicensesDialog().setVisible(true); + } + +} diff --git a/src/main/java/mobac/gui/actions/OpenInWebbrowser.java b/src/main/java/mobac/gui/actions/OpenInWebbrowser.java new file mode 100644 index 0000000..dfe0bc1 --- /dev/null +++ b/src/main/java/mobac/gui/actions/OpenInWebbrowser.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import mobac.program.Logging; + +public class OpenInWebbrowser implements ActionListener, MouseListener { + + URI uri; + + public OpenInWebbrowser(URI uri) { + super(); + this.uri = uri; + } + + public OpenInWebbrowser(String uri) throws URISyntaxException { + super(); + this.uri = new URI(uri); + } + + public void actionPerformed(ActionEvent event) { + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + try { + desktop.browse(uri); + } catch (IOException e) { + Logging.LOG.error("Failed to open web browser",e); + } + } + } + + public void mouseReleased(MouseEvent e) { + actionPerformed(null); + } + + public void mouseClicked(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + } + +} diff --git a/src/main/java/mobac/gui/actions/PanelShowHide.java b/src/main/java/mobac/gui/actions/PanelShowHide.java new file mode 100644 index 0000000..b0c3144 --- /dev/null +++ b/src/main/java/mobac/gui/actions/PanelShowHide.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JPanel; + +public class PanelShowHide implements ActionListener { + + private JPanel panel; + + public PanelShowHide(JPanel panel) { + super(); + this.panel = panel; + } + + + public void actionPerformed(ActionEvent e) { + panel.setVisible(!panel.isVisible()); + } + +} diff --git a/src/main/java/mobac/gui/actions/RefreshCustomMapsources.java b/src/main/java/mobac/gui/actions/RefreshCustomMapsources.java new file mode 100644 index 0000000..ef6013c --- /dev/null +++ b/src/main/java/mobac/gui/actions/RefreshCustomMapsources.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JOptionPane; + +import mobac.gui.MainGUI; +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSource; +import mobac.utilities.I18nUtils; + +public class RefreshCustomMapsources implements ActionListener { + + public void actionPerformed(ActionEvent e) { + MapSourcesManager manager = MapSourcesManager.getInstance(); + MainGUI gui = MainGUI.getMainGUI(); + MapSource selectedMapSource = gui.getSelectedMapSource(); + boolean updateGui = false; + int count = 0; + for (MapSource mapSource : manager.getAllAvailableMapSources()) { + if (mapSource instanceof FileBasedMapSource) { + FileBasedMapSource fbms = (FileBasedMapSource) mapSource; + fbms.reinitialize(); + count++; + if (mapSource.equals(selectedMapSource)) + updateGui = true; + } + } + if (updateGui) { + /* + * The currently selected map source was updated - we have to force an GUI update in case the available zoom + * levels has been changed + */ + gui.mapSourceChanged(selectedMapSource); + } + JOptionPane.showMessageDialog(gui, String.format(I18nUtils.localizedStringForKey("msg_refresh_all_map_source_done"), count)); + } + +} diff --git a/src/main/java/mobac/gui/actions/SelectionModeCircle.java b/src/main/java/mobac/gui/actions/SelectionModeCircle.java new file mode 100644 index 0000000..7c712e0 --- /dev/null +++ b/src/main/java/mobac/gui/actions/SelectionModeCircle.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.PolygonCircleSelectionMapController; + +public class SelectionModeCircle implements ActionListener { + + public void actionPerformed(ActionEvent e) { + PreviewMap previewMap = MainGUI.getMainGUI().previewMap; + previewMap.setMapSelectionController(new PolygonCircleSelectionMapController(previewMap)); + + } + +} diff --git a/src/main/java/mobac/gui/actions/SelectionModePolygon.java b/src/main/java/mobac/gui/actions/SelectionModePolygon.java new file mode 100644 index 0000000..60029e6 --- /dev/null +++ b/src/main/java/mobac/gui/actions/SelectionModePolygon.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.PolygonSelectionMapController; + +public class SelectionModePolygon implements ActionListener { + + public void actionPerformed(ActionEvent e) { + PreviewMap previewMap = MainGUI.getMainGUI().previewMap; + previewMap.setMapSelectionController(new PolygonSelectionMapController(previewMap)); + + } + +} diff --git a/src/main/java/mobac/gui/actions/SelectionModeRectangle.java b/src/main/java/mobac/gui/actions/SelectionModeRectangle.java new file mode 100644 index 0000000..3d06605 --- /dev/null +++ b/src/main/java/mobac/gui/actions/SelectionModeRectangle.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.RectangleSelectionMapController; + +public class SelectionModeRectangle implements ActionListener { + + public void actionPerformed(ActionEvent e) { + PreviewMap previewMap = MainGUI.getMainGUI().previewMap; + previewMap.setMapSelectionController(new RectangleSelectionMapController(previewMap)); + } + +} diff --git a/src/main/java/mobac/gui/actions/ShowAboutDialog.java b/src/main/java/mobac/gui/actions/ShowAboutDialog.java new file mode 100644 index 0000000..7f14467 --- /dev/null +++ b/src/main/java/mobac/gui/actions/ShowAboutDialog.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.dialogs.AboutDialog; + +public class ShowAboutDialog implements ActionListener { + + public void actionPerformed(ActionEvent e) { + new AboutDialog().setVisible(true); + } + +} diff --git a/src/main/java/mobac/gui/actions/ShowHelpAction.java b/src/main/java/mobac/gui/actions/ShowHelpAction.java new file mode 100644 index 0000000..e8a48a3 --- /dev/null +++ b/src/main/java/mobac/gui/actions/ShowHelpAction.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import mobac.gui.dialogs.Help; + + +public class ShowHelpAction implements ActionListener { + public void actionPerformed(ActionEvent event) { + Help.showHelp(); + } +} diff --git a/src/main/java/mobac/gui/actions/ShowReadme.java b/src/main/java/mobac/gui/actions/ShowReadme.java new file mode 100644 index 0000000..6e19d5a --- /dev/null +++ b/src/main/java/mobac/gui/actions/ShowReadme.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.actions; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.IOException; + +import javax.swing.JOptionPane; + +import mobac.gui.MainGUI; +import mobac.program.DirectoryManager; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; + +public class ShowReadme implements ActionListener { + + public void actionPerformed(ActionEvent event) { + File readme = new File(DirectoryManager.programDir, "README.HTM"); + if (!readme.isFile()) { + JOptionPane.showMessageDialog(MainGUI.getMainGUI(), + I18nUtils.localizedStringForKey("msg_no_found_readme_file"), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.ERROR_MESSAGE); + return; + } + try { + Desktop.getDesktop().browse(readme.toURI()); + } catch (IOException e) { + GUIExceptionHandler.processException(e); + } + } + +} diff --git a/src/main/java/mobac/gui/atlastree/DragDropController.java b/src/main/java/mobac/gui/atlastree/DragDropController.java new file mode 100644 index 0000000..351f994 --- /dev/null +++ b/src/main/java/mobac/gui/atlastree/DragDropController.java @@ -0,0 +1,217 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.atlastree; + +import java.awt.HeadlessException; +import java.awt.Point; +import java.awt.datatransfer.Transferable; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DragGestureEvent; +import java.awt.dnd.DragGestureListener; +import java.awt.dnd.DragGestureRecognizer; +import java.awt.dnd.DragSource; +import java.awt.dnd.DragSourceDragEvent; +import java.awt.dnd.DragSourceDropEvent; +import java.awt.dnd.DragSourceEvent; +import java.awt.dnd.DragSourceListener; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetContext; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; + +import javax.swing.JOptionPane; +import javax.swing.JTree; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import mobac.exceptions.InvalidNameException; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.model.AtlasTreeModel; +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + + +public class DragDropController { + + static Logger log = Logger.getLogger(DragDropController.class); + + public DragDropController(JAtlasTree atlasTree) { + super(); + this.atlasTree = atlasTree; + new AtlasDragSource(); + new AtlasDropTarget(); + } + + JAtlasTree atlasTree; + + protected class AtlasDragSource implements DragSourceListener, DragGestureListener { + + final DragGestureRecognizer recognizer; + final DragSource source; + + public AtlasDragSource() { + source = new DragSource(); + recognizer = source.createDefaultDragGestureRecognizer(atlasTree, + DnDConstants.ACTION_MOVE, this); + } + + public void dragGestureRecognized(DragGestureEvent dge) { + TreePath path = atlasTree.getSelectionPath(); + if ((path == null) || (path.getPathCount() <= 1)) + // We can't move the root node or an empty selection + return; + TreeNode oldNode = (TreeNode) path.getLastPathComponent(); + if (!(oldNode instanceof LayerInterface || oldNode instanceof MapInterface)) + return; + Transferable transferable = new NodeTransferWrapper(oldNode); + source.startDrag(dge, DragSource.DefaultMoveNoDrop, transferable, this); + } + + /** + * Called whenever the drop target changes and it has bee accepted ( + */ + public void dragEnter(DragSourceDragEvent dsde) { + dsde.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop); + } + + public void dragOver(DragSourceDragEvent dsde) { + } + + public void dragDropEnd(DragSourceDropEvent dsde) { + } + + public void dragExit(DragSourceEvent dse) { + dse.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop); + } + + public void dropActionChanged(DragSourceDragEvent dsde) { + } + + } + + protected class AtlasDropTarget implements DropTargetListener { + + final DropTarget target; + + public AtlasDropTarget() throws HeadlessException { + super(); + target = new DropTarget(atlasTree, this); + } + + public synchronized void dragEnter(DropTargetDragEvent dtde) { + } + + public synchronized void dragExit(DropTargetEvent dte) { + } + + public synchronized void dragOver(DropTargetDragEvent dtde) { + try { + Transferable t = dtde.getTransferable(); + Object o = t.getTransferData(NodeTransferWrapper.ATLAS_OBJECT_FLAVOR); + TreeNode node = getNodeForEvent(dtde); + if (o instanceof LayerInterface && node instanceof LayerInterface) { + dtde.acceptDrag(dtde.getDropAction()); + return; + } + if (o instanceof MapInterface && node instanceof LayerInterface + || node instanceof MapInterface) { + dtde.acceptDrag(dtde.getDropAction()); + return; + } + dtde.rejectDrag(); + } catch (Exception e) { + log.error("", e); + } + } + + public void dropActionChanged(DropTargetDragEvent dtde) { + } + + public synchronized void drop(DropTargetDropEvent dtde) { + try { + TreeNode sourceNode = (TreeNode) dtde.getTransferable().getTransferData( + NodeTransferWrapper.ATLAS_OBJECT_FLAVOR); + + Point pt = dtde.getLocation(); + DropTargetContext dtc = dtde.getDropTargetContext(); + JTree tree = (JTree) dtc.getComponent(); + TreePath parentpath = tree.getClosestPathForLocation(pt.x, pt.y); + TreeNode targetNode = (TreeNode) parentpath.getLastPathComponent(); + + if (targetNode.equals(sourceNode) || targetNode.getParent().equals(sourceNode)) { + dtde.rejectDrop(); + return; + } + AtlasTreeModel atlasTreeModel = (AtlasTreeModel) atlasTree.getModel(); + if (sourceNode instanceof LayerInterface && targetNode instanceof LayerInterface) + mergeLayers(atlasTreeModel, (LayerInterface) sourceNode, + (LayerInterface) targetNode); + + if (targetNode instanceof MapInterface) + // We can not make a map child of another map + // -> use it's layer instead + targetNode = targetNode.getParent(); + + if (sourceNode instanceof MapInterface && targetNode instanceof LayerInterface) + moveMap(atlasTreeModel, (MapInterface) sourceNode, (LayerInterface) targetNode); + + } catch (Exception e) { + log.error("", e); + atlasTree.getTreeModel().notifyStructureChanged(); + dtde.rejectDrop(); + } + } + + protected void mergeLayers(AtlasTreeModel atlasTreeModel, LayerInterface sourceLayer, + LayerInterface targetLayer) throws InvalidNameException { + int answer = JOptionPane.showConfirmDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_confirm_merge_layer"), + sourceLayer.getName(), targetLayer.getName()), + I18nUtils.localizedStringForKey("msg_confirm_merge_layer_title"), + JOptionPane.YES_NO_OPTION); + if (answer != JOptionPane.YES_OPTION) + return; + try { + atlasTreeModel.mergeLayers(sourceLayer, targetLayer); + } catch (InvalidNameException e) { + JOptionPane.showMessageDialog(null, e.getMessage(), + I18nUtils.localizedStringForKey("msg_merge_layer_failed"), + JOptionPane.ERROR_MESSAGE); + throw e; + } + } + + protected void moveMap(AtlasTreeModel atlasTreeModel, MapInterface map, + LayerInterface targetLayer) throws InvalidNameException { + atlasTreeModel.moveMap(map, targetLayer); + } + + private TreeNode getNodeForEvent(DropTargetDragEvent dtde) { + Point p = dtde.getLocation(); + DropTargetContext dtc = dtde.getDropTargetContext(); + JTree tree = (JTree) dtc.getComponent(); + TreePath path = tree.getClosestPathForLocation(p.x, p.y); + return (TreeNode) path.getLastPathComponent(); + } + + } + +} diff --git a/src/main/java/mobac/gui/atlastree/JAtlasTree.java b/src/main/java/mobac/gui/atlastree/JAtlasTree.java new file mode 100644 index 0000000..e311281 --- /dev/null +++ b/src/main/java/mobac/gui/atlastree/JAtlasTree.java @@ -0,0 +1,412 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.atlastree; + +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.dnd.Autoscroll; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; + +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPopupMenu; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import mobac.gui.MainGUI; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.layer.MapAreaHighlightingLayer; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.AtlasObject; +import mobac.program.interfaces.CapabilityDeletable; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.RequiresSQLite; +import mobac.program.interfaces.ToolTipProvider; +import mobac.program.model.Atlas; +import mobac.program.model.AtlasOutputFormat; +import mobac.program.model.AtlasTreeModel; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.MapSelection; +import mobac.program.model.Profile; +import mobac.program.model.TileImageParameters; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.jdbc.SQLiteLoader; + +import org.apache.log4j.Logger; + +public class JAtlasTree extends JTree implements Autoscroll { + + private static final long serialVersionUID = 1L; + private static final int margin = 12; + + private static final String MSG_ATLAS_VERSION_MISMATCH = I18nUtils.localizedStringForKey("msg_atlas_version_mismatch"); + + private static final String MSG_ATLAS_DATA_CHECK_FAILED = I18nUtils.localizedStringForKey("msg_atlas_data_check_failed"); + + private static final String MSG_ATLAS_EMPTY = I18nUtils.localizedStringForKey("msg_atlas_is_empty"); + + private static final String ACTION_DELETE_NODE = "DELETE_NODE"; + + private static final Logger log = Logger.getLogger(JAtlasTree.class); + + private AtlasTreeModel treeModel; + + private PreviewMap mapView; + + protected NodeRenderer nodeRenderer; + + protected String defaultToolTiptext; + + protected KeyStroke deleteNodeKS; + + protected DragDropController ddc; + + protected boolean displaySelectedMapArea = false; + + public JAtlasTree(PreviewMap mapView) { + super(new AtlasTreeModel()); + if (mapView == null) + throw new NullPointerException("MapView parameter is null"); + this.mapView = mapView; + getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + ddc = new DragDropController(this); + treeModel = (AtlasTreeModel) getModel(); + // setRootVisible(false); + setShowsRootHandles(true); + nodeRenderer = new NodeRenderer(); + setCellRenderer(nodeRenderer); + setCellEditor(new NodeEditor(this)); + setToolTipText(""); + defaultToolTiptext = I18nUtils.localizedStringForKey("lp_atlas_default_tip"); + setAutoscrolls(true); + addMouseListener(new MouseController(this)); + + InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); + ActionMap actionMap = getActionMap(); + + // map moving + inputMap.put(deleteNodeKS = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), ACTION_DELETE_NODE); + actionMap.put(ACTION_DELETE_NODE, new AbstractAction(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_delete_node")) { + + private static final long serialVersionUID = 1L; + + public void actionPerformed(ActionEvent e) { + deleteSelectedNode(); + JAtlasTree.this.mapView.repaint(); + } + + }); + + } + + public boolean testAtlasContentValid() { + AtlasInterface atlas = getAtlas(); + if (RequiresSQLite.class.isAssignableFrom(atlas.getOutputFormat().getMapCreatorClass())) { + if (!SQLiteLoader.loadSQLiteOrShowError()) + return false; + } + if (atlas.calculateTilesToDownload() == 0) { + JOptionPane.showMessageDialog(null, "" + MSG_ATLAS_EMPTY + "", "Error - atlas has no content", + JOptionPane.ERROR_MESSAGE); + return false; + } + return true; + } + + @Override + public String getToolTipText(MouseEvent event) { + if (getRowForLocation(event.getX(), event.getY()) == -1) + return defaultToolTiptext; + TreePath curPath = getPathForLocation(event.getX(), event.getY()); + Object o = curPath.getLastPathComponent(); + if (o == null || !(o instanceof ToolTipProvider)) + return null; + return ((ToolTipProvider) o).getToolTip(); + } + + @Override + public boolean isPathEditable(TreePath path) { + return super.isPathEditable(path) && (path.getLastPathComponent() instanceof AtlasObject); + } + + public AtlasTreeModel getTreeModel() { + return treeModel; + } + + public void newAtlas(String name, AtlasOutputFormat format) { + log.debug("Creating new atlas"); + Atlas newAtlas = Atlas.newInstance(); + newAtlas.setOutputFormat(format); + newAtlas.setName(name); + treeModel.setAtlas(newAtlas); + mapView.repaint(); + } + + public void newAtlas() { + log.debug("Resetting atlas tree model"); + Atlas newAtlas = Atlas.newInstance(); + newAtlas.setName(MainGUI.getMainGUI().getUserText()); + treeModel.setAtlas(newAtlas); + mapView.repaint(); + } + + /** + * Changes the atlas format + */ + public void convertAtlas(AtlasOutputFormat format) { + log.debug("Converting the atlas format to " + format); + treeModel.getAtlas().setOutputFormat(format); + } + + public void deleteSelectedNode() { + TreePath path = getSelectionPath(); + if (path == null) + return; + TreeNode selected = (TreeNode) path.getLastPathComponent(); + int[] selectedRows = getSelectionRows(); + + if (!(selected instanceof CapabilityDeletable)) + return; + treeModel.notifyNodeDelete(selected); + ((CapabilityDeletable) selected).delete(); + + int selRow = Math.min(selectedRows[0], getRowCount() - 1); + TreePath path1 = path.getParentPath(); + TreePath path2 = getPathForRow(selRow).getParentPath(); + if (path1 != path2) { + // next row belongs to different parent node -> we select parent + // node instead + setSelectionPath(path1); + } else { + setSelectionRow(selRow); + scrollRowToVisible(selRow); + } + } + + public AtlasInterface getAtlas() { + return treeModel.getAtlas(); + } + + public boolean load(Profile profile) { + log.debug("Loading profile " + profile); + try { + treeModel.load(profile); + if (treeModel.getAtlas() instanceof Atlas) { + Atlas atlas = (Atlas) treeModel.getAtlas(); + if (atlas.getVersion() < Atlas.CURRENT_ATLAS_VERSION) { + JOptionPane.showMessageDialog(null, MSG_ATLAS_VERSION_MISMATCH, "Outdated atlas version", + JOptionPane.WARNING_MESSAGE); + return true; + } + } + boolean problemsDetected = Profile.checkAtlas(treeModel.getAtlas()); + if (problemsDetected) { + JOptionPane.showMessageDialog(null, MSG_ATLAS_DATA_CHECK_FAILED, "Atlas loading problem", + JOptionPane.WARNING_MESSAGE); + } + return true; + } catch (Exception e) { + GUIExceptionHandler.processException(e); + return false; + } + } + + public boolean save(Profile profile) { + try { + treeModel.save(profile); + return true; + } catch (Exception e) { + GUIExceptionHandler.processException(e); + return false; + } + } + + protected void showNodePopupMenu(MouseEvent event) { + JPopupMenu pm = new JPopupMenu(); + final TreePath selPath = getPathForLocation(event.getX(), event.getY()); + setSelectionPath(selPath); + JMenuItem mi = null; + if (selPath != null) { + // not clicked on empty area + final Object o = selPath.getLastPathComponent(); + if (o == null) + return; + if (o instanceof ToolTipProvider) { + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_show_detail")); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + ToolTipProvider ttp = (ToolTipProvider) o; + JOptionPane.showMessageDialog(MainGUI.getMainGUI(), ttp.getToolTip()); + } + }); + pm.add(mi); + } + if (o instanceof AtlasObject) { + final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_display_select_area")); + final MapAreaHighlightingLayer msl = new MapAreaHighlightingLayer(this); + cbmi.setSelected(displaySelectedMapArea); + cbmi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (displaySelectedMapArea) { + MapAreaHighlightingLayer.removeHighlightingLayers(); + } else { + mapView.setSelectionByTileCoordinate(null, null, false); + MapAreaHighlightingLayer.removeHighlightingLayers(); + mapView.mapLayers.add(msl); + } + displaySelectedMapArea = !displaySelectedMapArea; + mapView.repaint(); + } + }); + pm.add(cbmi); + } + if (o instanceof MapInterface) { + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_select_map_box")); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + MapInterface map = (MapInterface) o; + mapView.setMapSource(map.getMapSource()); + mapView.setSelectionByTileCoordinate(map.getZoom(), map.getMinTileCoordinate(), map + .getMaxTileCoordinate(), true); + } + }); + pm.add(mi); + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_zoom_to_map_box")); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + MapInterface map = (MapInterface) o; + MapSelection ms = new MapSelection(map); + mapView.setMapSource(map.getMapSource()); + mapView.setSelectionAndZoomTo(ms, true); + mapView.setSelectionByTileCoordinate(map.getZoom(), map.getMinTileCoordinate(), map + .getMaxTileCoordinate(), true); + } + }); + pm.add(mi); + } + if (o instanceof LayerInterface) { + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_zoom_to")); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + LayerInterface layer = (LayerInterface) o; + EastNorthCoordinate max = new EastNorthCoordinate(Double.NEGATIVE_INFINITY, + Double.NEGATIVE_INFINITY); + EastNorthCoordinate min = new EastNorthCoordinate(Double.POSITIVE_INFINITY, + Double.POSITIVE_INFINITY); + for (MapInterface map : layer) { + MapSelection ms = new MapSelection(map); + EastNorthCoordinate mapMax = ms.getMax(); + EastNorthCoordinate mapMin = ms.getMin(); + max.lat = Math.max(max.lat, mapMax.lat); + max.lon = Math.max(max.lon, mapMax.lon); + min.lat = Math.min(min.lat, mapMin.lat); + min.lon = Math.min(min.lon, mapMin.lon); + } + MapSelection ms = new MapSelection(mapView.getMapSource(), max, min); + mapView.zoomTo(ms); + } + }); + pm.add(mi); + } + if (o instanceof AtlasObject) { + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_rename")); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + JAtlasTree.this.startEditingAtPath(selPath); + } + }); + pm.add(mi); + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_apply_tile_process")); + mi.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + AtlasObject atlasObject = (AtlasObject) o; + TileImageParameters p = MainGUI.getMainGUI().getSelectedTileImageParameters(); + applyTileImageParameters(atlasObject, p); + } + }); + pm.add(mi); + } + if (o instanceof CapabilityDeletable) { + pm.addSeparator(); + mi = new JMenuItem(getActionMap().get(ACTION_DELETE_NODE)); + mi.setAccelerator(deleteNodeKS); + pm.add(mi); + } + } + if (pm.getComponentCount() > 0) + pm.addSeparator(); + mi = new JMenuItem(I18nUtils.localizedStringForKey("lp_atlas_pop_menu_clear_atals")); + mi.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + newAtlas(); + } + }); + pm.add(mi); + pm.show(this, event.getX(), event.getY()); + } + + protected void applyTileImageParameters(Object o, TileImageParameters p) { + if (o instanceof Iterable) { + Iterable it = (Iterable) o; + for (Object ao : it) { + applyTileImageParameters(ao, p); + } + } else if (o instanceof MapInterface) { + ((MapInterface) o).setParameters(p); + } + } + + protected void selectElementOnMap(Object o) { + if (o instanceof MapInterface) { + MapInterface map = (MapInterface) o; + mapView.setMapSource(map.getMapSource()); + mapView.setSelectionByTileCoordinate(map.getZoom(), map.getMinTileCoordinate(), map.getMaxTileCoordinate(), + true); + } + } + + public void autoscroll(Point cursorLocn) { + int realrow = getRowForLocation(cursorLocn.x, cursorLocn.y); + Rectangle outer = getBounds(); + realrow = (cursorLocn.y + outer.y <= margin ? realrow < 1 ? 0 : realrow - 1 + : realrow < getRowCount() - 1 ? realrow + 1 : realrow); + scrollRowToVisible(realrow); + } + + public Insets getAutoscrollInsets() { + Rectangle outer = getBounds(); + Rectangle inner = getParent().getBounds(); + return new Insets(inner.y - outer.y + margin, inner.x - outer.x + margin, outer.height - inner.height - inner.y + + outer.y + margin, outer.width - inner.width - inner.x + outer.x + margin); + } + +} diff --git a/src/main/java/mobac/gui/atlastree/MouseController.java b/src/main/java/mobac/gui/atlastree/MouseController.java new file mode 100644 index 0000000..60a301a --- /dev/null +++ b/src/main/java/mobac/gui/atlastree/MouseController.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.atlastree; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.tree.TreePath; + +public class MouseController extends MouseAdapter { + + JAtlasTree atlasTree; + + public MouseController(JAtlasTree atlasTree) { + super(); + this.atlasTree = atlasTree; + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() != MouseEvent.BUTTON1 || e.getClickCount() != 2) + return; + TreePath selPath = atlasTree.getSelectionPath(); + if (selPath == null) + return; // clicked on empty area + atlasTree.selectElementOnMap(selPath.getLastPathComponent()); + } + + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + atlasTree.showNodePopupMenu(e); + } + } + + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + atlasTree.showNodePopupMenu(e); + } + } + +} diff --git a/src/main/java/mobac/gui/atlastree/NodeEditor.java b/src/main/java/mobac/gui/atlastree/NodeEditor.java new file mode 100644 index 0000000..417c852 --- /dev/null +++ b/src/main/java/mobac/gui/atlastree/NodeEditor.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.atlastree; + +import java.awt.Component; + +import javax.swing.DefaultCellEditor; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellEditor; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellEditor; +import javax.swing.tree.TreeCellRenderer; + +import mobac.gui.components.JAtlasNameField; + + +public class NodeEditor extends DefaultTreeCellEditor { + + public NodeEditor(JAtlasTree atlasTree) { + super(atlasTree, null); + atlasTree.setEditable(true); + } + + @Override + public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, + boolean expanded, boolean leaf, int row) { + // Each node type has it's own TreeCellRenderer implementation + // this not covered by DefaultTreeCellEditor - therefore we have to + // correct the renderer each time an editorComponent is requested + TreeCellRenderer tcr = tree.getCellRenderer(); + renderer = (DefaultTreeCellRenderer) tcr.getTreeCellRendererComponent(tree, value, + isSelected, expanded, leaf, row, true); + return super.getTreeCellEditorComponent(tree, value, isSelected, expanded, leaf, row); + } + + @Override + protected TreeCellEditor createTreeCellEditor() { + return new DefaultCellEditor(new JAtlasNameField()); + } + +} diff --git a/src/main/java/mobac/gui/atlastree/NodeRenderer.java b/src/main/java/mobac/gui/atlastree/NodeRenderer.java new file mode 100644 index 0000000..18f6e86 --- /dev/null +++ b/src/main/java/mobac/gui/atlastree/NodeRenderer.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.atlastree; + +import java.awt.Component; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellRenderer; + +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.utilities.Utilities; + + +public class NodeRenderer implements TreeCellRenderer { + + private static ImageIcon atlasIcon = new ImageIcon(); + private static ImageIcon layerIcon = new ImageIcon(); + private static ImageIcon mapIcon = new ImageIcon(); + + static { + atlasIcon = Utilities.loadResourceImageIcon("atlas.png"); + layerIcon = Utilities.loadResourceImageIcon("layer.png"); + mapIcon = Utilities.loadResourceImageIcon("map.png"); + } + + DefaultTreeCellRenderer atlasRenderer; + DefaultTreeCellRenderer layerRenderer; + DefaultTreeCellRenderer mapRenderer; + + public NodeRenderer() { + atlasRenderer = new SimpleTreeCellRenderer(atlasIcon); + layerRenderer = new SimpleTreeCellRenderer(layerIcon); + mapRenderer = new SimpleTreeCellRenderer(mapIcon); + } + + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, + boolean expanded, boolean leaf, int row, boolean hasFocus) { + TreeCellRenderer tcr; + if (value instanceof AtlasInterface) { + tcr = atlasRenderer; + } else if (value instanceof LayerInterface) + tcr = layerRenderer; + else + tcr = mapRenderer; + return tcr.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, + hasFocus); + } + + protected static class SimpleTreeCellRenderer extends DefaultTreeCellRenderer { + + private static final long serialVersionUID = 1L; + + public SimpleTreeCellRenderer(Icon icon) { + super(); + setIcon(icon); + setOpenIcon(icon); + setClosedIcon(icon); + setLeafIcon(icon); + } + } +} diff --git a/src/main/java/mobac/gui/atlastree/NodeTransferWrapper.java b/src/main/java/mobac/gui/atlastree/NodeTransferWrapper.java new file mode 100644 index 0000000..b933872 --- /dev/null +++ b/src/main/java/mobac/gui/atlastree/NodeTransferWrapper.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.atlastree; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; + +import javax.swing.tree.TreeNode; + +import mobac.program.interfaces.AtlasObject; + + +public class NodeTransferWrapper implements Transferable { + + public static final DataFlavor ATLAS_OBJECT_FLAVOR = new DataFlavor(AtlasObject.class, "AtlasObject"); + public static final DataFlavor[] FLAVORS = new DataFlavor[] { ATLAS_OBJECT_FLAVOR }; + + private TreeNode node; + + public NodeTransferWrapper(TreeNode node) { + this.node = node; + } + + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (!ATLAS_OBJECT_FLAVOR.equals(flavor)) + throw new UnsupportedFlavorException(flavor); + return node; + } + + public DataFlavor[] getTransferDataFlavors() { + return FLAVORS; + } + + public boolean isDataFlavorSupported(DataFlavor flavor) { + return ATLAS_OBJECT_FLAVOR.equals(flavor); + } + +} diff --git a/src/main/java/mobac/gui/components/FilledLayeredPane.java b/src/main/java/mobac/gui/components/FilledLayeredPane.java new file mode 100644 index 0000000..3898b6e --- /dev/null +++ b/src/main/java/mobac/gui/components/FilledLayeredPane.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.Component; + +import javax.swing.JLayeredPane; + +public class FilledLayeredPane extends JLayeredPane { + + private static final long serialVersionUID = 1L; + + /** + * Layout each of the components in this JLayeredPane so that they all fill + * the entire extents of the layered pane -- from (0,0) to (getWidth(), + * getHeight()) + */ + @Override + public void doLayout() { + // Synchronizing on getTreeLock, because I see other layouts doing + // that. + // see BorderLayout::layoutContainer(Container) + synchronized (getTreeLock()) { + int w = getWidth(); + int h = getHeight(); + for (Component c : getComponents()) { + c.setBounds(0, 0, w, h); + } + } + } +} diff --git a/src/main/java/mobac/gui/components/JAtlasNameField.java b/src/main/java/mobac/gui/components/JAtlasNameField.java new file mode 100644 index 0000000..df05a03 --- /dev/null +++ b/src/main/java/mobac/gui/components/JAtlasNameField.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import javax.swing.JTextField; + +import mobac.program.model.Profile; + + +/** + * A {@link JTextField} that only accepts ASCII characters, numbers and spaces. + * + */ +public class JAtlasNameField extends JRegexTextField { + + private static final long serialVersionUID = 1L; + + public JAtlasNameField() { + super(Profile.PROFILE_NAME_REGEX, 40); + } + +} diff --git a/src/main/java/mobac/gui/components/JBookmarkMenuItem.java b/src/main/java/mobac/gui/components/JBookmarkMenuItem.java new file mode 100644 index 0000000..cd9d5fa --- /dev/null +++ b/src/main/java/mobac/gui/components/JBookmarkMenuItem.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; + +import mobac.gui.MainGUI; +import mobac.program.model.Bookmark; + +public class JBookmarkMenuItem extends JMenuItem implements ActionListener { + + private final Bookmark bookmark; + + public JBookmarkMenuItem(Bookmark bookmark) { + super(bookmark.toString()); + this.bookmark = bookmark; + addActionListener(this); + } + + public void actionPerformed(ActionEvent paramActionEvent) { + MainGUI.getMainGUI().previewMap.gotoPositionBookmark(bookmark); + + } + +} diff --git a/src/main/java/mobac/gui/components/JCollapsiblePanel.java b/src/main/java/mobac/gui/components/JCollapsiblePanel.java new file mode 100644 index 0000000..7d6e3bc --- /dev/null +++ b/src/main/java/mobac/gui/components/JCollapsiblePanel.java @@ -0,0 +1,343 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagLayout; +import java.awt.LayoutManager; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.Border; + +import mobac.utilities.GBC; +import mobac.utilities.Utilities; + + +/** + * Bases upon "TitleContainer" from project "swivel" + * http://code.google.com/p/swivel/ (LGPL license) + * + * A {@code TitleContainer} is a simple container that provides an easily + * visible title. + * + */ +public class JCollapsiblePanel extends JPanel { + + private static final long serialVersionUID = 1L; + + protected static final int DEFAULT_TITLE_PADDING = 3; + protected static final Color DEFAULT_TITLE_BACKGROUND_COLOR = Color.LIGHT_GRAY; + protected static final Color DEFAULT_TITLE_COLOR = Color.BLACK; + + private static ImageIcon arrowClosed; + private static ImageIcon arrowOpen; + + static { + try { + arrowClosed = Utilities.loadResourceImageIcon("arrow_closed.png"); + arrowOpen = Utilities.loadResourceImageIcon("arrow_open.png"); + } catch (Exception e) { + arrowClosed = new ImageIcon(); + arrowOpen = new ImageIcon(); + } + } + + // title + protected final JLabel titleIcon; + protected final JLabel titleLabel; + protected final JPanel titlePanel; + + // main component + protected Container contentContainer; + + // collapsing + protected final CollapsingMouseListener collapsingMouseListener; + private boolean isCollapsed; + + /** + * Constructs a {@code TitleContainer} that wraps the specified container. + * + * @param container + * the main container + */ + public JCollapsiblePanel(Container container) { + this(container, ""); + } + + public JCollapsiblePanel(String title) { + this(new JPanel(), title); + } + + public JCollapsiblePanel(String title, LayoutManager layout) { + this(new JPanel(layout), title); + setName(title); + } + + /** + * Constructs a {@code TitleContainer} that wraps the specified component + * and has the specified title. + * + * @param container + * the main container + * @param title + * the title + */ + public JCollapsiblePanel(Container container, String title) { + super(); + setName(title); + titleIcon = new JLabel(arrowOpen); + titleLabel = new JLabel(title); + titlePanel = new JPanel(new GridBagLayout()); + // mainComponentPanel = new JPanel(new GridBagLayout()); + collapsingMouseListener = new CollapsingMouseListener(); + + titleIcon.setMinimumSize(new Dimension(40, 40)); + titleIcon.setPreferredSize(titleIcon.getPreferredSize()); + + // set collapse behavior + titlePanel.addMouseListener(collapsingMouseListener); + + // look and feel + setTitleBackgroundColor(DEFAULT_TITLE_BACKGROUND_COLOR); + setTitleColor(DEFAULT_TITLE_COLOR); + setTitleBarPadding(DEFAULT_TITLE_PADDING); + + // layout + titlePanel.add(titleIcon, GBC.std()); + titlePanel.add(titleLabel, GBC.std().insets(5, 0, 1, 0)); + titlePanel.add(Box.createHorizontalGlue(), GBC.eol().fill()); + setLayout(new BorderLayout()); + add(titlePanel, BorderLayout.NORTH); + add(container, BorderLayout.CENTER); + setContentContainer(container); + setBorder(BorderFactory.createEtchedBorder()); + + } + + /** + * This method provides a programmatic way to collapse the container. + * + * @param collapsed + * whether the container should be collapsed or shown + */ + public void setCollapsed(boolean collapsed) { + if (isCollapsed == collapsed) { + return; + } + + if (collapsed) { + titleIcon.setIcon(arrowClosed); + + // We have to make sure that the panel width does not shrink because + // of the hidden content of contentContainer + Dimension dcont = contentContainer.getLayout().preferredLayoutSize(contentContainer); + Dimension pref = titlePanel.getPreferredSize(); + titlePanel.setPreferredSize(new Dimension(dcont.width, pref.height)); + } else { + titleIcon.setIcon(arrowOpen); + } + contentContainer.setVisible(!collapsed); + + isCollapsed = collapsed; + revalidate(); + } + + /** + * Gets whether the container is collapsed or not. + * + * @return whether the container is collapsed or not + */ + public boolean isCollapsed() { + return isCollapsed; + } + + /** + * Sets the title of this container. + * + * @param title + * the title + */ + public void setTitle(String title) { + if (title != null) { + titleLabel.setText(title); + } else { + titleLabel.setText(""); + } + } + + /** + * Gets the this container's title. + * + * @return the title + */ + public String getTitle() { + return titleLabel.getText(); + } + + /** + * Sets the main content container for this container. + * + * @param container + * the main content container + */ + public void setContentContainer(Container container) { + // don't need to do anything if the main component hasn't changed + if (container == this.contentContainer) { + return; + } + + // remove the main component + if (this.contentContainer != null) { + remove(this.contentContainer); + } + + // replace the main component + this.contentContainer = container; + add(container, BorderLayout.CENTER); + + // repaint main component + revalidate(); + } + + /** + * Gets the main content container for this container. + * + * @return the main container + */ + public Container getContentContainer() { + return contentContainer; + } + + public void addContent(Component comp, Object constraints) { + contentContainer.add(comp, constraints); + } + + /** + * Sets the title font. + * + * @param font + * the title font + */ + public void setTitleFont(Font font) { + super.setFont(font); + if (titleLabel != null) { + titleLabel.setFont(font); + } + } + + /** + * Sets the background color of the title bar. + * + * @param color + * the color + */ + public void setTitleBackgroundColor(Color color) { + this.titlePanel.setBackground(color); + } + + /** + * Sets the title text color. + * + * @param color + * the color + */ + public void setTitleColor(Color color) { + this.titleLabel.setForeground(color); + } + + /** + * Sets the title bar padding in pixels. + * + * @param padding + * the title bar padding in pixels + */ + public void setTitleBarPadding(int padding) { + Border border = BorderFactory.createEmptyBorder(padding, padding, padding, padding); + this.titlePanel.setBorder(border); + } + + /** + * Sets the visibility of the title bar. If the title bar is invisible, the + * user will not be able to collapse or decollapse the container. + * + * @param visible + * visibility of the title bar + */ + public void setTitleBarVisible(boolean visible) { + this.titlePanel.setVisible(visible); + } + + /** + * Gets the visibility of the title bar. + * + * @return true if the title bar is visible, false otherwise + */ + public boolean isTitleBarVisible() { + return this.titlePanel.isVisible(); + } + + //-------------------------------------------------------------------------- + + /** + * A {@code MouseListener} that changes the cursor when moved over the title + * bar to indicate that it is clickable. Clicking the title bar collapses + * the container. + */ + private class CollapsingMouseListener extends MouseAdapter { + private final Cursor CLICK_ME_CURSOR = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + + /** + * Collapses or shows the titled component. + * + * {@inheritDoc} + */ + public void mousePressed(MouseEvent e) { + setCollapsed(!JCollapsiblePanel.this.isCollapsed); + } + + /** + * Changes the cursor to indicate clickability. + * + * {@inheritDoc} + */ + public void mouseEntered(MouseEvent e) { + titlePanel.setCursor(CLICK_ME_CURSOR); + } + + /** + * Changes the cursor back to the default. + * + * {@inheritDoc} + */ + public void mouseExited(MouseEvent e) { + titlePanel.setCursor(Cursor.getDefaultCursor()); + } + } + +} diff --git a/src/main/java/mobac/gui/components/JCoordinateField.java b/src/main/java/mobac/gui/components/JCoordinateField.java new file mode 100644 index 0000000..6a55d85 --- /dev/null +++ b/src/main/java/mobac/gui/components/JCoordinateField.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.Color; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; + +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.geo.CoordinateDms2Format; + +public class JCoordinateField extends JTextField { + + private static final long serialVersionUID = 1L; + + private static final Color ERROR_COLOR = new Color(255, 100, 100); + + private static final String INVALID_TEXT = I18nUtils.localizedStringForKey("lp_coords_invalid_text"); + + private JCoordinateListener coordinateListener; + private boolean inputIsValid = true; + + private NumberFormat numberFormat; + + private final double min; + private final double max; + + public JCoordinateField(double min, double max) { + super(10); + this.min = min; + this.max = max; + numberFormat = new CoordinateDms2Format(new DecimalFormatSymbols()); + coordinateListener = new JCoordinateListener(); + coordinateListener.checkCoordinate(null); + } + + @Override + public Point getToolTipLocation(MouseEvent event) { + if (getToolTipText().length() > 0) + return super.getToolTipLocation(event); + else + // We don't want a tool tip but Java does not allow to disable it? + // -> show it at a point where no user will ever see it + return new Point(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + public void setCoordinate(double value) { + try { + // We know that the number is valid, therefore we can skip the check + // -> saves CPU power while selecting via preview map + boolean newValid = true; + coordinateListener.setEnabled(false); + if (Double.isNaN(value)) { + super.setText(""); + newValid = false; + } else { + super.setText(numberFormat.format(value)); + } + if (newValid != inputIsValid) + coordinateListener.changeValidMode(true); + } finally { + coordinateListener.setEnabled(true); + } + } + + public double getCoordinate() throws ParseException { + ParsePosition pos = new ParsePosition(0); + String text = JCoordinateField.this.getText(); + Number num = numberFormat.parse(text, pos); + if (num == null || pos.getErrorIndex() >= 0 || Double.isNaN(num.doubleValue())) + throw new ParseException(text, pos.getErrorIndex()); + return num.doubleValue(); + } + + public double getCoordinateOrNaN() { + ParsePosition pos = new ParsePosition(0); + String text = JCoordinateField.this.getText(); + Number num = numberFormat.parse(text, pos); + if (num == null || pos.getErrorIndex() >= 0) + return Double.NaN; + return num.doubleValue(); + } + + public boolean isInputValid() { + return inputIsValid; + } + + public NumberFormat getNumberFormat() { + return numberFormat; + } + + public void setNumberFormat(NumberFormat numberFormat) { + double coord = getCoordinateOrNaN(); + this.numberFormat = numberFormat; + setCoordinate(coord); + } + + protected class JCoordinateListener implements DocumentListener { + + private Color defaultColor; + + private boolean enabled; + + private JCoordinateListener() { + enabled = true; + defaultColor = JCoordinateField.this.getBackground(); + JCoordinateField.this.getDocument().addDocumentListener(this); + } + + private void checkCoordinate(DocumentEvent de) { + if (!enabled) + return; + boolean valid = false; + try { + ParsePosition pos = new ParsePosition(0); + String text = JCoordinateField.this.getText(); + Number num = numberFormat.parse(text, pos); + if (num == null) { + valid = false; + return; + } + double d = num.doubleValue(); + valid = (!Double.isNaN(d)) && (d >= min) && (d <= max); + } catch (Exception e) { + valid = false; + } + if (valid != inputIsValid) + changeValidMode(valid); + } + + private void changeValidMode(boolean valid) { + Color newC = valid ? defaultColor : ERROR_COLOR; + JCoordinateField.this.setBackground(newC); + String toolTip = valid ? "" : String.format(INVALID_TEXT, numberFormat.format(min), + numberFormat.format(max)); + JCoordinateField.this.setToolTipText(toolTip); + if (toolTip.length() > 0) + Utilities.showTooltipNow(JCoordinateField.this); + inputIsValid = valid; + } + + public void changedUpdate(DocumentEvent e) { + checkCoordinate(e); + } + + public void insertUpdate(DocumentEvent e) { + checkCoordinate(e); + } + + public void removeUpdate(DocumentEvent e) { + checkCoordinate(e); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } +} diff --git a/src/main/java/mobac/gui/components/JDirectoryChooser.java b/src/main/java/mobac/gui/components/JDirectoryChooser.java new file mode 100644 index 0000000..446259e --- /dev/null +++ b/src/main/java/mobac/gui/components/JDirectoryChooser.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.filechooser.FileFilter; + +import mobac.utilities.I18nUtils; + +public class JDirectoryChooser extends JFileChooser { + + private static final long serialVersionUID = -1954689476383812988L; + + public JDirectoryChooser() { + super(); + setDialogType(CUSTOM_DIALOG); + setDialogTitle(I18nUtils.localizedStringForKey("dlg_select_dir_title")); + //setApproveButtonText("Select Directory"); + setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + setAcceptAllFileFilterUsed(false); + addChoosableFileFilter(new FileFilter() { + + @Override + public boolean accept(File f) { + return f.isDirectory(); + } + + @Override + public String getDescription() { + return I18nUtils.localizedStringForKey("dlg_select_dir_description"); + } + }); + } + + @Override + public void approveSelection() { + if (!this.getFileFilter().accept(this.getSelectedFile())) + return; + super.approveSelection(); + } + +} diff --git a/src/main/java/mobac/gui/components/JDistanceSlider.java b/src/main/java/mobac/gui/components/JDistanceSlider.java new file mode 100644 index 0000000..755f51d --- /dev/null +++ b/src/main/java/mobac/gui/components/JDistanceSlider.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.util.Hashtable; + +import javax.swing.JLabel; +import javax.swing.JSlider; + +import mobac.program.interfaces.MapSpace; +import mobac.program.model.UnitSystem; + +public class JDistanceSlider extends JSlider { + + private static final long serialVersionUID = 1L; + + private final Hashtable labelTable; + + public JDistanceSlider(MapSpace mapSpace, int zoom, int y, UnitSystem unit, int pixelMin, int pixelMax) { + super(pixelMin, pixelMax); + labelTable = new Hashtable(); + + int diff4 = (pixelMax - pixelMin) / 4; + int[] labelvalues = new int[] { pixelMin, pixelMin + diff4, pixelMin + 2 * diff4, pixelMin + 3 * diff4, + pixelMax }; + + for (int i : labelvalues) { + double distance = mapSpace.horizontalDistance(zoom, y, i) * unit.earthRadius * unit.unitFactor; + String label; + if (distance > unit.unitFactor) { + distance /= unit.unitFactor; + label = String.format("%2.0f %s", distance, unit.unitLarge); + } else + label = String.format("%2.0f %s", distance, unit.unitSmall); + labelTable.put(new Integer(i), new JLabel(label)); + } + setPaintTicks(true); + setMajorTickSpacing(diff4); + setLabelTable(labelTable); + setPaintLabels(true); + } +} diff --git a/src/main/java/mobac/gui/components/JDropDownButton.java b/src/main/java/mobac/gui/components/JDropDownButton.java new file mode 100644 index 0000000..fe8a6cf --- /dev/null +++ b/src/main/java/mobac/gui/components/JDropDownButton.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.SwingConstants; +import javax.swing.plaf.basic.BasicArrowButton; + +public class JDropDownButton extends JButton { + + private static final long serialVersionUID = 1L; + + private BasicArrowButton arrowButton; + private JPopupMenu buttonPopupMenu; + + public JDropDownButton(String text) { + super(text); + buttonPopupMenu = new JPopupMenu(); + arrowButton = new BasicArrowButton(SwingConstants.SOUTH, null, null, Color.BLACK, null); + arrowButton.setBorder(BorderFactory.createEmptyBorder()); + arrowButton.setFocusable(false); + setHorizontalAlignment(SwingConstants.LEFT); + setLayout(new BorderLayout()); + add(arrowButton, BorderLayout.EAST); + arrowButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Rectangle r = getBounds(); + buttonPopupMenu.show(JDropDownButton.this, r.x, r.y + r.height); + } + }); + } + + public void addDropDownItem(String text, ActionListener l) { + JMenuItem item = new JMenuItem(text); + item.addActionListener(l); + buttonPopupMenu.add(item); + } + + public void addDropDownItem(String text, Action a) { + JMenuItem item = new JMenuItem(text); + item.setAction(a); + buttonPopupMenu.add(item); + } + + public void addDropDownItem(JMenuItem item) { + buttonPopupMenu.add(item); + } + +} diff --git a/src/main/java/mobac/gui/components/JIntCombo.java b/src/main/java/mobac/gui/components/JIntCombo.java new file mode 100644 index 0000000..05b523a --- /dev/null +++ b/src/main/java/mobac/gui/components/JIntCombo.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.Component; +import java.awt.event.ActionListener; +import java.util.Vector; + +import javax.swing.ComboBoxEditor; +import javax.swing.JComboBox; + +import org.apache.log4j.Logger; + +public class JIntCombo extends JComboBox { + + private static final long serialVersionUID = 1L; + + static Logger log = Logger.getLogger(JIntCombo.class); + + protected JIntField editorComponent; + protected Integer defaultValue; + + public JIntCombo(Vector values, Integer defaultValue) { + super(values); + this.defaultValue = defaultValue; + + createEditorComponent(); + setEditable(true); + setEditor(new Editor()); + setMaximumRowCount(values.size()); + setSelectedItem(defaultValue); + } + + protected void createEditorComponent() { + } + + public int getValue() { + try { + return editorComponent.getValue(); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public void setValue(int newValue) { + setSelectedIndex(-1); + editorComponent.setValue(newValue, true); + } + + public boolean isValueValid() { + return editorComponent.isInputValid(); + } + + protected class Editor implements ComboBoxEditor { + + public Editor() { + super(); + } + + public void addActionListener(ActionListener l) { + editorComponent.addActionListener(l); + } + + public Component getEditorComponent() { + return editorComponent; + } + + public Object getItem() { + try { + return editorComponent.getValue(); + } catch (NumberFormatException e) { + return getValue(); + } + } + + public void removeActionListener(ActionListener l) { + editorComponent.removeActionListener(l); + } + + public void selectAll() { + editorComponent.selectAll(); + } + + public void setItem(Object entry) { + if (entry == null) + return; + editorComponent.setValue(((Integer) entry).intValue(), true); + } + + } + +} diff --git a/src/main/java/mobac/gui/components/JIntField.java b/src/main/java/mobac/gui/components/JIntField.java new file mode 100644 index 0000000..a8189c4 --- /dev/null +++ b/src/main/java/mobac/gui/components/JIntField.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.Color; + +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import mobac.program.model.NumericDocument; +import mobac.utilities.Utilities; + + +public class JIntField extends JTextField { + + private static final long serialVersionUID = 1L; + + protected Color errorColor = new Color(255, 100, 100); + + public int min = 0; + + public int max = 0; + + private String errorText; + + private InputListener listener; + private boolean inputIsValid = true; + + public JIntField(int min, int max, int columns, String errorText) { + super(columns); + this.min = min; + this.max = max; + this.errorText = errorText; + setDocument(new NumericDocument()); + listener = new InputListener(); + listener.checkInput(null); + setBorder(new EmptyBorder(2, 2, 2, 0)); + } + + public void setErrorColor(Color c) { + errorColor = c; + } + + public int getValue() throws NumberFormatException { + return Integer.parseInt(getText()); + } + + public void setValue(int newValue, boolean check) { + if (newValue <= 0) + super.setText(""); + else + super.setText(Integer.toString(newValue)); + if (check) + listener.checkInput(null); + } + + public void setText(String t) { + throw new RuntimeException("Calling setText() is not allowed!"); + } + + public boolean isInputValid() { + return testInputValid(); + } + + private boolean testInputValid() { + try { + int i = Integer.parseInt(getText()); + return (i >= min) && (i <= max); + } catch (NumberFormatException e) { + return false; + } + } + + + + protected class InputListener implements DocumentListener { + + private Color defaultColor; + + private InputListener() { + defaultColor = JIntField.this.getBackground(); + JIntField.this.getDocument().addDocumentListener(this); + } + + private void checkInput(DocumentEvent de) { + boolean valid = false; + try { + valid = testInputValid(); + } catch (Exception e) { + valid = false; + } + if (valid != inputIsValid) + setDisplayedValidMode(valid); + inputIsValid = valid; + } + + private void setDisplayedValidMode(boolean valid) { + Color newC = valid ? defaultColor : errorColor; + JIntField.this.setBackground(newC); + String toolTip = valid ? "" : String.format(errorText, new Object[] { min, max }); + JIntField.this.setToolTipText(toolTip); + if (toolTip.length() > 0) + Utilities.showTooltipNow(JIntField.this); + } + + public void changedUpdate(DocumentEvent e) { + checkInput(e); + } + + public void insertUpdate(DocumentEvent e) { + checkInput(e); + } + + public void removeUpdate(DocumentEvent e) { + checkInput(e); + } + + } +} diff --git a/src/main/java/mobac/gui/components/JMapSizeCombo.java b/src/main/java/mobac/gui/components/JMapSizeCombo.java new file mode 100644 index 0000000..9876dd7 --- /dev/null +++ b/src/main/java/mobac/gui/components/JMapSizeCombo.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.Color; +import java.util.Vector; + +import org.apache.log4j.Logger; + +public class JMapSizeCombo extends JIntCombo { + + private static final long serialVersionUID = 1L; + + public static final int MIN = 10; + + public static final int MAX = Integer.MAX_VALUE; + + static Vector MAP_SIZE_VALUES; + + static Integer DEFAULT; + + static Logger log = Logger.getLogger(JMapSizeCombo.class); + + static { + // Sizes from 1024 to 32768 + MAP_SIZE_VALUES = new Vector(11); + MAP_SIZE_VALUES.addElement(new Integer(128000)); + MAP_SIZE_VALUES.addElement(new Integer(1048575)); + MAP_SIZE_VALUES.addElement(new Integer(65536)); + MAP_SIZE_VALUES.addElement(DEFAULT = new Integer(32767)); + MAP_SIZE_VALUES.addElement(new Integer(30000)); + MAP_SIZE_VALUES.addElement(new Integer(25000)); + MAP_SIZE_VALUES.addElement(new Integer(20000)); + MAP_SIZE_VALUES.addElement(new Integer(15000)); + MAP_SIZE_VALUES.addElement(new Integer(10000)); + MAP_SIZE_VALUES.addElement(new Integer(2048)); + MAP_SIZE_VALUES.addElement(new Integer(1024)); + } + + public JMapSizeCombo() { + super(MAP_SIZE_VALUES, DEFAULT); + setEditable(true); + setEditor(new Editor()); + setMaximumRowCount(MAP_SIZE_VALUES.size()); + setSelectedItem(DEFAULT); + } + + @Override + protected void createEditorComponent() { + editorComponent = new JIntField(MIN, MAX, 4, ""); + editorComponent.setErrorColor(Color.ORANGE); + } + +} diff --git a/src/main/java/mobac/gui/components/JMenuItem2.java b/src/main/java/mobac/gui/components/JMenuItem2.java new file mode 100644 index 0000000..6f47032 --- /dev/null +++ b/src/main/java/mobac/gui/components/JMenuItem2.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JMenuItem; + +public class JMenuItem2 extends JMenuItem implements ActionListener { + + private Class actionClass; + + public JMenuItem2(String text, int mnemonic, Class actionClass) { + super(text, mnemonic); + this.actionClass = actionClass; + addActionListener(this); + } + + public JMenuItem2(String text, Class actionClass) { + super(text); + this.actionClass = actionClass; + addActionListener(this); + } + + public void actionPerformed(ActionEvent event) { + ActionListener al; + try { + al = actionClass.newInstance(); + al.actionPerformed(event); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/mobac/gui/components/JObjectCheckBox.java b/src/main/java/mobac/gui/components/JObjectCheckBox.java new file mode 100644 index 0000000..e429aa7 --- /dev/null +++ b/src/main/java/mobac/gui/components/JObjectCheckBox.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JCheckBox; + +/** + * An extended {@link JCheckBox} implementation that allows to link one user + * object to the checkbox. + * + * @param + * type of the user object linked/stored with the checkbox + */ +public class JObjectCheckBox extends JCheckBox { + + private static final long serialVersionUID = 1L; + + private E object; + + public JObjectCheckBox(Icon icon) { + super(icon); + } + + public JObjectCheckBox(String text) { + super(text); + } + + public JObjectCheckBox(Action a) { + super(a); + } + + public JObjectCheckBox(Icon icon, boolean selected) { + super(icon, selected); + } + + public JObjectCheckBox(String text, boolean selected) { + super(text, selected); + } + + public JObjectCheckBox(String text, Icon icon) { + super(text, icon); + } + + public JObjectCheckBox(String text, Icon icon, boolean selected) { + super(text, icon, selected); + } + + public E getObject() { + return object; + } + + public void setObject(E object) { + this.object = object; + } + +} diff --git a/src/main/java/mobac/gui/components/JProfilesComboBox.java b/src/main/java/mobac/gui/components/JProfilesComboBox.java new file mode 100644 index 0000000..f31b44f --- /dev/null +++ b/src/main/java/mobac/gui/components/JProfilesComboBox.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.plaf.basic.BasicComboBoxEditor; + +import mobac.program.model.Profile; + +/** + * An editable {@link JComboBox} for displaying the saved atlases profiles. + */ +public class JProfilesComboBox extends JComboBox { + + private static final long serialVersionUID = 1L; + + public JProfilesComboBox() { + super(); + setEditable(true); + setEditor(new ProfilesComboBoxEditor()); + } + + public void loadProfilesList() { + setModel(new DefaultComboBoxModel(Profile.getProfiles())); + setSelectedIndex(-1); + } + + public boolean deleteSelectedProfile() { + Profile profile = (Profile) getSelectedItem(); + if (profile == null) + return false; + profile.delete(); + setSelectedIndex(-1); + removeItem(profile); + return true; + } + + /** + * + * @return the selected profile or null if no profile is + * selected or a new unsaved profile is selected + */ + public Profile getSelectedProfile() { + Object selItem = getSelectedItem(); + if (selItem instanceof Profile) + return (Profile) selItem; + else + return null; + } + + protected static class ProfilesComboBoxEditor extends BasicComboBoxEditor { + @Override + protected JTextField createEditorComponent() { + JAtlasNameField field = new JAtlasNameField(); + field.setBorder(new EmptyBorder(2, 2, 2, 0)); + return field; + } + } +} diff --git a/src/main/java/mobac/gui/components/JRegexTextField.java b/src/main/java/mobac/gui/components/JRegexTextField.java new file mode 100644 index 0000000..6605ef0 --- /dev/null +++ b/src/main/java/mobac/gui/components/JRegexTextField.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.awt.Toolkit; +import java.util.regex.Pattern; + +import javax.swing.JTextField; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.PlainDocument; + +public class JRegexTextField extends JTextField { + + private static final long serialVersionUID = 1L; + + public JRegexTextField(String regex, int maxLength) { + super(); + setDocument(new AtlasNameDocument(regex, maxLength)); + } + + public class AtlasNameDocument extends PlainDocument { + + private static final long serialVersionUID = 1L; + + public Pattern pattern; + public int maxLength; + + public AtlasNameDocument(String regex, int maxLength) { + super(); + pattern = Pattern.compile(regex); + this.maxLength = maxLength; + } + + public void insertString(int offset, String str, AttributeSet attr) + throws BadLocationException { + + if (str == null) + return; + + if (!pattern.matcher(str).matches()) { + Toolkit.getDefaultToolkit().beep(); + return; + } + + String oldText = JRegexTextField.this.getText(); + + super.insertString(offset, str, attr); + + // Maximum length exceeded? + if (JRegexTextField.this.getText().length() > maxLength) { + JRegexTextField.this.setText(oldText); + Toolkit.getDefaultToolkit().beep(); + } + } + } +} diff --git a/src/main/java/mobac/gui/components/JTileSizeCombo.java b/src/main/java/mobac/gui/components/JTileSizeCombo.java new file mode 100644 index 0000000..4ea1fd9 --- /dev/null +++ b/src/main/java/mobac/gui/components/JTileSizeCombo.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.util.Vector; + +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +public class JTileSizeCombo extends JIntCombo { + + private static final long serialVersionUID = 1L; + + public static final int MIN = 50; + + public static final int MAX = 8192; + + static Vector TILE_SIZE_VALUES; + + static Integer DEFAULT; + + static Logger log = Logger.getLogger(JTileSizeCombo.class); + + static { + DEFAULT = new Integer(256); + TILE_SIZE_VALUES = new Vector(); + TILE_SIZE_VALUES.addElement(new Integer(64)); + TILE_SIZE_VALUES.addElement(new Integer(128)); + TILE_SIZE_VALUES.addElement(DEFAULT); + TILE_SIZE_VALUES.addElement(new Integer(512)); + TILE_SIZE_VALUES.addElement(new Integer(768)); + TILE_SIZE_VALUES.addElement(new Integer(1024)); + TILE_SIZE_VALUES.addElement(new Integer(1536)); + for (int i = 2048; i <= MAX; i += 1024) { + TILE_SIZE_VALUES.addElement(new Integer(i)); + } + } + + public JTileSizeCombo() { + super(TILE_SIZE_VALUES, DEFAULT); + setEditable(true); + setEditor(new Editor()); + setMaximumRowCount(TILE_SIZE_VALUES.size()); + setSelectedItem(DEFAULT); + } + + @Override + protected void createEditorComponent() { + editorComponent = new JIntField(MIN, MAX, 4, I18nUtils.localizedStringForKey("msg_invalid_tile_size")); + } + +} diff --git a/src/main/java/mobac/gui/components/JTimeSlider.java b/src/main/java/mobac/gui/components/JTimeSlider.java new file mode 100644 index 0000000..8fddcac --- /dev/null +++ b/src/main/java/mobac/gui/components/JTimeSlider.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import java.util.Hashtable; +import java.util.concurrent.TimeUnit; + +import javax.swing.JLabel; +import javax.swing.JSlider; + +public class JTimeSlider extends JSlider { + + private static final long serialVersionUID = 1L; + + private static final Hashtable LABEL_TABLE; + + private static final double LOG2 = Math.log(2d); + private static final double C1 = -1000.4d / LOG2; + private static final double C2 = 3d * LOG2; + + private static final int MIN = 10000; + + private static final int MAX = 31000; + + static { + LABEL_TABLE = new Hashtable(); + int hour = timeToSliderValue(TimeUnit.HOURS.toMillis(1)); + int day = timeToSliderValue(TimeUnit.DAYS.toMillis(1)); + int month = timeToSliderValue(TimeUnit.DAYS.toMillis(31)); + int year = timeToSliderValue(TimeUnit.DAYS.toMillis(365)); + LABEL_TABLE.put(new Integer(MIN), new JLabel("0 sec")); + LABEL_TABLE.put(new Integer(hour), new JLabel("1 hour")); + LABEL_TABLE.put(new Integer(day), new JLabel("1 day")); + LABEL_TABLE.put(new Integer(month), new JLabel("1 month")); + LABEL_TABLE.put(new Integer(year), new JLabel("1 year")); + LABEL_TABLE.put(new Integer(MAX), new JLabel("never")); + } + + public JTimeSlider() { + super(MIN, MAX); + setMinorTickSpacing(1000); + setPaintTicks(true); + setLabelTable(LABEL_TABLE); + setPaintLabels(true); + } + + public long getTimeSecondsValue() { + return sliderToTimeValue(super.getValue()) / 1000; + } + + public long getTimeMilliValue() { + return sliderToTimeValue(super.getValue()); + } + + public void setTimeMilliValue(long time) { + super.setValue(timeToSliderValue(time)); + } + + private static long sliderToTimeValue(int sliderValue) { + return -1024000 + (long) (1000 * Math.pow(2d, sliderValue / 1000d)); + } + + private static int timeToSliderValue(long timeValue) { + + return (int) (C1 * (C2 - Math.log((timeValue + 1024000) / 125d))); + } +} diff --git a/src/main/java/mobac/gui/components/JZoomCheckBox.java b/src/main/java/mobac/gui/components/JZoomCheckBox.java new file mode 100644 index 0000000..ac6883a --- /dev/null +++ b/src/main/java/mobac/gui/components/JZoomCheckBox.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.components; + +import javax.swing.JCheckBox; + +import mobac.utilities.I18nUtils; + +public class JZoomCheckBox extends JCheckBox { + + private static final long serialVersionUID = 1L; + + private final int zoomLevel; + + public JZoomCheckBox(int zoomLevel) { + super(); + this.zoomLevel = zoomLevel; + setToolTipText(String.format(I18nUtils.localizedStringForKey("lp_zoom_number_tips"),zoomLevel)); + } + + public int getZoomLevel() { + return zoomLevel; + } + + +} diff --git a/src/main/java/mobac/gui/dialogs/AboutDialog.java b/src/main/java/mobac/gui/dialogs/AboutDialog.java new file mode 100644 index 0000000..bc6a59f --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/AboutDialog.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.HeadlessException; +import java.awt.Toolkit; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.UIManager; + +import mobac.gui.MainGUI; +import mobac.program.ProgramInfo; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +public class AboutDialog extends JDialog implements MouseListener { + + public AboutDialog() throws HeadlessException { + super(MainGUI.getMainGUI(), I18nUtils.localizedStringForKey("dlg_about_title")); + setIconImages(MainGUI.MOBAC_ICONS); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setResizable(false); + + JPanel panel = new JPanel(null); + panel.setBackground(Color.WHITE); + GBC std = GBC.std(); + GBC eol = GBC.eol(); + std.insets(3, 3, 3, 3); + eol.insets(3, 3, 3, 3); + ImageIcon splash = Utilities.loadResourceImageIcon("Splash.jpg"); + Dimension size = new Dimension(splash.getIconWidth(), splash.getIconHeight()); + panel.setPreferredSize(size); + panel.setMinimumSize(size); + panel.setMaximumSize(size); + panel.setSize(size); + + JLabel splashLabel = new JLabel(splash); + JPanel infoPanel = new JPanel(new GridBagLayout()); + infoPanel.setBackground(Color.WHITE); + infoPanel.setOpaque(false); + infoPanel.add(new JLabel(I18nUtils.localizedStringForKey("dlg_about_version")), std); + infoPanel.add(new JLabel(ProgramInfo.getVersion()), eol); + infoPanel.add(new JLabel(I18nUtils.localizedStringForKey("dlg_about_program_version")), std); + infoPanel.add(new JLabel(ProgramInfo.getRevisionStr()), eol); + + panel.add(infoPanel); + panel.add(splashLabel); + + infoPanel.setBounds(200, 155, 320, 200); + splashLabel.setBounds(0, 0, splash.getIconWidth(), splash.getIconHeight()); + + add(panel); + pack(); + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation((dim.width - getWidth()) / 2, (dim.height - getHeight()) / 2); + + addMouseListener(this); + } + + public void mouseClicked(MouseEvent e) { + setVisible(false); + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public static void main(String[] args) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + ProgramInfo.initialize(); // Load revision info + JDialog dlg = new AboutDialog(); + dlg.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/mobac/gui/dialogs/FontChooser.java b/src/main/java/mobac/gui/dialogs/FontChooser.java new file mode 100644 index 0000000..0eaf865 --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/FontChooser.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.Dialog.ModalityType; +import java.awt.Font; +import java.awt.GraphicsEnvironment; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import mobac.utilities.GBCTable; +import mobac.utilities.I18nUtils; + +public class FontChooser { + + private static final String FONT_NAMES[] = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getAvailableFontFamilyNames(); + private static final String STYLES[] = new String[] { "Plain", "Bold", "Italic", "Bold + Italic" }; + + public static final Font DEFAULT = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + + public static String encodeFont(Font font) { + String style; + switch (font.getStyle()) { + case Font.PLAIN: + style = "PLAIN"; + break; + case Font.BOLD: + style = "BOLD"; + break; + case Font.ITALIC: + style = "ITALIC"; + break; + case Font.BOLD | Font.ITALIC: + style = "BOLDITALIC"; + break; + default: + style = "PLAIN"; + } + return font.getName() + "-" + style + "-" + font.getSize(); + } + + private static JScrollPane scroll(JList jList, String title) { + JLabel jLabel = new JLabel(title); + jLabel.setHorizontalAlignment(JLabel.CENTER); + JScrollPane jScrollPane = new JScrollPane(jList); + jScrollPane.setColumnHeaderView(jLabel); + return jScrollPane; + } + + private final JDialog jDialog = new JDialog(); + + private final JLabel jLabelPreview = new JLabel("DUMMY"); + + private final JList jListName = createJList(FONT_NAMES); + private final JList jListStyle = createJList(STYLES); + private final JList jListSize = createJList(new Integer[] { 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24 }); + + private final JButton jButtonOK = new JButton(I18nUtils.localizedStringForKey("OK")), jButtonCancel = new JButton( + I18nUtils.localizedStringForKey("Cancel")); + + private boolean wasCanceled; + + public FontChooser() { + jDialog.setTitle(I18nUtils.localizedStringForKey("dlg_font_choose_title")); + jDialog.setModalityType(ModalityType.APPLICATION_MODAL); + + jLabelPreview.setHorizontalAlignment(JLabel.CENTER); + jLabelPreview.setVerticalAlignment(JLabel.CENTER); + jLabelPreview.setBorder(BorderFactory.createTitledBorder(I18nUtils + .localizedStringForKey("dlg_font_choose_preview"))); + + jButtonOK.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + wasCanceled = false; + jDialog.setVisible(false); + } + }); + + jButtonCancel.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + jDialog.setVisible(false); + } + }); + + JPanel buttonPane = new JPanel(); + buttonPane.add(jButtonOK); + buttonPane.add(jButtonCancel); + + JPanel jPanel = new JPanel(new GridBagLayout()); + jPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + GBCTable gbc = new GBCTable(); + jPanel.add(scroll(jListName, I18nUtils.localizedStringForKey("dlg_font_choose_name")), gbc.begin().fill()); + jPanel.add(scroll(jListStyle, I18nUtils.localizedStringForKey("dlg_font_choose_style")), gbc.incX().fill()); + jPanel.add(scroll(jListSize, I18nUtils.localizedStringForKey("dlg_font_choose_size")), gbc.incX().fill()); + jPanel.add(jLabelPreview, gbc.begin(1, 2).fillH().gridwidth(3)); + jPanel.add(buttonPane, gbc.incY().fillH().gridwidth(3)); + + jDialog.setContentPane(jPanel); + jDialog.setSize(384, 384); + jDialog.setMinimumSize(jDialog.getSize()); + setFont(DEFAULT); + } + + private JList createJList(E[] objects) { + JList jList = new JList(objects); + jList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + jList.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + if (!e.getValueIsAdjusting()) { + Font font = getFont(); + jLabelPreview.setFont(font); + jLabelPreview.setText(encodeFont(font)); + } + } + }); + return jList; + } + + public void setFont(Font font) { + if (font == null) { + font = DEFAULT; + } + jListName.setSelectedValue(font.getName(), true); + jListStyle.setSelectedIndex(font.getStyle()); + jListSize.setSelectedValue(font.getSize(), true); + } + + public Font getFont() { + String name = (String) jListName.getSelectedValue(); + if (name == null) { + name = DEFAULT.getName(); + } + int style = jListStyle.getSelectedIndex(); + if (style == -1) { + style = DEFAULT.getStyle(); + } + Integer size = (Integer) jListSize.getSelectedValue(); + if (size == null) { + size = DEFAULT.getSize(); + } + return new Font(name, style, size); + } + + public void show() { + wasCanceled = true; + jDialog.setLocationRelativeTo(jDialog.getParent()); + jDialog.setVisible(true); + } + + public boolean wasCanceled() { + return wasCanceled; + } +} diff --git a/src/main/java/mobac/gui/dialogs/Help.java b/src/main/java/mobac/gui/dialogs/Help.java new file mode 100644 index 0000000..b8e8edb --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/Help.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.HeadlessException; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.io.DataInputStream; +import java.io.IOException; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; + +import mobac.gui.MainGUI; +import mobac.utilities.Charsets; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; + +public class Help extends JFrame implements WindowListener { + + private static Help INSTANCE = null; + + public static synchronized void showHelp() { + if (INSTANCE == null) + INSTANCE = new Help(); + INSTANCE.setVisible(true); + } + + public Help() throws HeadlessException { + super(I18nUtils.localizedStringForKey("dlg_help_title")); + setIconImages(MainGUI.MOBAC_ICONS); + setLayout(new GridBagLayout()); + JLabel text = new JLabel(); + JButton closeButton = new JButton(I18nUtils.localizedStringForKey("Close")); + closeButton.setDefaultCapable(true); + closeButton.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + setVisible(false); + dispose(); + } + }); + + DataInputStream in = new DataInputStream( + I18nUtils.getI18nResourceAsStream("resources/text/help_dialog", "html")); + byte[] buf; + try { + buf = new byte[in.available()]; + in.readFully(buf); + in.close(); + String helpMessage = new String(buf, Charsets.UTF_8); + // Strip out all line breaks because JOptionPane shows + // the raw HTML code otherwise + // helpMessage = helpMessage.replaceAll("\n", ""); + // text.setFont(mobac.gui.MainGUI.defaultFont()); + text.setText(helpMessage); + } catch (IOException e) { + } + add(text, GBC.eol().insets(10, 10, 10, 10)); + add(closeButton, GBC.eol().anchor(GBC.CENTER).insets(0, 0, 0, 10)); + pack(); + + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation((dim.width - getWidth()) / 2, (dim.height - getHeight()) / 2); + + setAlwaysOnTop(true); + setResizable(false); + } + + public void windowActivated(WindowEvent e) { + } + + public void windowClosed(WindowEvent e) { + INSTANCE = null; + } + + public void windowClosing(WindowEvent e) { + } + + public void windowDeactivated(WindowEvent e) { + } + + public void windowDeiconified(WindowEvent e) { + } + + public void windowIconified(WindowEvent e) { + } + + public void windowOpened(WindowEvent e) { + } +} diff --git a/src/main/java/mobac/gui/dialogs/LicensesDialog.java b/src/main/java/mobac/gui/dialogs/LicensesDialog.java new file mode 100644 index 0000000..1b9f750 --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/LicensesDialog.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.UIManager; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import mobac.gui.MainGUI; +import mobac.program.ProgramInfo; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +public class LicensesDialog extends JFrame implements ChangeListener, ActionListener { + + private LicenseInfo[] licenses = new LicenseInfo[] { new LicenseInfo("

Mobile Atlas Creator

", "gpl.txt"), + new LicenseInfo("

Library Apache Log4J

", "apache-2.0.txt"), + new LicenseInfo("

Library Apache Commons Codec

", "apache-2.0.txt"), + new LicenseInfo("

Library Apache Commons IO

", "apache-2.0.txt"), + new LicenseInfo("

Library Berkely-DB JavaEdition

", "license-dbd-je.txt"), + new LicenseInfo("

Library BeanShell

", "lgpl-3.0.txt"), + new LicenseInfo("

Library JavaPNG

", "gpl.txt"), + new LicenseInfo("

Library iTextPDF

", "agpl.txt") }; + + private final JTextArea textArea; + private final JTabbedPane tab; + private String currentLicense = null; + + public LicensesDialog() { + super(I18nUtils.localizedStringForKey("dlg_license_title")); + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setLayout(new GridBagLayout()); + setIconImages(MainGUI.MOBAC_ICONS); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + JButton ok = new JButton("OK"); + textArea = new JTextArea(); + textArea.setEditable(false); + textArea.setBackground(this.getBackground()); + JScrollPane textScroller = new JScrollPane(textArea); + textScroller.setPreferredSize(new Dimension(700, (int) (dim.height * 0.8))); + + tab = new JTabbedPane(JTabbedPane.LEFT, JTabbedPane.WRAP_TAB_LAYOUT); + Icon icon = new ImageIcon(new BufferedImage(1, 50, BufferedImage.TYPE_INT_ARGB)); + + boolean first = true; + for (LicenseInfo li : licenses) { + tab.addTab("" + li.name + "", icon, (first) ? textScroller : null); + first = false; + } + tab.addChangeListener(this); + stateChanged(null); + add(tab, GBC.eol().anchor(GBC.NORTH).fill()); + + // add(textScroller, GBC.eol()); + add(ok, GBC.eol().anchor(GBC.CENTER).insets(5, 10, 10, 10)); + ok.addActionListener(this); + pack(); + + setLocation((dim.width - getWidth()) / 2, (dim.height - getHeight()) / 2); + } + + public void stateChanged(ChangeEvent event) { + String license; + try { + String nextLicense = licenses[tab.getSelectedIndex()].licenseResource; + if (nextLicense.equals(currentLicense)) + return; + license = Utilities.loadTextResource("text/" + nextLicense); + currentLicense = nextLicense; + } catch (IOException e) { + license = "Failed to load license: " + e.getMessage(); + } + textArea.setText(license); + textArea.setCaretPosition(0); + + } + + public void actionPerformed(ActionEvent e) { + dispose(); + } + + private static class LicenseInfo { + public final String name; + public final String licenseResource; + + public LicenseInfo(String name, String licenseResource) { + super(); + this.name = name; + this.licenseResource = licenseResource; + } + + } + + public static void main(String[] args) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + ProgramInfo.initialize(); // Load revision info + JFrame dlg = new LicensesDialog(); + dlg.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/mobac/gui/dialogs/ManageBookmarks.java b/src/main/java/mobac/gui/dialogs/ManageBookmarks.java new file mode 100644 index 0000000..f1bd36c --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/ManageBookmarks.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.HeadlessException; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JList; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.xml.bind.JAXBException; + +import mobac.gui.MainGUI; +import mobac.program.DirectoryManager; +import mobac.program.Logging; +import mobac.program.model.Bookmark; +import mobac.program.model.Settings; +import mobac.utilities.GBC; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; + +public class ManageBookmarks extends JDialog implements ListSelectionListener, ActionListener { + + private JButton deleteButton; + + private JButton applyButton; + + private JList bookmarks; + + private DefaultListModel bookmarksModel; + + public ManageBookmarks(Window owner) throws HeadlessException { + super(owner, I18nUtils.localizedStringForKey("dlg_mgn_bookmark_title")); + setIconImages(MainGUI.MOBAC_ICONS); + setLayout(new GridBagLayout()); + applyButton = new JButton(I18nUtils.localizedStringForKey("Close")); + applyButton.addActionListener(this); + applyButton.setDefaultCapable(true); + + deleteButton = new JButton(I18nUtils.localizedStringForKey("dlg_mgn_bookmark_delete")); + deleteButton.addActionListener(this); + + bookmarksModel = new DefaultListModel(); + for (Bookmark b : Settings.getInstance().placeBookmarks) + bookmarksModel.addElement(b); + bookmarks = new JList(bookmarksModel); + bookmarks.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); + bookmarks.addListSelectionListener(this); + bookmarks.setVisibleRowCount(10); + bookmarks.setPreferredSize(new Dimension(250, 300)); + + add(bookmarks, GBC.eol().insets(10, 10, 10, 10).fill()); + add(deleteButton, GBC.eol().anchor(GBC.CENTER).insets(0, 0, 0, 10)); + add(applyButton, GBC.eol().anchor(GBC.CENTER).insets(0, 0, 0, 10)); + pack(); + + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation((dim.width - getWidth()) / 2, (dim.height - getHeight()) / 2); + + valueChanged(null); + } + + public void actionPerformed(ActionEvent e) { + if (deleteButton.equals(e.getSource())) + deleteSelectedEntries(); + else if (applyButton.equals(e.getSource())) + apply(); + } + + protected void deleteSelectedEntries() { + int[] selected = bookmarks.getSelectedIndices(); + for (int i = selected.length - 1; i >= 0; i--) + bookmarksModel.remove(selected[i]); + } + + protected void apply() { + ArrayList bookmarksList = new ArrayList(bookmarksModel.getSize()); + for (int i = 0; i < bookmarksModel.getSize(); i++) + bookmarksList.add((Bookmark) bookmarksModel.get(i)); + Settings.getInstance().placeBookmarks = bookmarksList; + setVisible(false); + dispose(); + } + + public void valueChanged(ListSelectionEvent e) { + deleteButton.setEnabled(bookmarks.getSelectedIndices().length > 0); + } + +} diff --git a/src/main/java/mobac/gui/dialogs/MessageDialogs.java b/src/main/java/mobac/gui/dialogs/MessageDialogs.java new file mode 100644 index 0000000..ab4dafb --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/MessageDialogs.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.Component; +import java.awt.Dimension; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; + +import mobac.utilities.I18nUtils; + +public class MessageDialogs { + + public static void showErrorMessage(Component parentComponent, String message, String title) { + JLabel label = new JLabel("" + message + ""); + int maxWidth = 400; + Dimension size = label.getPreferredSize(); + if (size.width > maxWidth) { + // Estimate the number of lines + int lineCount = (int) Math.ceil(((double) size.width) / maxWidth); + lineCount += 1; // Add one extra line as reserve + size.width = maxWidth; // Limit the maximum width + // Increase the size so that the + size.height *= lineCount; + label.setPreferredSize(size); + } + JOptionPane.showMessageDialog(null, label, I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + } + +} diff --git a/src/main/java/mobac/gui/dialogs/WorkinprogressDialog.java b/src/main/java/mobac/gui/dialogs/WorkinprogressDialog.java new file mode 100644 index 0000000..dc2d11e --- /dev/null +++ b/src/main/java/mobac/gui/dialogs/WorkinprogressDialog.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.dialogs; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; + +import mobac.program.tilestore.berkeleydb.DelayedInterruptThread; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +public class WorkinprogressDialog extends JDialog implements WindowListener { + + private static final Logger log = Logger.getLogger(WorkinprogressDialog.class); + + private final ThreadFactory threadFactory; + private Thread workerThread; + + public WorkinprogressDialog(Frame owner, String title) { + this(owner, title, Executors.defaultThreadFactory()); + } + + public WorkinprogressDialog(Frame owner, String title, ThreadFactory threadFactory) { + super(owner, title, true); + this.threadFactory = threadFactory; + setLayout(new FlowLayout()); + add(new JLabel(new ImageIcon(Utilities.getResourceImageUrl("ajax-loader.gif")))); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setLocationRelativeTo(owner); + addWindowListener(this); + JButton abort = new JButton(I18nUtils.localizedStringForKey("dlg_progress_about_btn")); + abort.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + log.debug("User interrupted process"); + WorkinprogressDialog.this.close(); + } + }); + add(abort); + pack(); + } + + public void startWork(final Runnable r) { + workerThread = threadFactory.newThread(new Runnable() { + + public void run() { + try { + r.run(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + WorkinprogressDialog.this.close(); + log.debug("Worker thread finished"); + } + } + + }); + Thread t1 = new Thread() { + @Override + public void run() { + setVisible(true); + } + }; + t1.start(); + } + + protected synchronized void abortWorking() { + try { + if (workerThread != null && !workerThread.isInterrupted()) { + log.debug("User aborted process - interrupting worker thread"); + workerThread.interrupt(); + workerThread = null; + } + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + public void close() { + abortWorking(); + setVisible(false); + } + + public void windowActivated(WindowEvent event) { + } + + public void windowOpened(WindowEvent event) { + workerThread.start(); + } + + public void windowClosed(WindowEvent event) { + abortWorking(); + } + + public void windowClosing(WindowEvent event) { + } + + public void windowDeactivated(WindowEvent event) { + } + + public void windowDeiconified(WindowEvent event) { + } + + public void windowIconified(WindowEvent event) { + } + + public static void main(String[] args) { + JFrame parentFrame = new JFrame(); + parentFrame.setSize(500, 150); + final JLabel jl = new JLabel(); + jl.setText(I18nUtils.localizedStringForKey("dlg_progress_count")); + + parentFrame.add(BorderLayout.CENTER, jl); + parentFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + parentFrame.setVisible(true); + + final WorkinprogressDialog dlg = new WorkinprogressDialog(parentFrame, I18nUtils.localizedStringForKey("dlg_progress_title"), + DelayedInterruptThread.createThreadFactory()); + + final Thread t = new Thread() { + + @Override + public void run() { + try { + for (int i = 0; i <= 500; i++) { + jl.setText(String.format(I18nUtils.localizedStringForKey("dlg_progress_count_i"),i)); + if (Thread.currentThread().isInterrupted()) { + System.out.println("Aborted"); + return; + } + Thread.sleep(25); + } + } catch (InterruptedException e) { + System.out.println("Aborted"); + return; + } finally { + dlg.setVisible(false); + } + } + + }; + dlg.startWork(t); + } +} diff --git a/src/main/java/mobac/gui/gpxtree/GpxEntry.java b/src/main/java/mobac/gui/gpxtree/GpxEntry.java new file mode 100644 index 0000000..9ddb77b --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/GpxEntry.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import javax.swing.tree.DefaultMutableTreeNode; + +import mobac.gui.mapview.layer.GpxLayer; + +/** + * Generalized entry in the gpx tree. All actual entries derive from this class. The class encapsulates everything + * gui-related as well as the actual gpx data for the editor. Subclasses: {@link GpxRootEntry}, {@link TrkEntry}, + * {@link RteEntry}, {@link WptEntry} + * + * @author lhoeppner + * + */ +public class GpxEntry { + private DefaultMutableTreeNode node; + private GpxLayer layer; + /** determines whether an entry can be a parent for waypoints */ + private boolean isWaypointParent = false; + + public void setLayer(GpxLayer layer) { + this.layer = layer; + } + + public GpxLayer getLayer() { + return layer; + } + + /** + * Remembers the associated tree node. + * + * @param node + */ + public void setNode(DefaultMutableTreeNode node) { + this.node = node; + } + + public DefaultMutableTreeNode getNode() { + return node; + } + + public void setWaypointParent(boolean isWaypointParent) { + this.isWaypointParent = isWaypointParent; + } + + public boolean isWaypointParent() { + return isWaypointParent; + } +} diff --git a/src/main/java/mobac/gui/gpxtree/GpxRootEntry.java b/src/main/java/mobac/gui/gpxtree/GpxRootEntry.java new file mode 100644 index 0000000..269c081 --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/GpxRootEntry.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import mobac.data.gpx.gpx11.Gpx; +import mobac.data.gpx.gpx11.MetadataType; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.utilities.I18nUtils; + +public class GpxRootEntry extends GpxEntry { + + public GpxRootEntry(GpxLayer layer) { + this.setLayer(layer); + this.setWaypointParent(true); + } + + public String toString() { + String name = getMetaDataName(); + if (name != null && !name.equals("")) { + return name; + } else { + if (getLayer().getFile() == null) { + return I18nUtils.localizedStringForKey("rp_gpx_root_default_name_nofile"); + } else { + return String.format(I18nUtils.localizedStringForKey("rp_gpx_root_default_name_hasfile"), getLayer() + .getFile().getName()); + } + } + } + + public String getMetaDataName() { + try { + return getLayer().getGpx().getMetadata().getName(); + } catch (NullPointerException e) { + return null; + } + } + + public void setMetaDataName(String name) { + Gpx gpx = getLayer().getGpx(); + if (gpx.getMetadata() == null) + gpx.setMetadata(new MetadataType()); + gpx.getMetadata().setName(name); + + // Notify the model about the changed node text + getLayer().getPanel().getTreeModel().nodeChanged(getNode()); + } +} diff --git a/src/main/java/mobac/gui/gpxtree/GpxTreeListener.java b/src/main/java/mobac/gui/gpxtree/GpxTreeListener.java new file mode 100644 index 0000000..3900c31 --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/GpxTreeListener.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +import mobac.gui.actions.GpxElementListener; +import mobac.utilities.I18nUtils; + +/** + * Listener for the gpx editor tree. + * + * @author lhoeppner + * + */ +public class GpxTreeListener implements MouseListener { + private JPopupMenu popup; + + public void actionPerformed(ActionEvent e) { + } + + public void mouseClicked(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } + } + + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + showPopup(e); + } + } + + /** + * Popup for all elements in the gpx tree. TODO separate for waypoints, files, tracks and routes + * + * @param e + */ + private void showPopup(MouseEvent e) { + JTree tree = (JTree) e.getSource(); + TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); + tree.setSelectionPath(selPath); + if (selPath == null) + return; + + DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent(); + + GpxEntry gpxEntry = null; + try { + gpxEntry = (GpxEntry) node.getUserObject(); + gpxEntry.setNode(node); + } catch (ClassCastException exc) { + } + + popup = new JPopupMenu(); + JMenuItem delete = new JMenuItem(I18nUtils.localizedStringForKey("rp_gpx_pop_menu_delete_element")); + delete.setName(GpxElementListener.MENU_NAME_DELETE); + GpxElementListener listener = new GpxElementListener(gpxEntry); + delete.addMouseListener(listener); + popup.add(delete); + JMenuItem rename = new JMenuItem(I18nUtils.localizedStringForKey("rp_gpx_pop_menu_rename_element")); + rename.setName(GpxElementListener.MENU_NAME_RENAME); + rename.addMouseListener(listener); + popup.add(rename); + + popup.show((Component) e.getSource(), e.getX(), e.getY()); + } +} diff --git a/src/main/java/mobac/gui/gpxtree/RteEntry.java b/src/main/java/mobac/gui/gpxtree/RteEntry.java new file mode 100644 index 0000000..091b1b0 --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/RteEntry.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import mobac.data.gpx.gpx11.RteType; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.utilities.I18nUtils; + +public class RteEntry extends GpxEntry { + private RteType rte; + + public RteEntry(RteType rte, GpxLayer layer) { + this.setRte(rte); + this.setLayer(layer); + this.setWaypointParent(true); + } + + public String toString() { + String name = ""; + try { + name = getRte().getName(); + } catch (NullPointerException e) { + // no name set + } + if (name != null && !name.equals("")) { + return name; + } else { + return I18nUtils.localizedStringForKey("rp_gpx_unname_route_name"); + } + } + + private void setRte(RteType rte) { + this.rte = rte; + } + + public RteType getRte() { + return rte; + } +} diff --git a/src/main/java/mobac/gui/gpxtree/TrkEntry.java b/src/main/java/mobac/gui/gpxtree/TrkEntry.java new file mode 100644 index 0000000..a9ee994 --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/TrkEntry.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import mobac.data.gpx.gpx11.TrkType; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.utilities.I18nUtils; + +public class TrkEntry extends GpxEntry { + private TrkType trk; + + public TrkEntry(TrkType trk, GpxLayer layer) { + this.trk = trk; + this.setLayer(layer); + this.setWaypointParent(false); + } + + public String toString() { + String name = ""; + try { + name = trk.getName(); + } catch (NullPointerException e) { + // no name set + } + if (name != null && !name.equals("")) { + return name; + } else { + return I18nUtils.localizedStringForKey("rp_gpx_unname_track_name"); + } + } + + public TrkType getTrk() { + return trk; + } +} diff --git a/src/main/java/mobac/gui/gpxtree/TrksegEntry.java b/src/main/java/mobac/gui/gpxtree/TrksegEntry.java new file mode 100644 index 0000000..78441ca --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/TrksegEntry.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import mobac.data.gpx.gpx11.TrksegType; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.utilities.I18nUtils; + +public class TrksegEntry extends GpxEntry { + private TrksegType trkseg; + private String name; + + public TrksegEntry(TrksegType trkseg, int segnum, GpxLayer layer) { + this.trkseg = trkseg; + this.name = String.format(I18nUtils.localizedStringForKey("rp_gpx_node_seg_name"), Integer.toString(segnum)); + this.setLayer(layer); + this.setWaypointParent(true); + } + + public String toString() { + return name; + } + + public TrksegType getTrkSeg() { + return trkseg; + } +} diff --git a/src/main/java/mobac/gui/gpxtree/WptEntry.java b/src/main/java/mobac/gui/gpxtree/WptEntry.java new file mode 100644 index 0000000..42c0e27 --- /dev/null +++ b/src/main/java/mobac/gui/gpxtree/WptEntry.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.gpxtree; + +import mobac.data.gpx.gpx11.WptType; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.utilities.I18nUtils; + +public class WptEntry extends GpxEntry { + private WptType wpt; + + public WptEntry(WptType wpt, GpxLayer layer) { + this.wpt = wpt; + this.setLayer(layer); + } + + public String toString() { + String name = ""; + try { + name = wpt.getName(); + } catch (NullPointerException e) { + // no name set + } + if (name != null && !name.equals("")) { + return name; + } else { + return I18nUtils.localizedStringForKey("rp_gpx_unname_wpt_name"); + } + } + + public WptType getWpt() { + return wpt; + } +} diff --git a/src/main/java/mobac/gui/listeners/AtlasModelListener.java b/src/main/java/mobac/gui/listeners/AtlasModelListener.java new file mode 100644 index 0000000..1b3bcb4 --- /dev/null +++ b/src/main/java/mobac/gui/listeners/AtlasModelListener.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * + */ +package mobac.gui.listeners; + +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; + +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.panels.JProfilesPanel; + +public class AtlasModelListener implements TreeModelListener { + + JAtlasTree atlasTree; + JProfilesPanel profilesPanel; + + public AtlasModelListener(JAtlasTree atlasTree, JProfilesPanel profilesPanel) { + super(); + this.atlasTree = atlasTree; + this.profilesPanel = profilesPanel; + } + + protected void changed() { + profilesPanel.getSaveAsButton().setEnabled(atlasTree.getAtlas().getLayerCount() > 0); + } + + public void treeNodesChanged(TreeModelEvent e) { + changed(); + } + + public void treeNodesInserted(TreeModelEvent e) { + changed(); + } + + public void treeNodesRemoved(TreeModelEvent e) { + changed(); + } + + public void treeStructureChanged(TreeModelEvent e) { + changed(); + } +} \ No newline at end of file diff --git a/src/main/java/mobac/gui/mapview/GridZoom.java b/src/main/java/mobac/gui/mapview/GridZoom.java new file mode 100644 index 0000000..430fadd --- /dev/null +++ b/src/main/java/mobac/gui/mapview/GridZoom.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +import mobac.utilities.I18nUtils; + +public class GridZoom { + + private int zoom; + + public int getZoom() { + return zoom; + } + + public GridZoom(int zoom) { + this.zoom = zoom; + } + + @Override + public String toString() { + return String.format(I18nUtils.localizedStringForKey("map_ctrl_zoom_grid_prefix_fmt"), zoom); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GridZoom)) + return false; + return ((GridZoom) obj).zoom == zoom; + } + + @Override + public int hashCode() { + assert false : "hashCode not designed"; + return -1; + } + +} diff --git a/src/main/java/mobac/gui/mapview/JMapViewer.java b/src/main/java/mobac/gui/mapview/JMapViewer.java new file mode 100644 index 0000000..d18680f --- /dev/null +++ b/src/main/java/mobac/gui/mapview/JMapViewer.java @@ -0,0 +1,559 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.Point2D; +import java.util.ConcurrentModificationException; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.gui.mapview.interfaces.MapTileLayer; +import mobac.gui.mapview.interfaces.TileLoaderListener; +import mobac.gui.mapview.layer.DefaultMapTileLayer; +import mobac.gui.mapview.layer.MapGridLayer; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +/** + * + * Provides a simple panel that displays pre-rendered map tiles loaded from the OpenStreetMap project. + * + * @author Jan Peter Stotz + * + */ +public class JMapViewer extends JPanel implements TileLoaderListener { + + private static final long serialVersionUID = 1L; + + private static Logger log = Logger.getLogger(JMapViewer.class); + + /** + * Vectors for clock-wise tile painting + */ + protected static final Point[] move = { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) }; + + public static final int MAX_ZOOM = 22; + public static final int MIN_ZOOM = 0; + + protected TileLoader tileLoader; + protected MemoryTileCache tileCache; + protected MapSource mapSource; + protected boolean usePlaceHolderTiles = true; + + protected boolean mapMarkersVisible; + protected MapGridLayer mapGridLayer = null; + + protected List mapTileLayers; + public List mapLayers; + + /** + * x- and y-position of the center of this map-panel on the world map denoted in screen pixel regarding the current + * zoom level. + */ + protected Point center = new Point(); + + /** + * Current zoom level + */ + protected int zoom; + + protected JSlider zoomSlider = new JSlider(0, 0); + protected JButton zoomInButton; + protected JButton zoomOutButton; + + protected JobDispatcher jobDispatcher; + + public JMapViewer(MapSource defaultMapSource, int downloadThreadCount) { + super(); + mapTileLayers = new LinkedList(); + mapLayers = new LinkedList(); + tileLoader = new TileLoader(this); + tileCache = new MemoryTileCache(); + jobDispatcher = JobDispatcher.getInstance(); + mapMarkersVisible = true; + setLayout(null); + setMapSource(defaultMapSource); + initializeZoomSlider(); + setMinimumSize(new Dimension(256, 256)); + setPreferredSize(new Dimension(400, 400)); + setDisplayPositionByLatLon(50.0, 9.0, 1); + } + + protected void initializeZoomSlider() { + zoomSlider.setOrientation(JSlider.VERTICAL); + zoomSlider.setBounds(10, 10, 30, 150); + zoomSlider.setOpaque(false); + zoomSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + setZoom(zoomSlider.getValue()); + } + }); + add(zoomSlider); + int size = 18; + try { + ImageIcon icon = Utilities.loadResourceImageIcon("plus.png"); + zoomInButton = new JButton(icon); + } catch (Exception e) { + zoomInButton = new JButton("+"); + zoomInButton.setFont(new Font("sansserif", Font.BOLD, 9)); + zoomInButton.setMargin(new Insets(0, 0, 0, 0)); + } + zoomInButton.setBounds(4, 155, size, size); + zoomInButton.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + zoomIn(); + } + }); + add(zoomInButton); + try { + ImageIcon icon = Utilities.loadResourceImageIcon("minus.png"); + zoomOutButton = new JButton(icon); + } catch (Exception e) { + zoomOutButton = new JButton("-"); + zoomOutButton.setFont(new Font("sansserif", Font.BOLD, 9)); + zoomOutButton.setMargin(new Insets(0, 0, 0, 0)); + } + zoomOutButton.setBounds(8 + size, 155, size, size); + zoomOutButton.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + zoomOut(); + } + }); + add(zoomOutButton); + } + + /** + * Changes the map pane so that it is centered on the specified coordinate at the given zoom level. + * + * @param lat + * latitude of the specified coordinate + * @param lon + * longitude of the specified coordinate + * @param zoom + * {@link #MIN_ZOOM} <= zoom level <= {@link #MAX_ZOOM} + */ + public void setDisplayPositionByLatLon(double lat, double lon, int zoom) { + setDisplayPositionByLatLon(new Point(getWidth() / 2, getHeight() / 2), lat, lon, zoom); + } + + /** + * Changes the map pane so that the specified coordinate at the given zoom level is displayed on the map at the + * screen coordinate mapPoint. + * + * @param mapPoint + * point on the map denoted in pixels where the coordinate should be set + * @param lat + * latitude of the specified coordinate + * @param lon + * longitude of the specified coordinate + * @param zoom + * {@link #MIN_ZOOM} <= zoom level <= {@link MapSource#getMaxZoom()} + */ + public void setDisplayPositionByLatLon(Point mapPoint, double lat, double lon, int zoom) { + zoom = Math.max(Math.min(zoom, mapSource.getMaxZoom()), mapSource.getMinZoom()); + MapSpace mapSpace = mapSource.getMapSpace(); + int x = mapSpace.cLonToX(lon, zoom); + int y = mapSpace.cLatToY(lat, zoom); + setDisplayPosition(mapPoint, x, y, zoom); + } + + public void setDisplayPosition(int x, int y, int zoom) { + setDisplayPosition(new Point(getWidth() / 2, getHeight() / 2), x, y, zoom); + } + + public void setDisplayPosition(Point mapPoint, int x, int y, int zoom) { + if (zoom > mapSource.getMaxZoom() || zoom < MIN_ZOOM) + return; + + // Get the plain tile number + Point p = new Point(); + p.x = x - mapPoint.x + getWidth() / 2; + p.y = y - mapPoint.y + getHeight() / 2; + center = p; + setIgnoreRepaint(true); + try { + int oldZoom = this.zoom; + this.zoom = zoom; + if (oldZoom != zoom) + zoomChanged(oldZoom); + if (zoomSlider.getValue() != zoom) + zoomSlider.setValue(zoom); + } finally { + setIgnoreRepaint(false); + repaint(); + } + } + + /** + * Sets the displayed map pane and zoom level so that the two points (x1/y1) and (x2/y2) visible. Please note that + * the coordinates have to be specified regarding {@link #MAX_ZOOM}. + * + * @param x1 + * @param y1 + * @param x2 + * @param y2 + */ + public void setDisplayToFitPixelCoordinates(int x1, int y1, int x2, int y2) { + int mapZoomMax = mapSource.getMaxZoom(); + int height = Math.max(0, getHeight()); + int width = Math.max(0, getWidth()); + int newZoom = MAX_ZOOM; + int x = Math.abs(x1 - x2); + int y = Math.abs(y1 - y2); + while (x > width || y > height || newZoom > mapZoomMax) { + newZoom--; + x >>= 1; + y >>= 1; + } + + // Do not select a zoom level that is unsupported by the current map + // source + newZoom = Math.max(mapSource.getMinZoom(), Math.min(mapSource.getMaxZoom(), newZoom)); + + x = Math.min(x2, x1) + Math.abs(x1 - x2) / 2; + y = Math.min(y2, y1) + Math.abs(y1 - y2) / 2; + int z = 1 << (MAX_ZOOM - newZoom); + x /= z; + y /= z; + setDisplayPosition(x, y, newZoom); + } + + public Point2D.Double getPosition() { + MapSpace mapSpace = mapSource.getMapSpace(); + double lon = mapSpace.cXToLon(center.x, zoom); + double lat = mapSpace.cYToLat(center.y, zoom); + return new Point2D.Double(lat, lon); + } + + public Point2D.Double getPosition(Point mapPoint) { + MapSpace mapSpace = mapSource.getMapSpace(); + int x = center.x + mapPoint.x - getWidth() / 2; + int y = center.y + mapPoint.y - getHeight() / 2; + double lon = mapSpace.cXToLon(x, zoom); + double lat = mapSpace.cYToLat(y, zoom); + return new Point2D.Double(lat, lon); + } + + /** + * Calculates the position on the map of a given coordinate + * + * @param lat + * @param lon + * @return point on the map or null if the point is not visible + */ + public Point getMapPosition(double lat, double lon) { + MapSpace mapSpace = mapSource.getMapSpace(); + int x = mapSpace.cLonToX(lon, zoom); + int y = mapSpace.cLatToY(lat, zoom); + x -= center.x - getWidth() / 2; + y -= center.y - getHeight() / 2; + if (x < 0 || y < 0 || x > getWidth() || y > getHeight()) + return null; + return new Point(x, y); + } + + @Override + protected void paintComponent(Graphics graphics) { + Graphics2D g = (Graphics2D) graphics; + // if (mapIsMoving) { + // mapIsMoving = false; + // Doesn't look very pretty but is much more faster + // g.copyArea(0, 0, getWidth(), getHeight(), -mapMoveX, -mapMoveY); + // return; + // } + super.paintComponent(g); + + int iMove = 0; + + int tileSize = mapSource.getMapSpace().getTileSize(); + + int tilex = center.x / tileSize; + int tiley = center.y / tileSize; + int off_x = (center.x % tileSize); + int off_y = (center.y % tileSize); + + int w2 = getWidth() / 2; + int h2 = getHeight() / 2; + int topLeftX = center.x - w2; + int topLeftY = center.y - h2; + + int posx = w2 - off_x; + int posy = h2 - off_y; + + int diff_left = off_x; + int diff_right = tileSize - off_x; + int diff_top = off_y; + int diff_bottom = tileSize - off_y; + + boolean start_left = diff_left < diff_right; + boolean start_top = diff_top < diff_bottom; + + if (start_top) { + if (start_left) + iMove = 2; + else + iMove = 3; + } else { + if (start_left) + iMove = 1; + else + iMove = 0; + } // calculate the visibility borders + int x_min = -tileSize; + int y_min = -tileSize; + int x_max = getWidth(); + int y_max = getHeight(); + + // paint the tiles in a spiral, starting from center of the map + boolean painted = (mapTileLayers.size() > 0); + for (MapTileLayer l : mapTileLayers) { + l.startPainting(mapSource); + } + int x = 0; + while (painted) { + painted = false; + for (int i = 0; i < 4; i++) { + if (i % 2 == 0) + x++; + for (int j = 0; j < x; j++) { + if (x_min <= posx && posx <= x_max && y_min <= posy && posy <= y_max) { + // tile is visible + painted = true; + for (MapTileLayer l : mapTileLayers) { + l.paintTile(g, posx, posy, tilex, tiley, zoom); + } + } + Point p = move[iMove]; + posx += p.x * tileSize; + posy += p.y * tileSize; + tilex += p.x; + tiley += p.y; + } + iMove = (iMove + 1) % move.length; + } + } + + int bottomRightX = topLeftX + getWidth(); + int bottomRightY = topLeftY + getHeight(); + try { + for (MapLayer l : mapLayers) { + l.paint(this, (Graphics2D) g, zoom, topLeftX, topLeftY, bottomRightX, bottomRightY); + } + } catch (ConcurrentModificationException e) { + // This may happen when multiple GPX files are loaded at once and in the mean time the map view is + // repainted. + SwingUtilities.invokeLater(new Runnable() { + public void run() { + JMapViewer.this.repaint(); + } + }); + } + + // outer border of the map + int mapSize = tileSize << zoom; + g.setColor(Color.BLACK); + g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize); + + // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20); + } + + /** + * Moves the visible map pane. + * + * @param x + * horizontal movement in pixel. + * @param y + * vertical movement in pixel + */ + public void moveMap(int x, int y) { + center.x += x; + center.y += y; + repaint(); + } + + /** + * @return the current zoom level + */ + public int getZoom() { + return zoom; + } + + /** + * Increases the current zoom level by one + */ + public void zoomIn() { + setZoom(zoom + 1); + } + + /** + * Increases the current zoom level by one + */ + public void zoomIn(Point mapPoint) { + setZoom(zoom + 1, mapPoint); + } + + /** + * Decreases the current zoom level by one + */ + public void zoomOut() { + setZoom(zoom - 1); + } + + /** + * Decreases the current zoom level by one + */ + public void zoomOut(Point mapPoint) { + setZoom(zoom - 1, mapPoint); + } + + public void setZoom(int zoom, Point mapPoint) { + if (zoom > mapSource.getMaxZoom() || zoom < mapSource.getMinZoom() || zoom == this.zoom) + return; + Point2D.Double zoomPos = getPosition(mapPoint); + jobDispatcher.cancelOutstandingJobs(); // Clearing outstanding load + // requests + setDisplayPositionByLatLon(mapPoint, zoomPos.x, zoomPos.y, zoom); + } + + public void setZoom(int zoom) { + setZoom(zoom, new Point(getWidth() / 2, getHeight() / 2)); + repaint(); + } + + /** + * Every time the zoom level changes this method is called. Override it in derived implementations for adapting zoom + * dependent values. The new zoom level can be obtained via {@link #getZoom()}. + * + * @param oldZoom + * the previous zoom level + */ + protected void zoomChanged(int oldZoom) { + zoomSlider.setToolTipText("Zoom level " + zoom); + zoomInButton.setToolTipText("Zoom to level " + (zoom + 1)); + zoomOutButton.setToolTipText("Zoom to level " + (zoom - 1)); + zoomOutButton.setEnabled(zoom > mapSource.getMinZoom()); + zoomInButton.setEnabled(zoom < mapSource.getMaxZoom()); + } + + public boolean isTileGridVisible() { + return (mapGridLayer != null); + } + + public void setTileGridVisible(boolean tileGridVisible) { + if (isTileGridVisible() == tileGridVisible) + return; + if (tileGridVisible) { + mapGridLayer = new MapGridLayer(); + addMapTileLayers(mapGridLayer); + } else { + removeMapTileLayers(mapGridLayer); + mapGridLayer = null; + } + repaint(); + } + + public boolean getMapMarkersVisible() { + return mapMarkersVisible; + } + + public void setZoomContolsVisible(boolean visible) { + zoomSlider.setVisible(visible); + zoomInButton.setVisible(visible); + zoomOutButton.setVisible(visible); + } + + public boolean getZoomContolsVisible() { + return zoomSlider.isVisible(); + } + + public MemoryTileCache getTileImageCache() { + return tileCache; + } + + public TileLoader getTileLoader() { + return tileLoader; + } + + public MapSource getMapSource() { + return mapSource; + } + + public void setMapSource(MapSource mapSource) { + if (mapSource.getMaxZoom() > MAX_ZOOM) + throw new RuntimeException("Maximum zoom level too high"); + if (mapSource.getMinZoom() < MIN_ZOOM) + throw new RuntimeException("Minumim zoom level too low"); + this.mapSource = mapSource; + zoomSlider.setMinimum(mapSource.getMinZoom()); + zoomSlider.setMaximum(mapSource.getMaxZoom()); + jobDispatcher.cancelOutstandingJobs(); + if (zoom > mapSource.getMaxZoom()) + setZoom(mapSource.getMaxZoom()); + mapTileLayers.clear(); + log.info("Map layer changed to: " + mapSource); + mapTileLayers.add(new DefaultMapTileLayer(this, mapSource)); + if (mapGridLayer != null) + mapTileLayers.add(mapGridLayer); + repaint(); + } + + public JobDispatcher getJobDispatcher() { + return jobDispatcher; + } + + public boolean isUsePlaceHolderTiles() { + return usePlaceHolderTiles; + } + + public void tileLoadingFinished(Tile tile, boolean success) { + repaint(); + } + + public void addMapTileLayers(MapTileLayer mapTileLayer) { + mapTileLayers.add(mapTileLayer); + } + + public void removeMapTileLayers(MapTileLayer mapTileLayer) { + mapTileLayers.remove(mapTileLayer); + } + +} diff --git a/src/main/java/mobac/gui/mapview/JobDispatcher.java b/src/main/java/mobac/gui/mapview/JobDispatcher.java new file mode 100644 index 0000000..18c3b16 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/JobDispatcher.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import mobac.program.tilestore.berkeleydb.DelayedInterruptThread; + +import org.apache.log4j.Logger; + +public class JobDispatcher implements ThreadFactory, RejectedExecutionHandler { + + private static final Logger log = Logger.getLogger(JobDispatcher.class); + private static final JobDispatcher INSTANCE = new JobDispatcher(); + + private static final int WORKER_THREAD_MAX_COUNT = 5; + + /** + * Specifies the time span in seconds that a worker thread waits for new jobs to perform. If the time span has + * elapsed the worker thread terminates itself. Only the first worker thread works differently, it ignores the + * timeout and will never terminate itself. + */ + private static final int WORKER_THREAD_TIMEOUT = 30; + + /** + * @return the singleton instance of the {@link JobDispatcher} + */ + public static JobDispatcher getInstance() { + return INSTANCE; + } + + private int WORKER_THREAD_ID = 1; + + private final BlockingQueue jobQueue; + + private final ThreadPoolExecutor executor; + + /** + * Removes all jobs from the queue that are currently not being processed. + */ + public void cancelOutstandingJobs() { + jobQueue.clear(); + } + + private JobDispatcher() { + jobQueue = new LinkedBlockingQueue(); + executor = new ThreadPoolExecutor(WORKER_THREAD_MAX_COUNT, WORKER_THREAD_MAX_COUNT, WORKER_THREAD_TIMEOUT, + TimeUnit.SECONDS, jobQueue, this, this); + executor.allowCoreThreadTimeOut(true); + } + + public void addJob(Runnable job) { + executor.execute(job); + } + + public Thread newThread(Runnable r) { + int id; + synchronized (this) { + id = WORKER_THREAD_ID++; + } + log.trace("New map preview worker thread created with id=" + id); + return new DelayedInterruptThread(r, "Map preview thread " + id); + } + + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + log.error("Map preview job rejected: " + r); + } + +} diff --git a/src/main/java/mobac/gui/mapview/MemoryTileCache.java b/src/main/java/mobac/gui/mapview/MemoryTileCache.java new file mode 100644 index 0000000..133967f --- /dev/null +++ b/src/main/java/mobac/gui/mapview/MemoryTileCache.java @@ -0,0 +1,284 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryNotificationInfo; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.Hashtable; + +import javax.management.Notification; +import javax.management.NotificationBroadcaster; +import javax.management.NotificationListener; + +import mobac.gui.mapview.Tile.TileState; +import mobac.program.interfaces.MapSource; + +import org.apache.log4j.Logger; + +/** + * {@link TileImageCache} implementation that stores all {@link Tile} objects in memory up to a certain limit ( + * {@link #getCacheSize()}). If the limit is exceeded the least recently used {@link Tile} objects will be deleted. + * + * @author Jan Peter Stotz + * @author r_x + */ +public class MemoryTileCache implements NotificationListener { + + protected final Logger log; + + /** + * Default cache size + */ + protected int cacheSize = 200; + + protected Hashtable hashtable; + + /** + * List of all tiles in their last recently used order + */ + protected CacheLinkedListElement lruTiles; + + public MemoryTileCache() { + log = Logger.getLogger(this.getClass()); + hashtable = new Hashtable(cacheSize); + lruTiles = new CacheLinkedListElement(); + + cacheSize = 500; + MemoryMXBean mbean = ManagementFactory.getMemoryMXBean(); + NotificationBroadcaster emitter = (NotificationBroadcaster) mbean; + emitter.addNotificationListener(this, null, null); + // Set-up each memory pool to notify if the free memory falls below 10% + for (MemoryPoolMXBean memPool : ManagementFactory.getMemoryPoolMXBeans()) { + if (memPool.isUsageThresholdSupported()) { + MemoryUsage memUsage = memPool.getUsage(); + memPool.setUsageThreshold((long) (memUsage.getMax() * 0.95)); + } + } + } + + /** + * In case we are running out of memory we free half of the cached down to a minimum of 25 cached tiles. + */ + public void handleNotification(Notification notification, Object handback) { + log.trace("Memory notification: " + notification.toString()); + if (!MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED.equals(notification.getType())) + return; + synchronized (lruTiles) { + int count_half = lruTiles.getElementCount() / 2; + count_half = Math.max(25, count_half); + if (lruTiles.getElementCount() <= count_half) + return; + log.warn("memory low - freeing cached tiles: " + lruTiles.getElementCount() + " -> " + count_half); + try { + while (lruTiles.getElementCount() > count_half) { + removeEntry(lruTiles.getLastElement()); + } + } catch (Exception e) { + log.error("", e); + } + } + } + + public void addTile(Tile tile) { + CacheEntry entry = createCacheEntry(tile); + hashtable.put(tile.getKey(), entry); + lruTiles.addFirst(entry); + if (hashtable.size() > cacheSize) + removeOldEntries(); + } + + public Tile getTile(MapSource source, int x, int y, int z) { + CacheEntry entry = hashtable.get(Tile.getTileKey(source, x, y, z)); + if (entry == null) + return null; + // We don't care about placeholder tiles and hourglass image tiles, the + // important tiles are the loaded ones + if (entry.tile.getTileState() == TileState.TS_LOADED) + lruTiles.moveElementToFirstPos(entry); + return entry.tile; + } + + /** + * Removes the least recently used tiles + */ + protected void removeOldEntries() { + synchronized (lruTiles) { + try { + while (lruTiles.getElementCount() > cacheSize) { + removeEntry(lruTiles.getLastElement()); + } + } catch (Exception e) { + log.warn("", e); + } + } + } + + protected void removeEntry(CacheEntry entry) { + hashtable.remove(entry.tile.getKey()); + lruTiles.removeEntry(entry); + } + + protected CacheEntry createCacheEntry(Tile tile) { + return new CacheEntry(tile); + } + + /** + * Clears the cache deleting all tiles from memory + */ + public void clear() { + synchronized (lruTiles) { + hashtable.clear(); + lruTiles.clear(); + } + } + + public int getTileCount() { + return hashtable.size(); + } + + public int getCacheSize() { + return cacheSize; + } + + /** + * Changes the maximum number of {@link Tile} objects that this cache holds. + * + * @param cacheSize + * new maximum number of tiles + */ + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + if (hashtable.size() > cacheSize) + removeOldEntries(); + } + + /** + * Linked list element holding the {@link Tile} and links to the {@link #next} and {@link #prev} item in the list. + */ + protected static class CacheEntry { + Tile tile; + + CacheEntry next; + CacheEntry prev; + + protected CacheEntry(Tile tile) { + this.tile = tile; + } + + public Tile getTile() { + return tile; + } + + public CacheEntry getNext() { + return next; + } + + public CacheEntry getPrev() { + return prev; + } + + } + + /** + * Special implementation of a double linked list for {@link CacheEntry} elements. It supports element removal in + * constant time - in difference to the Java implementation which needs O(n). + * + * @author Jan Peter Stotz + */ + protected static class CacheLinkedListElement { + protected CacheEntry firstElement = null; + protected CacheEntry lastElement; + protected int elementCount; + + public CacheLinkedListElement() { + clear(); + } + + public synchronized void clear() { + elementCount = 0; + firstElement = null; + lastElement = null; + } + + /** + * Add the element to the head of the list. + * + * @param new element to be added + */ + public synchronized void addFirst(CacheEntry element) { + if (elementCount == 0) { + firstElement = element; + lastElement = element; + element.prev = null; + element.next = null; + } else { + element.next = firstElement; + firstElement.prev = element; + element.prev = null; + firstElement = element; + } + elementCount++; + } + + /** + * Removes the specified elemntent form the list. + * + * @param element + * to be removed + */ + public synchronized void removeEntry(CacheEntry element) { + if (element.next != null) { + element.next.prev = element.prev; + } + if (element.prev != null) { + element.prev.next = element.next; + } + if (element == firstElement) + firstElement = element.next; + if (element == lastElement) + lastElement = element.prev; + element.next = null; + element.prev = null; + elementCount--; + } + + public synchronized void moveElementToFirstPos(CacheEntry entry) { + if (firstElement == entry) + return; + removeEntry(entry); + addFirst(entry); + } + + public int getElementCount() { + return elementCount; + } + + public CacheEntry getLastElement() { + return lastElement; + } + + public CacheEntry getFirstElement() { + return firstElement; + } + + } +} diff --git a/src/main/java/mobac/gui/mapview/PreviewMap.java b/src/main/java/mobac/gui/mapview/PreviewMap.java new file mode 100644 index 0000000..89a1a2d --- /dev/null +++ b/src/main/java/mobac/gui/mapview/PreviewMap.java @@ -0,0 +1,505 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.util.LinkedList; + +import mobac.gui.mapview.controller.DefaultMapController; +import mobac.gui.mapview.controller.JMapController; +import mobac.gui.mapview.controller.MapKeyboardController; +import mobac.gui.mapview.controller.RectangleSelectionMapController; +import mobac.gui.mapview.interfaces.MapEventListener; +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.Bookmark; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.MapSelection; +import mobac.program.model.MercatorPixelCoordinate; +import mobac.program.model.Settings; +import mobac.utilities.I18nUtils; +import mobac.utilities.MyMath; + +import org.apache.log4j.Logger; + +public class PreviewMap extends JMapViewer { + + private static final long serialVersionUID = 1L; + + public static final Color GRID_COLOR = new Color(200, 20, 20, 130); + public static final Color SEL_COLOR = new Color(0.9f, 0.7f, 0.7f, 0.6f); + public static final Color MAP_COLOR = new Color(1.0f, 0.84f, 0.0f, 0.4f); + + public static final int MAP_CONTROLLER_RECTANGLE_SELECT = 0; + public static final int MAP_CONTROLLER_GPX = 1; + + protected static final Font LOADING_FONT = new Font("Sans Serif", Font.BOLD, 30); + + private static Logger log = Logger.getLogger(PreviewMap.class); + + /** + * Interactive map selection max/min pixel coordinates regarding zoom level MAX_ZOOM + */ + private Point iSelectionMin; + private Point iSelectionMax; + + /** + * Map selection max/min pixel coordinates regarding zoom level MAX_ZOOM with respect to the grid zoom. + */ + private Point gridSelectionStart; + private Point gridSelectionEnd; + + /** + * Pre-painted transparent tile with grid lines on it. This makes painting the grid a lot faster in difference to + * painting each line or rectangle if the grid zoom is much higher that the current zoom level. + */ + private BufferedImage gridTile = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); + + private int gridZoom = -1; + private int gridSize; + + protected LinkedList mapEventListeners = new LinkedList(); + + protected JMapController mapKeyboardController; + protected JMapController mapSelectionController; + protected DefaultMapController defaultMapController; + + private final WgsGrid wgsGrid = new WgsGrid(Settings.getInstance().wgsGrid, this); + + public PreviewMap() { + super(MapSourcesManager.getInstance().getDefaultMapSource(), 5); + setEnabled(false); + defaultMapController = new DefaultMapController(this); + mapMarkersVisible = false; + setZoomContolsVisible(false); + + mapKeyboardController = new MapKeyboardController(this, true); + setMapSelectionController(new RectangleSelectionMapController(this)); + } + + public void setDisplayPositionByLatLon(EastNorthCoordinate c, int zoom) { + setDisplayPositionByLatLon(new Point(getWidth() / 2, getHeight() / 2), c.lat, c.lon, zoom); + } + + /** + * Updates the current position in {@link Settings} to the current view + */ + public void settingsSave() { + Settings settings = Settings.getInstance(); + settings.mapviewZoom = getZoom(); + settings.mapviewCenterCoordinate = getCenterCoordinate(); + settings.mapviewGridZoom = gridZoom; + settings.mapviewMapSource = mapSource.getName(); + settings.mapviewSelectionMin = iSelectionMin; + settings.mapviewSelectionMax = iSelectionMax; + } + + /** + * Sets the current view by the current values from {@link Settings} + */ + public void settingsLoad() { + Settings settings = Settings.getInstance(); + MapSource mapSource = MapSourcesManager.getInstance().getSourceByName(settings.mapviewMapSource); + if (mapSource != null) + setMapSource(mapSource); + EastNorthCoordinate c = settings.mapviewCenterCoordinate; + gridZoom = settings.mapviewGridZoom; + setDisplayPositionByLatLon(c, settings.mapviewZoom); + setSelectionByTileCoordinate(MAX_ZOOM, settings.mapviewSelectionMin, settings.mapviewSelectionMax, true); + } + + @Override + public void setMapSource(MapSource newMapSource) { + if (newMapSource.equals(mapSource)) + return; + log.trace("Preview map source changed from " + mapSource + " to " + newMapSource); + super.setMapSource(newMapSource); + if (mapEventListeners == null) + return; + for (MapEventListener listener : mapEventListeners) + listener.mapSourceChanged(mapSource); + } + + protected void zoomChanged(int oldZoom) { + log.trace("Preview map zoom changed from " + oldZoom + " to " + zoom); + if (mapEventListeners != null) + for (MapEventListener listener : mapEventListeners) + listener.zoomChanged(zoom); + updateGridValues(); + } + + public void setGridZoom(int gridZoom) { + if (gridZoom == this.gridZoom) + return; + this.gridZoom = gridZoom; + updateGridValues(); + applyGridOnSelection(); + updateMapSelection(); + repaint(); + } + + public int getGridZoom() { + return gridZoom; + } + + /** + * Updates the gridSize and the gridTile. This method has to called if + * mapSource or zoom as been changed. + */ + protected void updateGridValues() { + if (gridZoom < 0) + return; + int zoomToGridZoom = zoom - gridZoom; + int tileSize = mapSource.getMapSpace().getTileSize(); + if (zoomToGridZoom > 0) { + gridSize = tileSize << zoomToGridZoom; + gridTile = null; + } else { + gridSize = tileSize >> (-zoomToGridZoom); + BufferedImage newGridTile = null; + if (gridSize > 2) { + newGridTile = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = newGridTile.createGraphics(); + int alpha = 5 + (6 + zoomToGridZoom) * 16; + alpha = Math.max(0, alpha); + alpha = Math.min(130, alpha); + g.setColor(new Color(200, 20, 20, alpha)); + for (int x = 0; x < tileSize; x += gridSize) + g.drawLine(x, 0, x, 255); + for (int y = 0; y < tileSize; y += gridSize) + g.drawLine(0, y, 255, y); + } + gridTile = newGridTile; + } + } + + @Override + protected void paintComponent(Graphics graphics) { + if (!isEnabled()) { + graphics.setFont(LOADING_FONT); + graphics.drawString(I18nUtils.localizedStringForKey("map_loading_wait"), 100, 100); + return; + } + if (mapSource == null) + return; + Graphics2D g = (Graphics2D) graphics; + super.paintComponent(g); + + Point tlc = getTopLeftCoordinate(); + if (gridZoom >= 0) { + // Only paint grid if it is enabled (gridZoom not -1) + int max = (256 << zoom); + int w = Math.min(getWidth(), max - tlc.x); + int h = Math.min(getHeight(), max - tlc.y); + g.setColor(GRID_COLOR); + // g.setStroke(new BasicStroke(4.0f)); + if (gridSize > 1) { + int tilesize = mapSource.getMapSpace().getTileSize(); + if (gridSize >= tilesize) { + int off_x = tlc.x < 0 ? -tlc.x : -(tlc.x % gridSize); + int off_y = tlc.y < 0 ? -tlc.y : -(tlc.y % gridSize); + for (int x = off_x; x <= w; x += gridSize) { + g.drawLine(x, off_y, x, h); + } + for (int y = off_y; y <= h; y += gridSize) { + g.drawLine(off_x, y, w, y); + } + } else { + int off_x = (tlc.x < 0) ? tlc.x : tlc.x % tilesize; + int off_y = (tlc.y < 0) ? tlc.y : tlc.y % tilesize; + for (int x = -off_x; x < w; x += 256) { + for (int y = -off_y; y < h; y += 256) { + g.drawImage(gridTile, x, y, null); + } + } + } + } + } + if (gridSelectionStart != null && gridSelectionEnd != null) { + // Draw the selection rectangle widened by the current grid + int zoomDiff = MAX_ZOOM - zoom; + int x_min = (gridSelectionStart.x >> zoomDiff) - tlc.x; + int y_min = (gridSelectionStart.y >> zoomDiff) - tlc.y; + int x_max = (gridSelectionEnd.x >> zoomDiff) - tlc.x; + int y_max = (gridSelectionEnd.y >> zoomDiff) - tlc.y; + + int w = x_max - x_min + 1; + int h = y_max - y_min + 1; + g.setColor(SEL_COLOR); + g.fillRect(x_min, y_min, w, h); + } + if (iSelectionMin != null && iSelectionMax != null) { + // Draw the selection rectangle exactly as it has been specified by the user + int zoomDiff = MAX_ZOOM - zoom; + int x_min = (iSelectionMin.x >> zoomDiff) - tlc.x; + int y_min = (iSelectionMin.y >> zoomDiff) - tlc.y; + int x_max = (iSelectionMax.x >> zoomDiff) - tlc.x; + int y_max = (iSelectionMax.y >> zoomDiff) - tlc.y; + + int w = x_max - x_min + 1; + int h = y_max - y_min + 1; + g.setColor(GRID_COLOR); + g.drawRect(x_min, y_min, w, h); + } + if (mapSource instanceof MapSourceTextAttribution) { + MapSourceTextAttribution ta = (MapSourceTextAttribution) mapSource; + String attributionText = ta.getAttributionText(); + if (attributionText != null) { + Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g); + int text_x = getWidth() - 10 - (int) stringBounds.getWidth(); + int text_y = getHeight() - 1 - (int) stringBounds.getHeight(); + g.setColor(Color.black); + g.drawString(attributionText, text_x + 1, text_y + 1); + g.setColor(Color.white); + g.drawString(attributionText, text_x, text_y); + } + } + if (Settings.getInstance().wgsGrid.enabled) { + wgsGrid.paintWgsGrid(g, mapSource.getMapSpace(), tlc, zoom); + } + ScaleBar.paintScaleBar(this, g, mapSource.getMapSpace(), tlc, zoom); + } + + public Bookmark getPositionBookmark() { + return new Bookmark(mapSource, zoom, center.x, center.y); + } + + public void gotoPositionBookmark(Bookmark bookmark) { + setMapSource(bookmark.getMapSource()); + setDisplayPositionByLatLon(bookmark, bookmark.getZoom()); + setZoom(bookmark.getZoom()); + } + + /** + * @return Coordinate of the point in the center of the currently displayed map region + */ + public EastNorthCoordinate getCenterCoordinate() { + MapSpace mapSpace = mapSource.getMapSpace(); + double lon = mapSpace.cXToLon(center.x, zoom); + double lat = mapSpace.cYToLat(center.y, zoom); + return new EastNorthCoordinate(lat, lon); + } + + /** + * @return Coordinate of the top left corner visible regarding the current map source (pixel) + */ + public Point getTopLeftCoordinate() { + return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2)); + } + + public void zoomTo(MapSelection ms) { + if (!ms.isAreaSelected()) + return; + log.trace("Setting selection to: " + ms); + Point max = ms.getBottomRightPixelCoordinate(MAX_ZOOM); + Point min = ms.getTopLeftPixelCoordinate(MAX_ZOOM); + setDisplayToFitPixelCoordinates(max.x, max.y, min.x, min.y); + } + + /** + * Zooms to the specified {@link MapSelection} and sets the selection to it; + * + * @param ms + * @param notifyListeners + */ + public void setSelectionAndZoomTo(MapSelection ms, boolean notifyListeners) { + log.trace("Setting selection to: " + ms); + Point max = ms.getBottomRightPixelCoordinate(MAX_ZOOM); + Point min = ms.getTopLeftPixelCoordinate(MAX_ZOOM); + setDisplayToFitPixelCoordinates(max.x, max.y, min.x, min.y); + Point pStart = ms.getTopLeftPixelCoordinate(zoom); + Point pEnd = ms.getBottomRightPixelCoordinate(zoom); + setSelectionByTileCoordinate(pStart, pEnd, notifyListeners); + } + + /** + * + * @param pStart + * x/y tile coordinate of the top left tile regarding the current zoom level + * @param pEnd + * x/y tile coordinate of the bottom right tile regarding the current zoom level + * @param notifyListeners + */ + public void setSelectionByTileCoordinate(Point pStart, Point pEnd, boolean notifyListeners) { + setSelectionByTileCoordinate(zoom, pStart, pEnd, notifyListeners); + } + + /** + * Sets the rectangular selection to the absolute tile coordinates pStart and pEnd + * regarding the zoom-level cZoom. + * + * @param cZoom + * @param pStart + * @param pEnd + * @param notifyListeners + */ + public void setSelectionByTileCoordinate(int cZoom, Point pStart, Point pEnd, boolean notifyListeners) { + if (pStart == null || pEnd == null) { + iSelectionMin = null; + iSelectionMax = null; + gridSelectionStart = null; + gridSelectionEnd = null; + return; + } + + Point pNewStart = new Point(); + Point pNewEnd = new Point(); + int mapMaxCoordinate = mapSource.getMapSpace().getMaxPixels(cZoom) - 1; + // Sort x/y coordinate of points so that pNewStart < pnewEnd and limit selection to map size + pNewStart.x = Math.max(0, Math.min(mapMaxCoordinate, Math.min(pStart.x, pEnd.x))); + pNewStart.y = Math.max(0, Math.min(mapMaxCoordinate, Math.min(pStart.y, pEnd.y))); + pNewEnd.x = Math.max(0, Math.min(mapMaxCoordinate, Math.max(pStart.x, pEnd.x))); + pNewEnd.y = Math.max(0, Math.min(mapMaxCoordinate, Math.max(pStart.y, pEnd.y))); + + int zoomDiff = MAX_ZOOM - cZoom; + + pNewEnd.x <<= zoomDiff; + pNewEnd.y <<= zoomDiff; + pNewStart.x <<= zoomDiff; + pNewStart.y <<= zoomDiff; + + iSelectionMin = pNewStart; + iSelectionMax = pNewEnd; + gridSelectionStart = null; + gridSelectionEnd = null; + + updateGridValues(); + applyGridOnSelection(); + + if (notifyListeners) + updateMapSelection(); + repaint(); + } + + protected void applyGridOnSelection() { + if (gridZoom < 0) { + gridSelectionStart = iSelectionMin; + gridSelectionEnd = iSelectionMax; + return; + } + + if (iSelectionMin == null || iSelectionMax == null) + return; + + int gridZoomDiff = MAX_ZOOM - gridZoom; + int gridFactor = mapSource.getMapSpace().getTileSize() << gridZoomDiff; + + Point pNewStart = new Point(iSelectionMin); + Point pNewEnd = new Point(iSelectionMax); + + // Snap to the current grid + + pNewStart.x = MyMath.roundDownToNearest(pNewStart.x, gridFactor); + pNewStart.y = MyMath.roundDownToNearest(pNewStart.y, gridFactor); + pNewEnd.x = MyMath.roundUpToNearest(pNewEnd.x, gridFactor) - 1; + pNewEnd.y = MyMath.roundUpToNearest(pNewEnd.y, gridFactor) - 1; + + gridSelectionStart = pNewStart; + gridSelectionEnd = pNewEnd; + } + + /** + * Notifies all registered {@link MapEventListener} of a + * {@link MapEventListener#selectionChanged(MercatorPixelCoordinate, MercatorPixelCoordinate)} event. + */ + public void updateMapSelection() { + int x_min, y_min, x_max, y_max; + + if (gridZoom >= 0) { + if (gridSelectionStart == null || gridSelectionEnd == null) + return; + x_min = gridSelectionStart.x; + y_min = gridSelectionStart.y; + x_max = gridSelectionEnd.x; + y_max = gridSelectionEnd.y; + } else { + if (iSelectionMin == null || iSelectionMax == null) + return; + x_min = iSelectionMin.x; + y_min = iSelectionMin.y; + x_max = iSelectionMax.x; + y_max = iSelectionMax.y; + } + MercatorPixelCoordinate min = new MercatorPixelCoordinate(mapSource.getMapSpace(), x_min, y_min, MAX_ZOOM); + MercatorPixelCoordinate max = new MercatorPixelCoordinate(mapSource.getMapSpace(), x_max, y_max, MAX_ZOOM); + // log.debug("sel min: [" + min + "]"); + // log.debug("sel max: [" + max + "]"); + for (MapEventListener listener : mapEventListeners) + listener.selectionChanged(max, min); + } + + public void addMapEventListener(MapEventListener l) { + mapEventListeners.add(l); + } + + public void selectPreviousMap() { + for (MapEventListener listener : mapEventListeners) { + listener.selectPreviousMapSource(); + } + } + + public void selectNextMap() { + for (MapEventListener listener : mapEventListeners) { + listener.selectNextMapSource(); + } + } + + /** + * Clears the in-memory tile cache and performs a repaint which causes a reload of all displayed tiles (from disk or + * if not present from the map source via network). + */ + public void refreshMap() { + tileCache.clear(); + repaint(); + } + + public JMapController getMapKeyboardController() { + return mapKeyboardController; + } + + /** + * @return Currently active mapSelectionController + */ + public JMapController getMapSelectionController() { + return mapSelectionController; + } + + /** + * Sets a new mapSelectionController. Previous controller are disabled and removed. + * + * @param mapSelectionController + */ + public void setMapSelectionController(JMapController mapSelectionController) { + if (this.mapSelectionController != null) + this.mapSelectionController.disable(); + this.mapSelectionController = mapSelectionController; + mapSelectionController.enable(); + for (MapEventListener listener : mapEventListeners) { + listener.mapSelectionControllerChanged(mapSelectionController); + } + repaint(); + } + +} diff --git a/src/main/java/mobac/gui/mapview/ScaleBar.java b/src/main/java/mobac/gui/mapview/ScaleBar.java new file mode 100644 index 0000000..7e6e6f9 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/ScaleBar.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Stroke; + +import javax.swing.JComponent; + +import mobac.program.interfaces.MapSpace; +import mobac.program.model.Settings; +import mobac.program.model.UnitSystem; +import mobac.utilities.MyMath; + +/** + * Simple scale bar showing the map scale using the selected unit system. + */ +public class ScaleBar { + + private static final Stroke STROKE = new BasicStroke(1); + private static final Font FONT = new Font("Sans Serif", Font.PLAIN, 12); + + /** + * Horizontal margin between scale bar and right border of the map + */ + private static final int MARGIN_X = 40; + + /** + * Vertical margin between scale bar and bottom border of the map + */ + private static final int MARGIN_Y = 40; + + private static final int DESIRED_SCALE_BAR_WIDTH = 150; + + public static void paintScaleBar(JComponent c, Graphics2D g, MapSpace mapSpace, Point tlc, int zoom) { + Rectangle r = c.getBounds(); + int posX; + int posY = r.height - r.y; + posY -= MARGIN_Y; + posX = MARGIN_X; + + // int coordX = tlc.x + posX; + int coordY = tlc.y + posY; + + int w1 = DESIRED_SCALE_BAR_WIDTH; + + UnitSystem unitSystem = Settings.getInstance().unitSystem; + + // Calculate the angular distance of our desired scale bar + double ad = mapSpace.horizontalDistance(zoom, coordY, w1); + + String unit = unitSystem.unitLarge; + // convert angular into the selected unit system + double dist1 = ad * unitSystem.earthRadius; + // distance is smaller that one (km/mi)? the use smaller units (m/ft) + if (dist1 < 1.0) { + dist1 *= unitSystem.unitFactor; + unit = unitSystem.unitSmall; + } + // Round everything to a nice value + double dist2 = MyMath.prettyRound(dist1); + double factor = dist2 / dist1; + // apply the round factor to the width of our scale bar + int w2 = (int) (w1 * factor); + + g.setStroke(STROKE); + g.setColor(Color.YELLOW); + g.fillRect(posX, posY - 10, w2, 20); + g.setColor(Color.BLACK); + g.drawRect(posX, posY - 10, w2, 20); + String value = Integer.toString((int) dist2) + " " + unit; + g.setFont(FONT); + g.drawString(value, posX + 10, posY + 4); + } + +} diff --git a/src/main/java/mobac/gui/mapview/Tile.java b/src/main/java/mobac/gui/mapview/Tile.java new file mode 100644 index 0000000..534503d --- /dev/null +++ b/src/main/java/mobac/gui/mapview/Tile.java @@ -0,0 +1,266 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import mobac.program.interfaces.MapSource; +import mobac.utilities.Utilities; + +/** + * Holds one map tile. Additionally the code for loading the tile image and painting it is also included in this class. + * + * @author Jan Peter Stotz + */ +public class Tile { + + /** + * Hourglass image that is displayed until a map tile has been loaded + */ + public static BufferedImage LOADING_IMAGE; + public static BufferedImage ERROR_IMAGE; + + static { + try { + LOADING_IMAGE = ImageIO.read(Utilities.getResourceImageUrl("hourglass.png")); + ERROR_IMAGE = ImageIO.read(Utilities.getResourceImageUrl("error.png")); + } catch (Exception e1) { + LOADING_IMAGE = null; + ERROR_IMAGE = null; + } + } + + public enum TileState { + TS_NEW, TS_LOADING, TS_LOADED, TS_ERROR + }; + + protected MapSource mapSource; + protected int xtile; + protected int ytile; + protected int zoom; + protected BufferedImage image; + protected String key; + protected TileState tileState = TileState.TS_NEW; + + /** + * Creates a tile with empty image. + * + * @param mapSource + * @param xtile + * @param ytile + * @param zoom + */ + public Tile(MapSource mapSource, int xtile, int ytile, int zoom) { + super(); + this.mapSource = mapSource; + this.xtile = xtile; + this.ytile = ytile; + this.zoom = zoom; + this.image = LOADING_IMAGE; + this.key = getTileKey(mapSource, xtile, ytile, zoom); + } + + public Tile(MapSource source, int xtile, int ytile, int zoom, BufferedImage image) { + this(source, xtile, ytile, zoom); + this.image = image; + } + + /** + * Tries to get tiles of a lower or higher zoom level (one or two level difference) from cache and use it as a + * placeholder until the tile has been loaded. + */ + public void loadPlaceholderFromCache(MemoryTileCache cache) { + int tileSize = mapSource.getMapSpace().getTileSize(); + BufferedImage tmpImage = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_RGB); + Graphics2D g = (Graphics2D) tmpImage.getGraphics(); + // g.drawImage(image, 0, 0, null); + for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) { + // first we check if there are already the 2^x tiles + // of a higher detail level + int zoom_high = zoom + zoomDiff; + if (zoomDiff < 3 && zoom_high <= JMapViewer.MAX_ZOOM) { + int factor = 1 << zoomDiff; + int xtile_high = xtile << zoomDiff; + int ytile_high = ytile << zoomDiff; + double scale = 1.0 / factor; + g.setTransform(AffineTransform.getScaleInstance(scale, scale)); + int paintedTileCount = 0; + for (int x = 0; x < factor; x++) { + for (int y = 0; y < factor; y++) { + Tile tile = cache.getTile(mapSource, xtile_high + x, ytile_high + y, zoom_high); + if (tile != null && tile.tileState == TileState.TS_LOADED) { + paintedTileCount++; + tile.paint(g, x * tileSize, y * tileSize); + } + } + } + if (paintedTileCount == factor * factor) { + image = tmpImage; + return; + } + } + + int zoom_low = zoom - zoomDiff; + if (zoom_low >= JMapViewer.MIN_ZOOM) { + int xtile_low = xtile >> zoomDiff; + int ytile_low = ytile >> zoomDiff; + int factor = (1 << zoomDiff); + double scale = factor; + AffineTransform at = new AffineTransform(); + int translate_x = (xtile % factor) * tileSize; + int translate_y = (ytile % factor) * tileSize; + at.setTransform(scale, 0, 0, scale, -translate_x, -translate_y); + g.setTransform(at); + Tile tile = cache.getTile(mapSource, xtile_low, ytile_low, zoom_low); + if (tile != null && tile.tileState == TileState.TS_LOADED) { + tile.paint(g, 0, 0); + image = tmpImage; + return; + } + } + } + } + + public MapSource getSource() { + return mapSource; + } + + /** + * @return tile number on the x axis of this tile + */ + public int getXtile() { + return xtile; + } + + /** + * @return tile number on the y axis of this tile + */ + public int getYtile() { + return ytile; + } + + /** + * @return zoom level of this tile + */ + public int getZoom() { + return zoom; + } + + public BufferedImage getImage() { + return image; + } + + public void setImage(BufferedImage image) { + this.image = image; + } + + public void setErrorImage() { + image = ERROR_IMAGE; + tileState = TileState.TS_ERROR; + } + + public void loadImage(InputStream input) throws IOException { + image = ImageIO.read(input); + } + + public void loadImage(byte[] data) throws IOException { + loadImage(new ByteArrayInputStream(data)); + } + + /** + * @return key that identifies a tile + */ + public String getKey() { + return key; + } + + public boolean isErrorTile() { + return (ERROR_IMAGE.equals(image)); + } + + public TileState getTileState() { + return tileState; + } + + public void setTileState(TileState tileState) { + this.tileState = tileState; + } + + /** + * Paints the tile-image on the {@link Graphics} g at the position x/y. + * + * @param g + * @param x + * x-coordinate in g + * @param y + * y-coordinate in g + */ + public void paint(Graphics g, int x, int y) { + if (image == null) + return; + + int tileSize = mapSource.getMapSpace().getTileSize(); + //Google Scale = 2, retina support + g.drawImage(image, x, y, tileSize, tileSize, Color.WHITE, null); + //g.drawImage(image, x, y, Color.WHITE); + } + + public void paintTransparent(Graphics g, int x, int y) { + if (image == null) + return; + + int tileSize = mapSource.getMapSpace().getTileSize(); + //Google Scale = 2, retina support + g.drawImage(image, x, y, tileSize, tileSize, null); + //g.drawImage(image, x, y, null); + } + + @Override + public String toString() { + return "tile " + key; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Tile)) + return false; + Tile tile = (Tile) obj; + return (xtile == tile.xtile) && (ytile == tile.ytile) && (zoom == tile.zoom); + } + + @Override + public int hashCode() { + assert false : "hashCode not designed"; + return -1; + } + + public static String getTileKey(MapSource source, int xtile, int ytile, int zoom) { + return zoom + "/" + xtile + "/" + ytile + "@" + source.getName(); + } + +} diff --git a/src/main/java/mobac/gui/mapview/TileLoader.java b/src/main/java/mobac/gui/mapview/TileLoader.java new file mode 100644 index 0000000..c828209 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/TileLoader.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.image.BufferedImage; +import java.net.ConnectException; + +import mobac.exceptions.DownloadFailedException; +import mobac.gui.mapview.Tile.TileState; +import mobac.gui.mapview.interfaces.TileLoaderListener; +import mobac.program.download.TileDownLoader; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSource.LoadMethod; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreEntry; + +import org.apache.log4j.Logger; + +/** + * A {@link TileLoaderJobCreator} implementation that loads tiles from OSM via HTTP and saves all loaded files in a + * directory located in the the temporary directory. If a tile is present in this file cache it will not be loaded from + * OSM again. + * + * @author Jan Peter Stotz + * @author r_x + */ +public class TileLoader { + + private static final Logger log = Logger.getLogger(TileLoader.class); + + protected TileStore tileStore; + protected TileLoaderListener listener; + + public TileLoader(TileLoaderListener listener) { + super(); + this.listener = listener; + tileStore = TileStore.getInstance(); + } + + public Runnable createTileLoaderJob(final MapSource source, final int tilex, final int tiley, final int zoom) { + return new TileAsyncLoadJob(source, tilex, tiley, zoom); + } + + protected class TileAsyncLoadJob implements Runnable { + + final int tilex, tiley, zoom; + final MapSource mapSource; + Tile tile; + boolean fileTilePainted = false; + protected TileStoreEntry tileStoreEntry = null; + + public TileAsyncLoadJob(MapSource source, int tilex, int tiley, int zoom) { + super(); + this.mapSource = source; + this.tilex = tilex; + this.tiley = tiley; + this.zoom = zoom; + } + + public void run() { + MemoryTileCache cache = listener.getTileImageCache(); + synchronized (cache) { + tile = cache.getTile(mapSource, tilex, tiley, zoom); + if (tile == null || tile.tileState != TileState.TS_NEW) + return; + tile.setTileState(TileState.TS_LOADING); + } + if (loadTileFromStore()) + return; + if (fileTilePainted) { + Runnable job = new Runnable() { + + public void run() { + loadOrUpdateTile(); + } + }; + JobDispatcher.getInstance().addJob(job); + } else { + loadOrUpdateTile(); + } + } + + protected void loadOrUpdateTile() { + try { + BufferedImage image = mapSource.getTileImage(zoom, tilex, tiley, LoadMethod.DEFAULT); + if (image != null) { + tile.setImage(image); + tile.setTileState(TileState.TS_LOADED); + listener.tileLoadingFinished(tile, true); + } else { + tile.setErrorImage(); + listener.tileLoadingFinished(tile, false); + } + return; + } catch (ConnectException e) { + log.warn("Downloading of " + tile + " failed " + e.getMessage()); + } catch (DownloadFailedException e) { + log.warn("Downloading of " + tile + " failed " + e.getMessage()); + } catch (Exception e) { + log.debug("Downloading of " + tile + " failed", e); + } + tile.setErrorImage(); + listener.tileLoadingFinished(tile, false); + } + + protected boolean loadTileFromStore() { + try { + BufferedImage image = mapSource.getTileImage(zoom, tilex, tiley, LoadMethod.CACHE); + if (image == null) + return false; + tile.setImage(image); + listener.tileLoadingFinished(tile, true); + if (TileDownLoader.isTileExpired(tileStoreEntry)) + return false; + fileTilePainted = true; + return true; + } catch (Exception e) { + log.error("Failed to load tile (z=" + zoom + ",x=" + tilex + ",y=" + tiley + ") from tile store", e); + } + return false; + } + + } + +} diff --git a/src/main/java/mobac/gui/mapview/WgsGrid.java b/src/main/java/mobac/gui/mapview/WgsGrid.java new file mode 100644 index 0000000..b5f78d3 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/WgsGrid.java @@ -0,0 +1,284 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Stroke; + +import javax.swing.JComponent; + +import mobac.program.interfaces.MapSpace; +import mobac.program.model.Coordinate; +import mobac.program.model.SettingsWgsGrid; +import mobac.utilities.I18nUtils; + +public class WgsGrid { + + public static enum WgsDensity { + + DEGREES_90(0), DEGREES_45(1), DEGREES_30(2), DEGREES_15(3), DEGREES_10(4), DEGREES_5(5), DEGREES_2(6), DEGREE_1( + 7), MINUTES_30(8), MINUTES_20(9), MINUTES_10(10), MINUTES_5(11), MINUTES_2(12), MINUTE_1(13), SECONDS_30( + 15), SECONDS_20(15), SECONDS_10(16), SECONDS_5(17), SECONDS_2(18), SECOND_1(19); + + public final int iStep, minZoom; + public final boolean compressDegree, compressMinute, displayMinute, displaySecond; + //private final String string; + + private WgsDensity(final int minZoom) { + this.minZoom = minZoom; + String[] split = name().split("_"); + int value = Integer.parseInt(split[1]); + + if (split[0].startsWith("D")) { + iStep = value * Coordinate.DEGREE; + displayMinute = displaySecond = false; + compressDegree = compressMinute = false; + } else if (split[0].startsWith("M")) { + iStep = value * Coordinate.MINUTE; + compressDegree = true/* value <= 15 */; + displayMinute = true; + displaySecond = compressMinute = false; + } else { + iStep = value * Coordinate.SECOND; + compressDegree = displayMinute = displaySecond = true; + compressMinute = true/* value <= 15 */; + } + } + + public String toString() { + String[] split = name().split("_"); + String unitKey = "map_ctrl_wgs_grid_density_"+ split[0].toLowerCase(); + return I18nUtils.localizedStringForKey("map_ctrl_wgs_grid_density_prefix") + " " + split[1] + " " + + I18nUtils.localizedStringForKey(unitKey); + } + } + + public static enum Placement { + BOTTOM_RIGHT, BOTTOM_LEFT, TOP_RIGHT, TOP_LEFT + } + + private static final WgsDensity DENSITIES[] = WgsDensity.values(); + private static final Stroke BASIC_STROKE = new BasicStroke(1f); + private static final int LABEL_OFFSET = 2; + + private final StringBuilder stringBuilder = new StringBuilder(16); + private final Rectangle viewport = new Rectangle(); + public final SettingsWgsGrid s; + private final JComponent c; + + private Placement placement = Placement.BOTTOM_RIGHT; + private BasicStroke stroke; + private int lastDegree, lastMinute; + + public WgsGrid(SettingsWgsGrid s, JComponent c) { + this.s = s; + this.c = c; + } + + public void paintWgsGrid(Graphics2D g, MapSpace ms, Point tlc, int zoom) { + if (!s.enabled) { + return; + } + + // Check density + WgsDensity density = s.density; + while (zoom < density.minZoom) { + int index = density.ordinal(); + if (--index <= 0) { + return; + } + density = DENSITIES[index]; + } + + final int maxPixels = ms.getMaxPixels(zoom); + + // Check viewport + viewport.width = c.getWidth(); + viewport.height = c.getHeight(); + if (tlc.x > maxPixels || tlc.y > maxPixels || tlc.x + viewport.width < 0 || tlc.y + viewport.width < 0) { + return; + } + viewport.x = c.getX(); + viewport.y = c.getY(); + + // Translate according to mapSpace + viewport.translate(tlc.x, tlc.y); + g = (Graphics2D) g.create(); + g.translate(-tlc.x, -tlc.y); + + // Calculate viewport coordinates + final int x1 = Math.max(tlc.x, 0); + final int y1 = Math.max(tlc.y, 0); + final int x2 = Math.min(tlc.x + viewport.width, maxPixels); + final int y2 = Math.min(tlc.y + viewport.height, maxPixels); + + // Calculate line coordinates + final int hLineX1 = x1 + 1; + final int hLineX2 = x2 - 1; + final int vLineY1 = y1 + 1; + final int vLineY2 = y2 - 1; + + // Calculate line indexes + final int vMin = Coordinate.doubleToInt(ms.cXToLon(x1, zoom)) / density.iStep; + final int vMax = Coordinate.doubleToInt(ms.cXToLon(x2, zoom)) / density.iStep; + final int hMin = Coordinate.doubleToInt(ms.cYToLat(y2, zoom)) / density.iStep; + final int hMax = Coordinate.doubleToInt(ms.cYToLat(y1, zoom)) / density.iStep; + + g.setBackground(Color.WHITE); + g.setColor(s.color); + + g.setStroke(checkAndGetStroke()); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // Paint vertical lines + for (int i = vMin; i <= vMax; i++) { + int iLon = i * density.iStep; + int x = ms.cLonToX(Coordinate.intToDouble(iLon), zoom); + if (x > x1 && x < x2) { + g.drawLine(x, vLineY1, x, vLineY2); + } + } + + // Paint horizontal lines + for (int i = hMin; i <= hMax; i++) { + int iLat = i * density.iStep; + int y = ms.cLatToY(Coordinate.intToDouble(iLat), zoom); + if (y > y1 && y < y2) { + g.drawLine(hLineX1, y, hLineX2, y); + } + } + + // Set up font metrics + g.setStroke(BASIC_STROKE); + g.setFont(s.font); + final FontMetrics fontMetrics = g.getFontMetrics(); + final int fontDescent = fontMetrics.getDescent(); + final int fontHeight = fontMetrics.getHeight(); + + resetLabelCompression(); + + // Shared Y coordinates for vertical labels + int labelRectX; + int labelRectY = y2 - LABEL_OFFSET - fontHeight; + int labelY = y2 - LABEL_OFFSET - fontDescent; + int labelX; + + // Paint vertical labels + for (int i = vMin; i <= vMax; i++) { + + // Calculate coordinates + int iLon = i * density.iStep; + int x = ms.cLonToX(Coordinate.intToDouble(iLon), zoom); + + // Prepare label + String label = getLabel(iLon, density); + int stringWidth = fontMetrics.stringWidth(label); + labelRectX = labelX = x - stringWidth / 2; + + // Paint label + if (viewport.contains(labelRectX, labelRectY, stringWidth, fontHeight)) { + g.clearRect(labelRectX, labelRectY, stringWidth, fontHeight); + g.drawRect(labelRectX, labelRectY, stringWidth, fontHeight); + g.drawString(label, labelX, labelY); + } else { + resetLabelCompression(); + } + } + + resetLabelCompression(); + viewport.height -= fontHeight + LABEL_OFFSET; + labelX = labelRectX = x1 + LABEL_OFFSET; + + // Paint horizontal labels + for (int i = hMin; i <= hMax; i++) { + int iLat = i * density.iStep; + int y = ms.cLatToY(Coordinate.intToDouble(iLat), zoom); + + // Prepare label + String label = getLabel(iLat, density); + final int stringWidth = fontMetrics.stringWidth(label); + if (placement == Placement.BOTTOM_RIGHT || placement == Placement.TOP_RIGHT) { + labelX = labelRectX = x2 - LABEL_OFFSET - stringWidth; + } + labelRectY = y - fontHeight / 2; + labelY = labelRectY + fontHeight - fontDescent; + + // Paint label + if (viewport.contains(labelRectX, labelRectY, stringWidth, fontHeight)) { + g.clearRect(labelRectX, labelRectY, stringWidth, fontHeight); + g.drawRect(labelRectX, labelRectY, stringWidth, fontHeight); + g.drawString(label, labelX, labelY); + } else { + resetLabelCompression(); + } + } + g.dispose(); + } + + public void setPosition(Placement placement) { + this.placement = placement != null ? placement : Placement.BOTTOM_RIGHT; + } + + private Stroke checkAndGetStroke() { + if (stroke == null || stroke.getLineWidth() != s.width) { + stroke = new BasicStroke(s.width); + } + return stroke; + } + + private void resetLabelCompression() { + lastDegree = Integer.MAX_VALUE; + lastMinute = Integer.MAX_VALUE; + } + + private String getLabel(int coord, WgsDensity density) { + coord = Math.abs(coord); + int degree = Coordinate.getDegree(coord); + int minute = Coordinate.getMinute(coord); + int second = Coordinate.getSecond(coord); + stringBuilder.setLength(0); + stringBuilder.append(" "); + if (!s.compressLabels || lastDegree != degree || !density.compressDegree) { + stringBuilder.append(degree); + stringBuilder.append('\u00B0'); + } + if (density.displayMinute && (!s.compressLabels || lastMinute != minute || !density.compressMinute)) { + if (minute < 10) { + stringBuilder.append('0'); + } + stringBuilder.append(minute); + stringBuilder.append('\''); + } + if (density.displaySecond) { + if (second < 10) { + stringBuilder.append('0'); + } + stringBuilder.append(second); + stringBuilder.append('\"'); + } + stringBuilder.append(" "); + lastDegree = degree; + lastMinute = minute; + return stringBuilder.toString(); + } +} diff --git a/src/main/java/mobac/gui/mapview/controller/AbstractPolygonSelectionMapController.java b/src/main/java/mobac/gui/mapview/controller/AbstractPolygonSelectionMapController.java new file mode 100644 index 0000000..c54fb8b --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/AbstractPolygonSelectionMapController.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +import java.awt.Point; +import java.util.ArrayList; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.layer.PolygonSelectionLayer; + +/** + */ +public abstract class AbstractPolygonSelectionMapController extends JMapController { + + protected boolean finished = false; + protected ArrayList polygonPoints = new ArrayList(); + protected PolygonSelectionLayer mapLayer = null; + + public AbstractPolygonSelectionMapController(PreviewMap map) { + super(map, false); + mapLayer = new PolygonSelectionLayer(this); + } + + public void reset() { + polygonPoints = new ArrayList(); + finished = false; + } + + public void finishPolygon() { + finished = true; + } + + @Override + public void enable() { + map.mapLayers.add(mapLayer); + super.enable(); + } + + @Override + public void disable() { + map.mapLayers.remove(mapLayer); + super.disable(); + } + + /** + * @return List of absolute tile coordinate points regarding {@link JMapViewer#MAX_ZOOM} + */ + public ArrayList getPolygonPoints() { + return polygonPoints; + } + +} diff --git a/src/main/java/mobac/gui/mapview/controller/DefaultMapController.java b/src/main/java/mobac/gui/mapview/controller/DefaultMapController.java new file mode 100644 index 0000000..ad962ce --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/DefaultMapController.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +import mobac.gui.mapview.PreviewMap; +import mobac.utilities.OSUtilities; + +/** + * Default map controller which implements map moving by pressing the right mouse button and zooming by double click or + * by mouse wheel. + * + * @author Jan Peter Stotz + * + */ +public class DefaultMapController extends JMapController implements MouseListener, MouseMotionListener, + MouseWheelListener { + + private static final int MOUSE_BUTTONS_MASK = MouseEvent.BUTTON3_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK + | MouseEvent.BUTTON2_DOWN_MASK; + + private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK; + + public DefaultMapController(PreviewMap map) { + super(map, true); + } + + private Point lastDragPoint; + + private boolean isMoving = false; + + private boolean movementEnabled = true; + + private int movementMouseButton = MouseEvent.BUTTON3; + private int movementMouseButtonMask = MouseEvent.BUTTON3_DOWN_MASK; + + private boolean wheelZoomEnabled = true; + private boolean doubleClickZoomEnabled = true; + + public void mouseDragged(MouseEvent e) { + if (!movementEnabled || !isMoving) + return; + // Is only the selected mouse button pressed? + if ((e.getModifiersEx() & MOUSE_BUTTONS_MASK) == movementMouseButtonMask) { + Point p = e.getPoint(); + if (lastDragPoint != null) { + int diffx = lastDragPoint.x - p.x; + int diffy = lastDragPoint.y - p.y; + map.moveMap(diffx, diffy); + } + lastDragPoint = p; + } + } + + public void mouseClicked(MouseEvent e) { + if (doubleClickZoomEnabled && e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) + map.zoomIn(e.getPoint()); + } + + public void mousePressed(MouseEvent e) { + if (e.getButton() == movementMouseButton || OSUtilities.isPlatformOsx() + && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK) { + lastDragPoint = null; + isMoving = true; + } + } + + public void mouseReleased(MouseEvent e) { + if (e.getButton() == movementMouseButton || OSUtilities.isPlatformOsx() && e.getButton() == MouseEvent.BUTTON1) { + lastDragPoint = null; + isMoving = false; + } + } + + public void mouseWheelMoved(MouseWheelEvent e) { + if (wheelZoomEnabled) + map.setZoom(map.getZoom() - e.getWheelRotation(), e.getPoint()); + } + + public boolean isMovementEnabled() { + return movementEnabled; + } + + /** + * Enables or disables that the map pane can be moved using the mouse. + * + * @param movementEnabled + */ + public void setMovementEnabled(boolean movementEnabled) { + this.movementEnabled = movementEnabled; + } + + public int getMovementMouseButton() { + return movementMouseButton; + } + + /** + * Sets the mouse button that is used for moving the map. Possible values are: + *
    + *
  • {@link MouseEvent#BUTTON1} (left mouse button)
  • + *
  • {@link MouseEvent#BUTTON2} (middle mouse button)
  • + *
  • {@link MouseEvent#BUTTON3} (right mouse button)
  • + *
+ * + * @param movementMouseButton + */ + public void setMovementMouseButton(int movementMouseButton) { + this.movementMouseButton = movementMouseButton; + switch (movementMouseButton) { + case MouseEvent.BUTTON1: + movementMouseButtonMask = MouseEvent.BUTTON1_DOWN_MASK; + break; + case MouseEvent.BUTTON2: + movementMouseButtonMask = MouseEvent.BUTTON2_DOWN_MASK; + break; + case MouseEvent.BUTTON3: + movementMouseButtonMask = MouseEvent.BUTTON3_DOWN_MASK; + break; + default: + throw new RuntimeException("Unsupported button"); + } + } + + public boolean isWheelZoomEnabled() { + return wheelZoomEnabled; + } + + public void setWheelZoomEnabled(boolean wheelZoomEnabled) { + this.wheelZoomEnabled = wheelZoomEnabled; + } + + public boolean isDoubleClickZoomEnabled() { + return doubleClickZoomEnabled; + } + + public void setDoubleClickZoomEnabled(boolean doubleClickZoomEnabled) { + this.doubleClickZoomEnabled = doubleClickZoomEnabled; + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mouseMoved(MouseEvent e) { + + // Mac OSX simulates with ctrl + mouse 1 the second mouse button hence + // no dragging events get fired. + if (!OSUtilities.isPlatformOsx() || !movementEnabled || !isMoving) + return; + // Is only the selected mouse button pressed? + if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) { + Point p = e.getPoint(); + if (lastDragPoint != null) { + int diffx = lastDragPoint.x - p.x; + int diffy = lastDragPoint.y - p.y; + map.moveMap(diffx, diffy); + } + lastDragPoint = p; + } + + } + +} diff --git a/src/main/java/mobac/gui/mapview/controller/GpxMapController.java b/src/main/java/mobac/gui/mapview/controller/GpxMapController.java new file mode 100644 index 0000000..2191fc0 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/GpxMapController.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.math.BigDecimal; + +import javax.swing.JOptionPane; + +import mobac.data.gpx.gpx11.Gpx; +import mobac.data.gpx.gpx11.WptType; +import mobac.gui.actions.GpxEditor; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.gpxtree.GpxRootEntry; +import mobac.gui.gpxtree.RteEntry; +import mobac.gui.gpxtree.TrksegEntry; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.panels.JGpxPanel; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.I18nUtils; + +/** + * Allows to create new GPX way-points by clicking on the preview map + */ +public class GpxMapController extends JMapController implements MouseListener { + + private JGpxPanel panel; + private GpxEntry entry; + + public GpxMapController(PreviewMap map, JGpxPanel panel, boolean enabled) { + super(map, enabled); + this.panel = panel; + } + + public void mouseClicked(MouseEvent e) { + // Add new GPX point to currently selected GPX file + disable(); + if (e.getButton() == MouseEvent.BUTTON1) { + entry = panel.getSelectedEntry(); + Gpx gpx = entry.getLayer().getGpx(); + Point p = e.getPoint(); + Point tl = ((PreviewMap) map).getTopLeftCoordinate(); + p.x += tl.x; + p.y += tl.y; + MapSpace mapSpace = map.getMapSource().getMapSpace(); + int maxPixel = mapSpace.getMaxPixels(map.getZoom()); + if (p.x < 0 || p.x > maxPixel || p.y < 0 || p.y > maxPixel) + return; // outside of world region + double lon = mapSpace.cXToLon(p.x, map.getZoom()); + double lat = mapSpace.cYToLat(p.y, map.getZoom()); + String name = JOptionPane.showInputDialog(null, I18nUtils.localizedStringForKey("dlg_gpx_inpu_point_name")); + if (name == null) + return; + Gpx gpx11 = (Gpx) gpx; + WptType wpt = new WptType(); + wpt.setName(name); + wpt.setLat(new BigDecimal(lat)); + wpt.setLon(new BigDecimal(lon)); + GpxEditor editor = GpxEditor.getInstance(); + if (entry.getClass() == GpxRootEntry.class) { + gpx11.getWpt().add(wpt); + } else if (entry instanceof RteEntry) { + editor.findRteAndAdd((RteEntry) entry, wpt); + } else if (entry instanceof TrksegEntry) { + editor.findTrksegAndAdd((TrksegEntry) entry, wpt); + } + panel.addWpt(wpt, entry); + } + map.repaint(); + } + + public void repaint() { + map.repaint(); + } + + public void mouseEntered(MouseEvent e) { + + } + + public void mouseExited(MouseEvent e) { + + } + + public void mousePressed(MouseEvent e) { + + } + + public void mouseReleased(MouseEvent e) { + + } + + @Override + public void disable() { + super.disable(); + ((PreviewMap) map).getMapSelectionController().enable(); + } +} diff --git a/src/main/java/mobac/gui/mapview/controller/JMapController.java b/src/main/java/mobac/gui/mapview/controller/JMapController.java new file mode 100644 index 0000000..e77d8d1 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/JMapController.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +import java.awt.Point; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseWheelListener; + +import mobac.gui.mapview.PreviewMap; + +/** + * Abstract base class for all mouse controller implementations. For implementing your own controller create a class + * that derives from this one and implements one or more of the following interfaces: + *
    + *
  • {@link MouseListener}
  • + *
  • {@link MouseMotionListener}
  • + *
  • {@link MouseWheelListener}
  • + *
+ */ +public abstract class JMapController { + + protected final PreviewMap map; + protected boolean enabled = false; + + public JMapController(PreviewMap map) { + this.map = map; + } + + public JMapController(PreviewMap map, boolean enabled) { + this(map); + if (enabled) + enable(); + } + + public void enable() { + if (enabled) + return; + if (this instanceof MouseListener) + map.addMouseListener((MouseListener) this); + if (this instanceof MouseWheelListener) + map.addMouseWheelListener((MouseWheelListener) this); + if (this instanceof MouseMotionListener) + map.addMouseMotionListener((MouseMotionListener) this); + this.enabled = true; + } + + public void disable() { + if (!enabled) + return; + if (this instanceof MouseListener) + map.removeMouseListener((MouseListener) this); + if (this instanceof MouseWheelListener) + map.removeMouseWheelListener((MouseWheelListener) this); + if (this instanceof MouseMotionListener) + map.removeMouseMotionListener((MouseMotionListener) this); + this.enabled = false; + } + + protected Point convertToAbsolutePoint(Point p) { + Point mapPoint = map.getTopLeftCoordinate(); + mapPoint.x += p.getX(); + mapPoint.y += p.getY(); + mapPoint = map.getMapSource().getMapSpace().changeZoom(mapPoint, map.getZoom(), PreviewMap.MAX_ZOOM); + return mapPoint; + } +} diff --git a/src/main/java/mobac/gui/mapview/controller/MapKeyboardController.java b/src/main/java/mobac/gui/mapview/controller/MapKeyboardController.java new file mode 100644 index 0000000..73cfea8 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/MapKeyboardController.java @@ -0,0 +1,314 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.util.Timer; +import java.util.TimerTask; + +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.ComponentInputMap; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.KeyStroke; + +import mobac.gui.mapview.PreviewMap; + + +/** + * Implements the GUI logic for the preview map panel that manages the map + * movement by mouse and actions triggered by key strokes. + */ +public class MapKeyboardController extends JMapController { + + /** A Timer for smoothly moving the map area */ + private static final Timer timer = new Timer(true); + + /** Does the moving */ + private MoveTask moveTask = new MoveTask(); + + /** How often to do the moving (milliseconds) */ + private static long timerInterval = 20; + + /** The maximum speed (pixels per timer interval) */ + private static final double MAX_SPEED = 20; + + /** The speed increase per timer interval when a cursor button is clicked */ + private static final double ACCELERATION = 0.10; + + private final InputMap inputMap; + + public MapKeyboardController(PreviewMap map, boolean enabled) { + super(map); + + inputMap = new ComponentInputMap(map); + ActionMap actionMap = map.getActionMap(); + + // map moving + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "MOVE_RIGHT"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "MOVE_LEFT"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "MOVE_UP"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "MOVE_DOWN"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "STOP_MOVE_HORIZONTALLY"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "STOP_MOVE_HORIZONTALLY"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "STOP_MOVE_VERTICALLY"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "STOP_MOVE_VERTICALLY"); + + // zooming. To avoid confusion about which modifier key to use, + // we just add all keys left of the space bar + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.CTRL_DOWN_MASK, false), + "ZOOM_IN"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.META_DOWN_MASK, false), + "ZOOM_IN"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, InputEvent.ALT_DOWN_MASK, false), + "ZOOM_IN"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.CTRL_DOWN_MASK, false), + "ZOOM_OUT"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.META_DOWN_MASK, false), + "ZOOM_OUT"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, InputEvent.ALT_DOWN_MASK, false), + "ZOOM_OUT"); + + // map selection + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.CTRL_DOWN_MASK, false), + "PREVIOUS_MAP"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.META_DOWN_MASK, false), + "PREVIOUS_MAP"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, InputEvent.ALT_DOWN_MASK, false), + "PREVIOUS_MAP"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.CTRL_DOWN_MASK, false), + "NEXT_MAP"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.META_DOWN_MASK, false), + "NEXT_MAP"); + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, InputEvent.ALT_DOWN_MASK, false), + "NEXT_MAP"); + + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0, true), "REFRESH"); + + // action mapping + actionMap.put("MOVE_RIGHT", new MoveRightAction()); + actionMap.put("MOVE_LEFT", new MoveLeftAction()); + actionMap.put("MOVE_UP", new MoveUpAction()); + actionMap.put("MOVE_DOWN", new MoveDownAction()); + actionMap.put("STOP_MOVE_HORIZONTALLY", new StopMoveHorizontallyAction()); + actionMap.put("STOP_MOVE_VERTICALLY", new StopMoveVerticallyAction()); + actionMap.put("ZOOM_IN", new ZoomInAction()); + actionMap.put("ZOOM_OUT", new ZoomOutAction()); + actionMap.put("NEXT_MAP", new NextMapAction()); + actionMap.put("PREVIOUS_MAP", new PreviousMapAction()); + actionMap.put("REFRESH", new RefreshAction()); + if (enabled) + enable(); + } + + @Override + public void disable() { + map.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, new ComponentInputMap(map)); + } + + @Override + public void enable() { + map.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, inputMap); + } + + private class MoveRightAction extends AbstractAction { + private static final long serialVersionUID = -6758721144600926744L; + + public void actionPerformed(ActionEvent e) { + moveTask.setDirectionX(1); + } + } + + private class MoveLeftAction extends AbstractAction { + private static final long serialVersionUID = 2695221718338284951L; + + public void actionPerformed(ActionEvent e) { + moveTask.setDirectionX(-1); + } + } + + private class MoveUpAction extends AbstractAction { + private static final long serialVersionUID = -8414310977137213707L; + + public void actionPerformed(ActionEvent e) { + moveTask.setDirectionY(-1); + } + } + + private class MoveDownAction extends AbstractAction { + private static final long serialVersionUID = -5360890019457799681L; + + public void actionPerformed(ActionEvent e) { + moveTask.setDirectionY(1); + } + } + + private class StopMoveHorizontallyAction extends AbstractAction { + private static final long serialVersionUID = -5360890019457799681L; + + public void actionPerformed(ActionEvent e) { + moveTask.setDirectionX(0); + } + } + + private class StopMoveVerticallyAction extends AbstractAction { + private static final long serialVersionUID = -5360890019457799681L; + + public void actionPerformed(ActionEvent e) { + moveTask.setDirectionY(0); + } + } + + /** Moves the map depending on which cursor keys are pressed (or not) */ + private class MoveTask extends TimerTask { + /** The current x speed (pixels per timer interval) */ + private double speedX = 1; + + /** The current y speed (pixels per timer interval) */ + private double speedY = 1; + + /** The horizontal direction of movement, -1:left, 0:stop, 1:right */ + private int directionX = 0; + + /** The vertical direction of movement, -1:up, 0:stop, 1:down */ + private int directionY = 0; + + /** + * Indicated if moveTask is currently enabled (periodically + * executed via timer) or disabled + */ + protected boolean scheduled = false; + + protected void setDirectionX(int directionX) { + this.directionX = directionX; + updateScheduleStatus(); + } + + protected void setDirectionY(int directionY) { + this.directionY = directionY; + updateScheduleStatus(); + } + + private void updateScheduleStatus() { + boolean newMoveTaskState = !(directionX == 0 && directionY == 0); + + if (newMoveTaskState != scheduled) { + scheduled = newMoveTaskState; + if (newMoveTaskState) + timer.schedule(this, 0, timerInterval); + else { + // We have to create a new instance because rescheduling a + // once canceled TimerTask is not possible + moveTask = new MoveTask(); + cancel(); // Stop this TimerTask + } + } + } + + @Override + public void run() { + // update the x speed + switch (directionX) { + case -1: + if (speedX > -1) + speedX = -1; + if (speedX > -1 * MAX_SPEED) + speedX -= ACCELERATION; + break; + case 0: + speedX = 0; + break; + case 1: + if (speedX < 1) + speedX = 1; + if (speedX < MAX_SPEED) + speedX += ACCELERATION; + break; + } + + // update the y speed + switch (directionY) { + case -1: + if (speedY > -1) + speedY = -1; + if (speedY > -1 * MAX_SPEED) + speedY -= ACCELERATION; + break; + case 0: + speedY = 0; + break; + case 1: + if (speedY < 1) + speedY = 1; + if (speedY < MAX_SPEED) + speedY += ACCELERATION; + break; + } + + // move the map + int moveX = (int) Math.floor(speedX); + int moveY = (int) Math.floor(speedY); + if (moveX != 0 || moveY != 0) + map.moveMap(moveX, moveY); + } + } + + private class ZoomInAction extends AbstractAction { + private static final long serialVersionUID = 1471739991027644588L; + + public void actionPerformed(ActionEvent e) { + map.zoomIn(); + } + } + + private class ZoomOutAction extends AbstractAction { + private static final long serialVersionUID = 1471739991027644588L; + + public void actionPerformed(ActionEvent e) { + map.zoomOut(); + } + } + + private class PreviousMapAction extends AbstractAction { + private static final long serialVersionUID = -1492075614917423363L; + + public void actionPerformed(ActionEvent e) { + ((PreviewMap) map).selectPreviousMap(); + } + } + + private class NextMapAction extends AbstractAction { + private static final long serialVersionUID = -1491235614917423363L; + + public void actionPerformed(ActionEvent e) { + ((PreviewMap) map).selectNextMap(); + } + } + + private class RefreshAction extends AbstractAction { + + private static final long serialVersionUID = -7235666079485033823L; + + public void actionPerformed(ActionEvent e) { + ((PreviewMap) map).refreshMap(); + } + } + +} diff --git a/src/main/java/mobac/gui/mapview/controller/PolygonCircleSelectionMapController.java b/src/main/java/mobac/gui/mapview/controller/PolygonCircleSelectionMapController.java new file mode 100644 index 0000000..5246016 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/PolygonCircleSelectionMapController.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import mobac.gui.mapview.PreviewMap; + +/** + * Implements the GUI logic for the preview map panel that manages the map selection and actions triggered by key + * strokes. + * + */ +public class PolygonCircleSelectionMapController extends AbstractPolygonSelectionMapController implements + MouseMotionListener, MouseListener { + + private static final int POLYGON_POINTS = 16; + private static final double ANGLE_PART = Math.PI * 2.0 / POLYGON_POINTS; + + private Point center; + + public PolygonCircleSelectionMapController(PreviewMap map) { + super(map); + } + + public void mouseClicked(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + center = convertToAbsolutePoint(e.getPoint()); + polygonPoints.ensureCapacity(POLYGON_POINTS); + } + } + + public void mouseDragged(MouseEvent e) { + if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK) { + if (center != null) { + Point circlePoint = convertToAbsolutePoint(e.getPoint()); + double radius = circlePoint.distance(center); + polygonPoints.clear(); + for (int i = 0; i < POLYGON_POINTS; i++) { + double angle = ANGLE_PART * i; + int y = (int) Math.round(Math.sin(angle) * radius); + int x = (int) Math.round(Math.cos(angle) * radius); + polygonPoints.add(new Point(center.x + x, center.y + y)); + } + map.grabFocus(); + map.repaint(); + } + } + } + + public void mouseMoved(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + +} diff --git a/src/main/java/mobac/gui/mapview/controller/PolygonSelectionMapController.java b/src/main/java/mobac/gui/mapview/controller/PolygonSelectionMapController.java new file mode 100644 index 0000000..d1a04d3 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/PolygonSelectionMapController.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import mobac.gui.mapview.PreviewMap; + +/** + * Implements the GUI logic for the preview map panel that manages the map selection and actions triggered by key + * strokes. + * + */ +public class PolygonSelectionMapController extends AbstractPolygonSelectionMapController implements MouseListener { + + public PolygonSelectionMapController(PreviewMap map) { + super(map); + } + + public void mouseClicked(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + + } + + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + if (finished) + reset(); + Point mapPoint = map.getTopLeftCoordinate(); + mapPoint.x += e.getX(); + mapPoint.y += e.getY(); + mapPoint = map.getMapSource().getMapSpace().changeZoom(mapPoint, map.getZoom(), PreviewMap.MAX_ZOOM); + polygonPoints.add(mapPoint); + } + map.grabFocus(); + map.repaint(); + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + +} diff --git a/src/main/java/mobac/gui/mapview/controller/RectangleSelectionMapController.java b/src/main/java/mobac/gui/mapview/controller/RectangleSelectionMapController.java new file mode 100644 index 0000000..8ce3773 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/controller/RectangleSelectionMapController.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.controller; + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.layer.RectangleSelectionLayer; + +/** + * Implements the GUI logic for the preview map panel that manages the map selection and actions triggered by key + * strokes. + * + */ +public class RectangleSelectionMapController extends JMapController implements MouseMotionListener, MouseListener { + + /** + * start point of selection rectangle in absolute tile coordinated regarding {@link JMapViewer#MAX_ZOOM} + */ + private Point iStartSelectionPoint; + + /** + * end point of selection rectangle in absolute tile coordinated regarding {@link JMapViewer#MAX_ZOOM} + */ + private Point iEndSelectionPoint; + + protected RectangleSelectionLayer mapLayer; + + public RectangleSelectionMapController(PreviewMap map) { + super(map, false); + mapLayer = new RectangleSelectionLayer(this); + } + + @Override + public void enable() { + super.enable(); + // map.mapLayers.add(mapLayer); + } + + @Override + public void disable() { + map.mapLayers.remove(mapLayer); + map.setSelectionByTileCoordinate(null, null, true); + super.disable(); + } + + /** + * Start drawing the selection rectangle if it was the 1st button (left button) + */ + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + iStartSelectionPoint = convertToAbsolutePoint(e.getPoint()); + iEndSelectionPoint = convertToAbsolutePoint(e.getPoint()); + } + map.grabFocus(); + } + + public void mouseDragged(MouseEvent e) { + if ((e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == MouseEvent.BUTTON1_DOWN_MASK) { + if (iStartSelectionPoint != null) { + iEndSelectionPoint = convertToAbsolutePoint(e.getPoint()); + map.setSelectionByTileCoordinate(PreviewMap.MAX_ZOOM, iStartSelectionPoint, iEndSelectionPoint, true); + } + } + } + + /** + * When dragging the map change the cursor back to it's pre-move cursor. If a double-click occurs center and zoom + * the map on the clicked location. + */ + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + if (e.getClickCount() == 1) { + map.setSelectionByTileCoordinate(PreviewMap.MAX_ZOOM, iStartSelectionPoint, + convertToAbsolutePoint(e.getPoint()), true); + } + } + map.grabFocus(); + } + + public void mouseMoved(MouseEvent e) { + } + + public void mouseClicked(MouseEvent e) { + map.grabFocus(); + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public Point getiStartSelectionPoint() { + return iStartSelectionPoint; + } + + public Point getiEndSelectionPoint() { + return iEndSelectionPoint; + } + + public RectangleSelectionLayer getMapLayer() { + return mapLayer; + } + + public PreviewMap getMap() { + return map; + } + +} diff --git a/src/main/java/mobac/gui/mapview/interfaces/MapEventListener.java b/src/main/java/mobac/gui/mapview/interfaces/MapEventListener.java new file mode 100644 index 0000000..eb346ee --- /dev/null +++ b/src/main/java/mobac/gui/mapview/interfaces/MapEventListener.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.interfaces; + +import mobac.gui.mapview.controller.JMapController; +import mobac.program.interfaces.MapSource; +import mobac.program.model.MercatorPixelCoordinate; + +public interface MapEventListener { + + /** the selection changed */ + public void selectionChanged(MercatorPixelCoordinate max, MercatorPixelCoordinate min); + + /** the zoom changed */ + public void zoomChanged(int newZoomLevel); + + /** the grid zoom changed */ + public void gridZoomChanged(int newGridZoomLevel); + + /** select the next map source from the map list */ + public void selectNextMapSource(); + + /** select the previous map source from the map list */ + public void selectPreviousMapSource(); + + public void mapSourceChanged(MapSource newMapSource); + + public void mapSelectionControllerChanged(JMapController newMapController); +} diff --git a/src/main/java/mobac/gui/mapview/interfaces/MapLayer.java b/src/main/java/mobac/gui/mapview/interfaces/MapLayer.java new file mode 100644 index 0000000..2ca0ee0 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/interfaces/MapLayer.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.interfaces; + +import java.awt.Graphics2D; + +import mobac.gui.mapview.JMapViewer; + +/** + * General purpose map layer + */ +public interface MapLayer { + + /** + * + * @param map + * @param g + * @param zoom + * current zoom level + * @param minX + * top left x coordinate of the visible map region + * @param minYtop + * left y coordinate of the visible map region + * @param maxX + * bottom right x coordinate of the visible map region + * @param maxY + * bottom right y coordinate of the visible map region + */ + public void paint(JMapViewer map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY); + +} diff --git a/src/main/java/mobac/gui/mapview/interfaces/MapTileLayer.java b/src/main/java/mobac/gui/mapview/interfaces/MapTileLayer.java new file mode 100644 index 0000000..9cc9d25 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/interfaces/MapTileLayer.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.interfaces; + +import java.awt.Graphics; + +import mobac.program.interfaces.MapSource; + +public interface MapTileLayer { + + public void startPainting(MapSource mapSource); + + /** + * Paints the tile identified by tilex/tiley/ + * zoom onto the {@link Graphics} g with it's + * upper left corner at gx/gy. The size of each + * tile has to be 256 pixel x 256 pixel. + * + * @param g + * @param gx + * @param gy + * @param tilex + * @param tiley + */ + public void paintTile(Graphics g, int gx, int gy, int tilex, int tiley, int zoom); +} diff --git a/src/main/java/mobac/gui/mapview/interfaces/TileLoaderListener.java b/src/main/java/mobac/gui/mapview/interfaces/TileLoaderListener.java new file mode 100644 index 0000000..3a87b40 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/interfaces/TileLoaderListener.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.interfaces; + +import mobac.gui.mapview.MemoryTileCache; +import mobac.gui.mapview.Tile; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +public interface TileLoaderListener { + + /** + * Will be called if a new {@link Tile} has been loaded successfully. + * Loaded can mean downloaded or loaded from file cache. + * + * @param tile + */ + public void tileLoadingFinished(Tile tile, boolean success); + + public MemoryTileCache getTileImageCache(); +} diff --git a/src/main/java/mobac/gui/mapview/layer/DefaultMapTileLayer.java b/src/main/java/mobac/gui/mapview/layer/DefaultMapTileLayer.java new file mode 100644 index 0000000..2b56288 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/DefaultMapTileLayer.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Graphics; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.Tile; +import mobac.gui.mapview.Tile.TileState; +import mobac.gui.mapview.interfaces.MapTileLayer; +import mobac.program.interfaces.MapSource; + +public class DefaultMapTileLayer implements MapTileLayer { + + protected JMapViewer mapViewer; + + protected MapSource mapSource; + + protected boolean usePlaceHolders; + + public DefaultMapTileLayer(JMapViewer mapViewer, MapSource mapSource) { + this.mapViewer = mapViewer; + this.mapSource = mapSource; + } + + public void startPainting(MapSource mapSource) { + usePlaceHolders = mapViewer.isUsePlaceHolderTiles(); + } + + public void paintTile(Graphics g, int gx, int gy, int tilex, int tiley, int zoom) { + Tile tile = getTile(tilex, tiley, zoom); + if (tile == null) + return; + tile.paint(g, gx, gy); + } + + /** + * retrieves a tile from the cache. If the tile is not present in the cache a load job is added to the working queue + * of {@link JobThread}. + * + * @param tilex + * @param tiley + * @param zoom + * @return specified tile from the cache or null if the tile was not found in the cache. + */ + protected Tile getTile(int tilex, int tiley, int zoom) { + int max = (1 << zoom); + if (tilex < 0 || tilex >= max || tiley < 0 || tiley >= max) + return null; + Tile tile = mapViewer.getTileImageCache().getTile(mapSource, tilex, tiley, zoom); + if (tile == null) { + tile = new Tile(mapSource, tilex, tiley, zoom); + mapViewer.getTileImageCache().addTile(tile); + if (usePlaceHolders) + tile.loadPlaceholderFromCache(mapViewer.getTileImageCache()); + } + if (tile.getTileState() == TileState.TS_NEW) { + mapViewer.getJobDispatcher().addJob( + mapViewer.getTileLoader().createTileLoaderJob(mapSource, tilex, tiley, zoom)); + } + return tile; + + } + +} diff --git a/src/main/java/mobac/gui/mapview/layer/GpxLayer.java b/src/main/java/mobac/gui/mapview/layer/GpxLayer.java new file mode 100644 index 0000000..0909cea --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/GpxLayer.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Stroke; +import java.io.File; + +import mobac.data.gpx.gpx11.Gpx; +import mobac.data.gpx.gpx11.RteType; +import mobac.data.gpx.gpx11.TrkType; +import mobac.data.gpx.gpx11.TrksegType; +import mobac.data.gpx.gpx11.WptType; +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.gui.panels.JGpxPanel; +import mobac.program.interfaces.MapSpace; + + +/** + * A {@link MapLayer} displaying the content of a loaded GPX file in a {@link JMapViewer} instance. + */ +public class GpxLayer implements MapLayer { + + private static int POINT_RADIUS = 4; + private static int POINT_DIAMETER = 2 * POINT_RADIUS; + + private Color wptPointColor = new Color(0, 0, 200); + private Color trkPointColor = Color.RED; + private Color rtePointColor = new Color(0, 200, 0); + + private Stroke outlineStroke = new BasicStroke(1); + + private Stroke lineStroke = new BasicStroke(2.0f); + + // private Logger log = Logger.getLogger(GpxLayer.class); + + /** the associated gpx file handle */ + private File file; + /** the associated gpx object */ + private final Gpx gpx; + /** the associated panel that displays the nodes of the gpx file */ + private JGpxPanel panel; + + private boolean showWaypoints = true; + private boolean showWaypointName = true; + private boolean showTracks = true; + private boolean showRoutes = true; + + private int lastTrackPointX = Integer.MIN_VALUE; + private int lastTrackPointY = Integer.MIN_VALUE; + + public GpxLayer(Gpx gpx) { + this.gpx = gpx; + } + + public void paint(JMapViewer map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + g.setColor(wptPointColor); + final MapSpace mapSpace = map.getMapSource().getMapSpace(); + if (showWaypoints) { + for (WptType pt : gpx.getWpt()) { + paintPoint(pt, wptPointColor, g, showWaypointName, mapSpace, zoom, minX, minY, maxX, maxY); + } + } + if (showTracks) { + for (TrkType trk : gpx.getTrk()) { + for (TrksegType seg : trk.getTrkseg()) { + lastTrackPointX = Integer.MIN_VALUE; + lastTrackPointY = Integer.MIN_VALUE; + for (WptType pt : seg.getTrkpt()) { + paintTrack(pt, trkPointColor, g, mapSpace, zoom, minX, minY, maxX, maxY); + } + } + } + } + if (showRoutes) { + for (RteType rte : gpx.getRte()) { + lastTrackPointX = Integer.MIN_VALUE; + lastTrackPointY = Integer.MIN_VALUE; + for (WptType pt : rte.getRtept()) { + paintTrack(pt, rtePointColor, g, mapSpace, zoom, minX, minY, maxX, maxY); + } + } + } + } + + private boolean paintPoint(final WptType point, Color color, final Graphics2D g, boolean paintPointName, + MapSpace mapSpace, int zoom, int minX, int minY, int maxX, int maxY) { + int x = mapSpace.cLonToX(point.getLon().doubleValue(), zoom); + if (x < minX || x > maxX) + return false; // Point outside of visible region + int y = mapSpace.cLatToY(point.getLat().doubleValue(), zoom); + if (y < minY || y > maxY) + return false; // Point outside of visible region + x -= minX; + y -= minY; + g.setColor(color); + g.fillOval(x - POINT_RADIUS, y - POINT_RADIUS, POINT_DIAMETER, POINT_DIAMETER); + g.setColor(Color.BLACK); + g.setStroke(outlineStroke); + g.drawOval(x - POINT_RADIUS, y - POINT_RADIUS, POINT_DIAMETER, POINT_DIAMETER); + if (paintPointName && point.getName() != null) + g.drawString(point.getName(), x + POINT_RADIUS + 5, y - POINT_RADIUS); + + return true; + } + + private boolean paintTrack(final WptType point, Color color, final Graphics2D g, MapSpace mapSpace, int zoom, + int minX, int minY, int maxX, int maxY) { + // Absolute map space coordinates + int xAbs = mapSpace.cLonToX(point.getLon().doubleValue(), zoom); + int yAbs = mapSpace.cLatToY(point.getLat().doubleValue(), zoom); + // Relative coordinates regarding the top left point on map + int x = xAbs - minX; + int y = yAbs - minY; + g.setColor(color); + if (lastTrackPointX != Integer.MIN_VALUE && lastTrackPointY != Integer.MIN_VALUE) { + g.setStroke(lineStroke); + g.drawLine(lastTrackPointX, lastTrackPointY, x, y); + } + lastTrackPointX = x; + lastTrackPointY = y; + return true; + } + + /** + * The associated gpx object + * + * @return + */ + public Gpx getGpx() { + return gpx; + } + + public void setPanel(JGpxPanel panel) { + this.panel = panel; + } + + public JGpxPanel getPanel() { + return panel; + } + + public void setFile(File file) { + this.file = file; + } + + /** + * The associated gpx file handle + * + * @return + */ + public File getFile() { + return file; + } + +} diff --git a/src/main/java/mobac/gui/mapview/layer/MapAreaHighlightingLayer.java b/src/main/java/mobac/gui/mapview/layer/MapAreaHighlightingLayer.java new file mode 100644 index 0000000..4e8e02b --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/MapAreaHighlightingLayer.java @@ -0,0 +1,189 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Polygon; +import java.util.Iterator; + +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; + +import mobac.gui.MainGUI; +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.AtlasObject; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.model.MapPolygon; + +public class MapAreaHighlightingLayer implements MapLayer, TreeModelListener { + + private final JAtlasTree tree; + + private TreeSelectionListener treeListener; + + private AtlasObject object; + + public static void removeHighlightingLayers() { + PreviewMap previewMap = MainGUI.getMainGUI().previewMap; + Iterator mapLayers = previewMap.mapLayers.iterator(); + MapLayer ml; + while (mapLayers.hasNext()) { + ml = mapLayers.next(); + if (ml instanceof MapAreaHighlightingLayer) { + mapLayers.remove(); + ((MapAreaHighlightingLayer) ml).unregisterTreeListener(); + } + } + } + + public MapAreaHighlightingLayer(AtlasObject atlasObject) { + tree = null; + treeListener = null; + this.object = atlasObject; + } + + public MapAreaHighlightingLayer(JAtlasTree tree) { + this.tree = tree; + object = (AtlasObject) tree.getSelectionPath().getLastPathComponent(); + MainGUI.getMainGUI().previewMap.repaint(); + treeListener = new TreeSelectionListener() { + + public void valueChanged(TreeSelectionEvent event) { + try { + object = (AtlasObject) event.getNewLeadSelectionPath().getLastPathComponent(); + } catch (Exception e) { + object = null; + } + MainGUI.getMainGUI().previewMap.repaint(); + } + }; + tree.addTreeSelectionListener(treeListener); + tree.getModel().addTreeModelListener(this); + } + + public void paint(JMapViewer mapViewer, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + if (object == null) + return; + if (object instanceof AtlasInterface) { + for (LayerInterface layer : (AtlasInterface) object) { + for (MapInterface map : layer) { + paintMap(map, g, zoom, minX, minY, maxX, maxY); + } + } + } else if (object instanceof LayerInterface) { + for (MapInterface map : (LayerInterface) object) { + paintMap(map, g, zoom, minX, minY, maxX, maxY); + } + } else { + paintMap((MapInterface) object, g, zoom, minX, minY, maxX, maxY); + } + } + + protected void paintMap(MapInterface map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + if (map instanceof MapPolygon) + paintMapPolygon((MapPolygon) map, g, zoom, minX, minY, maxX, maxY); + else + paintMapRectangle(map, g, zoom, minX, minY, maxX, maxY); + } + + protected void paintMapRectangle(MapInterface map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + Point max = map.getMaxTileCoordinate(); + Point min = map.getMinTileCoordinate(); + int zoomDiff = map.getZoom() - zoom; + int mapX = applyZoomDiff(min.x, zoomDiff); + int mapY = applyZoomDiff(min.y, zoomDiff); + int mapW = applyZoomDiff(max.x - min.x + 1, zoomDiff); + int mapH = applyZoomDiff(max.y - min.y + 1, zoomDiff); + int x = mapX - minX; + int y = mapY - minY; + int w = mapW; + int h = mapH; + g.setColor(PreviewMap.MAP_COLOR); + g.fillRect(x, y, w, h); + g.setColor(PreviewMap.GRID_COLOR); + g.drawRect(x, y, w, h); + } + + protected void paintMapPolygon(MapPolygon map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + Polygon p = map.getPolygon(); + int zoomDiff = map.getZoom() - zoom; + + int[] px = new int[p.npoints]; + int[] py = new int[p.npoints]; + for (int i = 0; i < px.length; i++) { + px[i] = applyZoomDiff(p.xpoints[i], zoomDiff) - minX; + py[i] = applyZoomDiff(p.ypoints[i], zoomDiff) - minY; + } + g.setColor(PreviewMap.MAP_COLOR); + g.fillPolygon(px, py, px.length); + } + + private static int applyZoomDiff(int pixelCoord, int zoomDiff) { + return (zoomDiff > 0) ? pixelCoord >> zoomDiff : pixelCoord << -zoomDiff; + } + + protected void unregisterTreeListener() { + if (treeListener == null) + return; + try { + tree.getModel().removeTreeModelListener(this); + tree.removeTreeSelectionListener(treeListener); + treeListener = null; + } catch (Exception e) { + } + } + + @Override + protected void finalize() throws Throwable { + unregisterTreeListener(); + super.finalize(); + } + + public void treeNodesChanged(TreeModelEvent e) { + MainGUI.getMainGUI().previewMap.repaint(); + } + + public void treeNodesInserted(TreeModelEvent e) { + MainGUI.getMainGUI().previewMap.repaint(); + } + + public void treeNodesRemoved(TreeModelEvent e) { + MainGUI.getMainGUI().previewMap.repaint(); + } + + public void treeStructureChanged(TreeModelEvent e) { + MainGUI.getMainGUI().previewMap.repaint(); + } + + public AtlasObject getObject() { + return object; + } + + public void setObject(AtlasObject object) { + this.object = object; + } + + +} diff --git a/src/main/java/mobac/gui/mapview/layer/MapGridLayer.java b/src/main/java/mobac/gui/mapview/layer/MapGridLayer.java new file mode 100644 index 0000000..aee7a97 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/MapGridLayer.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Graphics; + +import mobac.gui.mapview.interfaces.MapTileLayer; +import mobac.program.interfaces.MapSource; + + +/** + * A simple layer that paints the tile borders. + */ +public class MapGridLayer implements MapTileLayer { + + protected int tileSize; + + public void startPainting(MapSource mapSource) { + tileSize = mapSource.getMapSpace().getTileSize(); + } + + public void paintTile(Graphics g, int gx, int gy, int tilex, int tiley, int zoom) { + g.drawRect(gx, gy, tileSize, tileSize); + } + +} diff --git a/src/main/java/mobac/gui/mapview/layer/OverlayMapTileLayer.java b/src/main/java/mobac/gui/mapview/layer/OverlayMapTileLayer.java new file mode 100644 index 0000000..ba14bd9 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/OverlayMapTileLayer.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Graphics; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.Tile; +import mobac.gui.mapview.Tile.TileState; +import mobac.gui.mapview.interfaces.MapTileLayer; +import mobac.program.interfaces.MapSource; + +public class OverlayMapTileLayer implements MapTileLayer { + + protected JMapViewer mapViewer; + protected MapSource mapSource; + + public OverlayMapTileLayer(JMapViewer mapViewer, MapSource tileSource) { + this.mapViewer = mapViewer; + this.mapSource = tileSource; + } + + public void startPainting(MapSource mapSource) { + } + + public void paintTile(Graphics g, int gx, int gy, int tilex, int tiley, int zoom) { + Tile tile = getTile(tilex, tiley, zoom); + if (tile == null) + return; + tile.paintTransparent(g, gx, gy); + } + + /** + * retrieves a tile from the cache. If the tile is not present in the cache a load job is added to the working queue + * of {@link JobThread}. + * + * @param tilex + * @param tiley + * @param zoom + * @return specified tile from the cache or null if the tile was not found in the cache. + */ + protected Tile getTile(int tilex, int tiley, int zoom) { + int max = (1 << zoom); + if (tilex < 0 || tilex >= max || tiley < 0 || tiley >= max) + return null; + Tile tile = mapViewer.getTileImageCache().getTile(mapSource, tilex, tiley, zoom); + if (tile == null) { + tile = new Tile(mapSource, tilex, tiley, zoom); + mapViewer.getTileImageCache().addTile(tile); + } + if (tile.getTileState() == TileState.TS_NEW) { + mapViewer.getJobDispatcher().addJob( + mapViewer.getTileLoader().createTileLoaderJob(mapSource, tilex, tiley, zoom)); + } + return tile; + } + +} diff --git a/src/main/java/mobac/gui/mapview/layer/PolygonSelectionLayer.java b/src/main/java/mobac/gui/mapview/layer/PolygonSelectionLayer.java new file mode 100644 index 0000000..e71db70 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/PolygonSelectionLayer.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.AffineTransform; +import java.util.List; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.AbstractPolygonSelectionMapController; +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.program.interfaces.MapSpace; + +/** + * Displays a polygon on the map - only for testing purposes + */ +public class PolygonSelectionLayer implements MapLayer { + + private final AbstractPolygonSelectionMapController mapController; + + public PolygonSelectionLayer(AbstractPolygonSelectionMapController mapController) { + this.mapController = mapController; + } + + public void paint(JMapViewer map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + MapSpace mapSpace = map.getMapSource().getMapSpace(); + g.setColor(Color.RED); + Point lastPoint = null; + List pointList = mapController.getPolygonPoints(); + if (pointList.size() == 0) + return; + AffineTransform at = g.getTransform(); + try { + g.translate(-minX, -minY); + for (Point p : pointList) { + Point p1 = mapSpace.changeZoom(p, PreviewMap.MAX_ZOOM, zoom); + g.fillOval(p1.x - 3, p1.y - 3, 6, 6); + if (lastPoint != null) { + g.drawLine(p1.x, p1.y, lastPoint.x, lastPoint.y); + } + lastPoint = p1; + } + // Draw line back to the starting point + Point p1 = mapSpace.changeZoom(pointList.get(0), PreviewMap.MAX_ZOOM, zoom); + g.drawLine(p1.x, p1.y, lastPoint.x, lastPoint.y); + } finally { + g.setTransform(at); + } + } + +} diff --git a/src/main/java/mobac/gui/mapview/layer/RectangleSelectionLayer.java b/src/main/java/mobac/gui/mapview/layer/RectangleSelectionLayer.java new file mode 100644 index 0000000..aea3624 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/RectangleSelectionLayer.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.AffineTransform; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.RectangleSelectionMapController; +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.program.interfaces.MapSpace; + +/** + * Displays a polygon on the map - only for testing purposes + */ +public class RectangleSelectionLayer implements MapLayer { + + private final RectangleSelectionMapController mapController; + + public RectangleSelectionLayer(RectangleSelectionMapController rectangleSelectionMapController) { + this.mapController = rectangleSelectionMapController; + } + + public void paint(JMapViewer map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + MapSpace mapSpace = map.getMapSource().getMapSpace(); + g.setColor(Color.BLUE); + Point p1 = mapController.getiStartSelectionPoint(); + Point p2 = mapController.getiEndSelectionPoint(); + if (p1 == null || p2 == null) + return; + p1 = mapSpace.changeZoom(p1, PreviewMap.MAX_ZOOM, zoom); + p2 = mapSpace.changeZoom(p2, PreviewMap.MAX_ZOOM, zoom); + + int x = Math.min(p1.x, p2.x); + int y = Math.min(p1.y, p2.y); + int w = Math.abs(p1.x - p2.x); + int h = Math.abs(p1.y - p2.y); + + AffineTransform at = g.getTransform(); + try { + g.translate(-minX, -minY); + g.drawRect(x, y, w, h); + } finally { + g.setTransform(at); + } + } +} diff --git a/src/main/java/mobac/gui/mapview/layer/ShapeLayer.java b/src/main/java/mobac/gui/mapview/layer/ShapeLayer.java new file mode 100644 index 0000000..68c3b0f --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/ShapeLayer.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; + +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.interfaces.MapLayer; + +/** + * Displays a polygon on the map - only for testing purposes + */ +public class ShapeLayer implements MapLayer { + + private Color color = new Color(0f, 1f, 0f, 0.5f); + + private int calculationZoom; + private Shape shape;; + + public ShapeLayer(Shape shape, int zoom) { + this.shape = shape; + } + + public void paint(JMapViewer map, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + AffineTransform af = g.getTransform(); + g.translate(-minX, -minY); + double scale; + if (zoom < calculationZoom) + scale = 1d / (1 << (calculationZoom - zoom)); + else + scale = 1 << (zoom - calculationZoom); + g.scale(scale, scale); + g.setColor(color); + g.fill(shape); + g.setTransform(af); + } + +} diff --git a/src/main/java/mobac/gui/mapview/layer/TileStoreCoverageLayer.java b/src/main/java/mobac/gui/mapview/layer/TileStoreCoverageLayer.java new file mode 100644 index 0000000..40127d6 --- /dev/null +++ b/src/main/java/mobac/gui/mapview/layer/TileStoreCoverageLayer.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.mapview.layer; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.util.Iterator; + +import javax.swing.JOptionPane; + +import mobac.gui.MainGUI; +import mobac.gui.dialogs.WorkinprogressDialog; +import mobac.gui.mapview.JMapViewer; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.interfaces.MapLayer; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.berkeleydb.DelayedInterruptThread; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; + +public class TileStoreCoverageLayer implements MapLayer { + + private final MapSource mapSource; + private final int zoom; + private final Point pixelCoordinateMin; + private final Point pixelCoordinateMax; + private final Point tileNumMin; + private final Point tileNumMax; + private BufferedImage coverageImage = null; + + public static void removeCacheCoverageLayers() { + try { + PreviewMap previewMap = MainGUI.getMainGUI().previewMap; + Iterator mapLayers = previewMap.mapLayers.iterator(); + MapLayer ml; + while (mapLayers.hasNext()) { + ml = mapLayers.next(); + if (ml instanceof TileStoreCoverageLayer) { + mapLayers.remove(); + } + } + } catch (Exception e) { + } + } + + public TileStoreCoverageLayer(PreviewMap mapViewer, MapSource mapSource, int zoom) { + this.mapSource = mapSource; + this.zoom = zoom; + + MapSpace mapSpace = mapSource.getMapSpace(); + int tileSize = mapSpace.getTileSize(); + int mapViewerZoom = mapViewer.getZoom(); + Point min = mapViewer.getTopLeftCoordinate(); + Point max = new Point(min.x + mapViewer.getWidth(), min.y + mapViewer.getHeight()); + min = mapSpace.changeZoom(min, mapViewerZoom, zoom); + max = mapSpace.changeZoom(max, mapViewerZoom, zoom); + + tileNumMax = new Point(max.x / tileSize, max.y / tileSize); + tileNumMin = new Point(min.x / tileSize, min.y / tileSize); + pixelCoordinateMax = new Point(tileNumMax.x * tileSize + tileSize - 1, tileNumMax.y * tileSize + tileSize - 1); + pixelCoordinateMin = new Point(tileNumMin.x * tileSize, tileNumMin.y * tileSize); + updateCoverageImage(); + } + + private void updateCoverageImage() { + coverageImage = null; + Runnable r = new Runnable() { + + public void run() { + try { + coverageImage = TileStore.getInstance().getCacheCoverage(mapSource, zoom, tileNumMin, tileNumMax); + if (coverageImage == null) + JOptionPane.showMessageDialog(MainGUI.getMainGUI(), + I18nUtils.localizedStringForKey("msg_tile_store_failed_retrieve_coverage"), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.ERROR_MESSAGE); + } catch (InterruptedException e) { + } catch (Exception e) { + GUIExceptionHandler.processException(e); + } + if (coverageImage == null) + removeCacheCoverageLayers(); + MainGUI.getMainGUI().previewMap.repaint(); + } + }; + WorkinprogressDialog dialog = new WorkinprogressDialog(MainGUI.getMainGUI(), "Loading coverage data", + DelayedInterruptThread.createThreadFactory()); + dialog.startWork(r); + } + + public void paint(JMapViewer mapViewer, Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + if (coverageImage == null) + return; + paintCoverage(g, zoom, minX, minY, maxX, maxY); + } + + protected void paintCoverage(Graphics2D g, int zoom, int minX, int minY, int maxX, int maxY) { + Point max = pixelCoordinateMax; + Point min = pixelCoordinateMin; + MapSpace mapSpace = mapSource.getMapSpace(); + int mapX = mapSpace.xChangeZoom(min.x, this.zoom, zoom); + int mapY = mapSpace.yChangeZoom(min.y, this.zoom, zoom); + int mapW = mapSpace.xChangeZoom(max.x - min.x + 1, this.zoom, zoom); + int mapH = mapSpace.yChangeZoom(max.y - min.y + 1, this.zoom, zoom); + int x = mapX - minX; + int y = mapY - minY; + int w = mapW; + int h = mapH; + g.drawImage(coverageImage, x, y, w, h, null); + } + +} diff --git a/src/main/java/mobac/gui/panels/JCoordinatesPanel.java b/src/main/java/mobac/gui/panels/JCoordinatesPanel.java new file mode 100644 index 0000000..7b9f41a --- /dev/null +++ b/src/main/java/mobac/gui/panels/JCoordinatesPanel.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.panels; + +import java.awt.BorderLayout; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.ParseException; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JLayeredPane; +import javax.swing.JMenuItem; +import javax.swing.JPanel; + +import mobac.gui.components.FilledLayeredPane; +import mobac.gui.components.JCollapsiblePanel; +import mobac.gui.components.JCoordinateField; +import mobac.gui.components.JDropDownButton; +import mobac.program.interfaces.MapSource; +import mobac.program.model.CoordinateStringFormat; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.MapSelection; +import mobac.program.model.MercatorPixelCoordinate; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; + +/** + * Encapsulates all interface components and code for the panel that shows the coordinates of the current selection and + * allows the user to enter own coordinates. + */ +public class JCoordinatesPanel extends JCollapsiblePanel { + + private static final long serialVersionUID = 1L; + + public static final String NAME = "Coordinates";//用于持久化 + + private JCoordinateField latMinTextField; + private JCoordinateField latMaxTextField; + private JCoordinateField lonMinTextField; + private JCoordinateField lonMaxTextField; + private JButton applySelectionButton; + + private CoordinateStringFormat csf = CoordinateStringFormat.DEG_ENG; + + public JCoordinatesPanel() { + super(I18nUtils.localizedStringForKey("lp_coords_title"), new GridBagLayout()); + setName(NAME); + // coordinates panel + latMaxTextField = new JCoordinateField(MapSelection.LAT_MIN, MapSelection.LAT_MAX); + latMaxTextField.setActionCommand("latMaxTextField"); + lonMinTextField = new JCoordinateField(MapSelection.LON_MIN, MapSelection.LON_MAX); + lonMinTextField.setActionCommand("longMinTextField"); + lonMaxTextField = new JCoordinateField(MapSelection.LON_MIN, MapSelection.LON_MAX); + lonMaxTextField.setActionCommand("longMaxTextField"); + latMinTextField = new JCoordinateField(MapSelection.LAT_MIN, MapSelection.LAT_MAX); + latMinTextField.setActionCommand("latMinTextField"); + + applySelectionButton = new JButton(I18nUtils.localizedStringForKey("lp_coords_select_btn_title")); + + JLabel latMaxLabel = new JLabel(I18nUtils.localizedStringForKey("lp_coords_label_N"), JLabel.CENTER); + JLabel lonMinLabel = new JLabel(I18nUtils.localizedStringForKey("lp_coords_label_W"), JLabel.CENTER); + JLabel lonMaxLabel = new JLabel(I18nUtils.localizedStringForKey("lp_coords_label_E"), JLabel.CENTER); + JLabel latMinLabel = new JLabel(I18nUtils.localizedStringForKey("lp_coords_label_S"), JLabel.CENTER); + + JPanel northPanel = new JPanel(new BorderLayout()); + JLayeredPane layeredPane = new FilledLayeredPane(); + + JPanel northInnerPanel = new JPanel(); + northInnerPanel.add(latMaxLabel); + northInnerPanel.add(latMaxTextField); + + JPanel formatButtonPanel = new JPanel(null); + formatButtonPanel.setOpaque(false); + JDropDownButton formatButton = new JDropDownButton(I18nUtils.localizedStringForKey("lp_coords_fmt_list_title")); + formatButton.setMargin(new Insets(0, 5, 0, 0)); + formatButton.setBounds(2, 2, 55, 20); + formatButtonPanel.add(formatButton); + for (CoordinateStringFormat csf : CoordinateStringFormat.values()) + formatButton.addDropDownItem(new JNumberFormatMenuItem(csf)); + + layeredPane.add(northInnerPanel, Integer.valueOf(0)); + layeredPane.setMinimumSize(northInnerPanel.getMinimumSize()); + layeredPane.setPreferredSize(northInnerPanel.getPreferredSize()); + layeredPane.add(formatButtonPanel, Integer.valueOf(2)); + northPanel.add(layeredPane, BorderLayout.CENTER); + + // northPanel.add(northInnerPanel, BorderLayout.CENTER); + contentContainer.add(northPanel, GBC.eol().fillH().insets(0, 5, 0, 0)); + + JPanel eastWestPanel = new JPanel(new GridBagLayout()); + eastWestPanel.add(lonMinLabel, GBC.std()); + eastWestPanel.add(lonMinTextField, GBC.std()); + eastWestPanel.add(lonMaxLabel, GBC.std().insets(10, 0, 0, 0)); + eastWestPanel.add(lonMaxTextField, GBC.std()); + contentContainer.add(eastWestPanel, GBC.eol().fill()); + + JPanel southPanel = new JPanel(); + southPanel.add(latMinLabel); + southPanel.add(latMinTextField); + contentContainer.add(southPanel, GBC.eol().anchor(GBC.CENTER)); + contentContainer.add(applySelectionButton, GBC.eol().anchor(GBC.CENTER).insets(0, 5, 0, 0)); + } + + public void setNumberFormat(CoordinateStringFormat csf) { + this.csf = csf; + latMaxTextField.setNumberFormat(csf.getNumberFormatLatitude()); + latMinTextField.setNumberFormat(csf.getNumberFormatLatitude()); + lonMaxTextField.setNumberFormat(csf.getNumberFormatLongitude()); + lonMinTextField.setNumberFormat(csf.getNumberFormatLongitude()); + } + + public CoordinateStringFormat getNumberFormat() { + return csf; + } + + public void setCoordinates(EastNorthCoordinate max, EastNorthCoordinate min) { + latMaxTextField.setCoordinate(max.lat); + lonMaxTextField.setCoordinate(max.lon); + latMinTextField.setCoordinate(min.lat); + lonMinTextField.setCoordinate(min.lon); + } + + public void setCoordinates(MapSelection ms) { + MercatorPixelCoordinate max = ms.getBottomRightPixelCoordinate(); + MercatorPixelCoordinate min = ms.getTopLeftPixelCoordinate(); + setSelection(max, min); + } + + public void setSelection(MercatorPixelCoordinate max, MercatorPixelCoordinate min) { + EastNorthCoordinate c1 = min.getEastNorthCoordinate(); + EastNorthCoordinate c2 = max.getEastNorthCoordinate(); + latMaxTextField.setCoordinate(c1.lat); + lonMaxTextField.setCoordinate(c2.lon); + latMinTextField.setCoordinate(c2.lat); + lonMinTextField.setCoordinate(c1.lon); + } + + /** + * Checks if the values for min/max langitude and min/max latitude are interchanged (smaller value in the max field + * and larger value in the min field) and swaps them if necessary. + */ + public void correctMinMax() { + try { + double lat1 = latMaxTextField.getCoordinate(); + double lat2 = latMinTextField.getCoordinate(); + if (lat1 < lat2) { + String tmp = latMaxTextField.getText(); + latMaxTextField.setText(latMinTextField.getText()); + latMinTextField.setText(tmp); + } + } catch (ParseException e) { + // one of the lat fields contains an invalid coordinate + } + try { + double lon1 = lonMaxTextField.getCoordinate(); + double lon2 = lonMinTextField.getCoordinate(); + if (lon1 < lon2) { + String tmp = lonMaxTextField.getText(); + lonMaxTextField.setText(lonMinTextField.getText()); + lonMinTextField.setText(tmp); + } + } catch (ParseException e) { + // one of the lon fields contains an invalid coordinate + } + } + + public MapSelection getMapSelection(MapSource mapSource) { + EastNorthCoordinate max = getMaxCoordinate(); + EastNorthCoordinate min = getMinCoordinate(); + return new MapSelection(mapSource, max, min); + } + + public EastNorthCoordinate getMaxCoordinate() { + return new EastNorthCoordinate(latMaxTextField.getCoordinateOrNaN(), lonMaxTextField.getCoordinateOrNaN()); + } + + public EastNorthCoordinate getMinCoordinate() { + return new EastNorthCoordinate(latMinTextField.getCoordinateOrNaN(), lonMinTextField.getCoordinateOrNaN()); + } + + public void addButtonActionListener(ActionListener l) { + applySelectionButton.addActionListener(l); + } + + protected class JNumberFormatMenuItem extends JMenuItem implements ActionListener { + + private static final long serialVersionUID = 1L; + private final CoordinateStringFormat csf; + + public JNumberFormatMenuItem(CoordinateStringFormat csf) { + super(csf.toString()); + this.csf = csf; + addActionListener(this); + } + + public void actionPerformed(ActionEvent e) { + System.out.println(e); + JCoordinatesPanel.this.setNumberFormat(csf); + } + + } +} diff --git a/src/main/java/mobac/gui/panels/JGpxPanel.java b/src/main/java/mobac/gui/panels/JGpxPanel.java new file mode 100644 index 0000000..8dcfe9b --- /dev/null +++ b/src/main/java/mobac/gui/panels/JGpxPanel.java @@ -0,0 +1,267 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.panels; + +import java.awt.GridBagLayout; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import mobac.data.gpx.gpx11.RteType; +import mobac.data.gpx.gpx11.TrkType; +import mobac.data.gpx.gpx11.TrksegType; +import mobac.data.gpx.gpx11.WptType; +import mobac.gui.actions.GpxAddPoint; +import mobac.gui.actions.GpxClear; +import mobac.gui.actions.GpxLoad; +import mobac.gui.actions.GpxNew; +import mobac.gui.actions.GpxSave; +import mobac.gui.components.JCollapsiblePanel; +import mobac.gui.gpxtree.GpxEntry; +import mobac.gui.gpxtree.GpxRootEntry; +import mobac.gui.gpxtree.GpxTreeListener; +import mobac.gui.gpxtree.RteEntry; +import mobac.gui.gpxtree.TrkEntry; +import mobac.gui.gpxtree.TrksegEntry; +import mobac.gui.gpxtree.WptEntry; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.layer.GpxLayer; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; + +/** + * Allows to load, display, edit and save gpx files using a tree view. TODO warn unsaved changes on exit + * + */ +public class JGpxPanel extends JCollapsiblePanel { + + private static final long serialVersionUID = 1L; + + private JTree tree; + private DefaultMutableTreeNode rootNode; + private DefaultTreeModel model; + private ArrayList openedFiles; + + private PreviewMap previewMap; + + public JGpxPanel(PreviewMap previewMap) { + super("Gpx", new GridBagLayout()); + + this.previewMap = previewMap; + + JButton newGpx = new JButton(I18nUtils.localizedStringForKey("rp_gpx_new_gpx")); + newGpx.addActionListener(new GpxNew(this)); + + JButton loadGpx = new JButton(I18nUtils.localizedStringForKey("rp_gpx_load_gpx")); + loadGpx.addActionListener(new GpxLoad(this)); + + JButton saveGpx = new JButton(I18nUtils.localizedStringForKey("rp_gpx_save_gpx")); + saveGpx.addActionListener(new GpxSave(this)); + + JButton clearGpx = new JButton(I18nUtils.localizedStringForKey("rp_gpx_clear_gpx")); + clearGpx.addActionListener(new GpxClear(this)); + + JButton addPointGpx = new JButton(I18nUtils.localizedStringForKey("rp_gpx_add_wpt")); + addPointGpx.addActionListener(new GpxAddPoint(this)); + + rootNode = new DefaultMutableTreeNode(I18nUtils.localizedStringForKey("rp_gpx_default_node_name")); + tree = new JTree(rootNode); + tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + tree.setRootVisible(false); + tree.setShowsRootHandles(true); + JScrollPane treeView = new JScrollPane(tree); + // treeView.setPreferredSize(new Dimension(100, 300)); + model = (DefaultTreeModel) tree.getModel(); + + tree.addMouseListener(new GpxTreeListener()); + + openedFiles = new ArrayList(); + + GBC eol = GBC.eol().fill(GBC.HORIZONTAL); + GBC std = GBC.std().fill(GBC.HORIZONTAL); + addContent(treeView, GBC.eol().fill()); + addContent(clearGpx, std); + addContent(addPointGpx, eol); + addContent(newGpx, std); + addContent(loadGpx, std); + addContent(saveGpx, eol); + } + + /** + * adds a layer for a new gpx file on the map and adds its structure to the treeview + * + */ + public GpxRootEntry addGpxLayer(GpxLayer layer) { + layer.setPanel(this); + GpxRootEntry gpxEntry = new GpxRootEntry(layer); + DefaultMutableTreeNode gpxNode = new DefaultMutableTreeNode(gpxEntry); + model.insertNodeInto(gpxNode, rootNode, rootNode.getChildCount()); + TreePath path = new TreePath(gpxNode.getPath()); + tree.scrollPathToVisible(new TreePath(path)); + tree.setSelectionPath(path); + + addRtes(layer, gpxNode); + addTrks(layer, gpxNode); + addWpts(layer, gpxNode); + + if (layer.getFile() != null) + openedFiles.add(layer.getFile().getAbsolutePath()); + + previewMap.mapLayers.add(layer); + return gpxEntry; + } + + /** + * @param layer + * @param gpxNode + * @param model + */ + private void addWpts(GpxLayer layer, DefaultMutableTreeNode gpxNode) { + List wpts = layer.getGpx().getWpt(); + for (WptType wpt : wpts) { + WptEntry wptEntry = new WptEntry(wpt, layer); + DefaultMutableTreeNode wptNode = new DefaultMutableTreeNode(wptEntry); + model.insertNodeInto(wptNode, gpxNode, gpxNode.getChildCount()); + } + } + + /** + * @param layer + * @param gpxNode + * @param model + */ + private void addTrks(GpxLayer layer, DefaultMutableTreeNode gpxNode) { + // tracks + List trks = layer.getGpx().getTrk(); + for (TrkType trk : trks) { + TrkEntry trkEntry = new TrkEntry(trk, layer); + DefaultMutableTreeNode trkNode = new DefaultMutableTreeNode(trkEntry); + model.insertNodeInto(trkNode, gpxNode, gpxNode.getChildCount()); + // trkseg + List trksegs = trk.getTrkseg(); + int counter = 1; + for (TrksegType trkseg : trksegs) { + TrksegEntry trksegEntry = new TrksegEntry(trkseg, counter, layer); + DefaultMutableTreeNode trksegNode = new DefaultMutableTreeNode(trksegEntry); + model.insertNodeInto(trksegNode, trkNode, trkNode.getChildCount()); + counter++; + + // add trkpts + List trkpts = trkseg.getTrkpt(); + for (WptType trkpt : trkpts) { + WptEntry trkptEntry = new WptEntry(trkpt, layer); + DefaultMutableTreeNode trkptNode = new DefaultMutableTreeNode(trkptEntry); + model.insertNodeInto(trkptNode, trksegNode, trksegNode.getChildCount()); + } + } + } + } + + /** + * adds routes and route points to the tree view + * + * @param layer + * @param gpxNode + */ + private void addRtes(GpxLayer layer, DefaultMutableTreeNode gpxNode) { + List rtes = layer.getGpx().getRte(); + for (RteType rte : rtes) { + RteEntry rteEntry = new RteEntry(rte, layer); + DefaultMutableTreeNode rteNode = new DefaultMutableTreeNode(rteEntry); + model.insertNodeInto(rteNode, gpxNode, gpxNode.getChildCount()); + // add rtepts + List rtepts = rte.getRtept(); + for (WptType rtept : rtepts) { + WptEntry rteptEntry = new WptEntry(rtept, layer); + DefaultMutableTreeNode rteptNode = new DefaultMutableTreeNode(rteptEntry); + model.insertNodeInto(rteptNode, rteNode, rteNode.getChildCount()); + } + } + } + + /** + * Updates the tree view to show the newly added waypoint. + * + * @param wpt + * - new waypoint + * @param gpxEntry + * - parent entry in the tree + */ + public void addWpt(WptType wpt, GpxEntry gpxEntry) { + WptEntry wptEntry = new WptEntry(wpt, gpxEntry.getLayer()); + DefaultMutableTreeNode wptNode = new DefaultMutableTreeNode(wptEntry); + model.insertNodeInto(wptNode, gpxEntry.getNode(), gpxEntry.getNode().getChildCount()); + } + + /** + * Updates the tree view after removing a waypoint. + * + * @param wpt + * - deleted waypoint + * @param gpxEntry + * - parent entry of the deleted element in the tree + */ + public void removeWpt(WptEntry wptEntry) { + DefaultMutableTreeNode wptNode = wptEntry.getNode(); + model.removeNodeFromParent(wptNode); + + // update layer (is changing the gpx enough? prolly not + // did remove it already...check wheter its enough + } + + public GpxEntry getSelectedEntry() { + TreePath selection = tree.getSelectionPath(); + if (selection == null) { + return null; + } + DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) selection.getLastPathComponent(); + + GpxEntry gpxEntry = null; + try { + gpxEntry = (GpxEntry) selectedNode.getUserObject(); + gpxEntry.setNode(selectedNode); + } catch (ClassCastException e) { + } + return gpxEntry; + } + + public boolean isFileOpen(String path) { + return openedFiles.contains(path); + } + + /** + * Resets the tree view. Used by GpxClear. + * + */ + public void resetModel() { + rootNode = new DefaultMutableTreeNode(I18nUtils.localizedStringForKey("rp_gpx_default_node_name")); + model.setRoot(rootNode); + openedFiles = new ArrayList(); + } + + public DefaultTreeModel getTreeModel() { + return model; + } + +} diff --git a/src/main/java/mobac/gui/panels/JProfilesPanel.java b/src/main/java/mobac/gui/panels/JProfilesPanel.java new file mode 100644 index 0000000..e99289f --- /dev/null +++ b/src/main/java/mobac/gui/panels/JProfilesPanel.java @@ -0,0 +1,189 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.panels; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import mobac.gui.atlastree.JAtlasTree; +import mobac.gui.components.JCollapsiblePanel; +import mobac.gui.components.JProfilesComboBox; +import mobac.program.model.Profile; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +public class JProfilesPanel extends JCollapsiblePanel { + + private static final long serialVersionUID = 1L; + + private JProfilesComboBox profilesCombo; + private JButton reloadButton; + private JButton deleteButton; + private JButton loadButton; + private JButton saveAsButton; + + public JProfilesPanel(JAtlasTree atlasTree) { + super(I18nUtils.localizedStringForKey("lp_atlas_profile_title"), new GridBagLayout()); + + if (atlasTree == null) + throw new NullPointerException(); + + // profiles combo box + profilesCombo = new JProfilesComboBox(); + profilesCombo.setToolTipText(I18nUtils.localizedStringForKey("lp_atlas_profile_combo_tips")); + profilesCombo.addActionListener(new ProfileListListener()); + + // delete profile button + deleteButton = new JButton(I18nUtils.localizedStringForKey("lp_atlas_profile_delete_btn_title")); + deleteButton.addActionListener(new DeleteProfileListener()); + deleteButton.setToolTipText(I18nUtils.localizedStringForKey("lp_atlas_profile_delete_btn_tips")); + + // save as profile button + saveAsButton = new JButton(I18nUtils.localizedStringForKey("lp_atlas_profile_save_btn_title")); + saveAsButton.setToolTipText(I18nUtils.localizedStringForKey("lp_atlas_profile_save_btn_tips")); + saveAsButton.addActionListener(new SaveAsProfileListener(atlasTree)); + + loadButton = new JButton(I18nUtils.localizedStringForKey("lp_atlas_profile_load_btn_title")); + loadButton.setToolTipText(I18nUtils.localizedStringForKey("lp_atlas_profile_load_btn_tips")); + + GBC gbc = GBC.eol().fill().insets(5, 5, 5, 5); + reloadButton = new JButton(Utilities.loadResourceImageIcon("refresh.png")); + reloadButton.setToolTipText(I18nUtils.localizedStringForKey("lp_atlas_profile_refresh_btn_tips")); + reloadButton.addActionListener(new ReloadListener()); + reloadButton.setPreferredSize(new Dimension(24, 0)); + + JPanel p = new JPanel(new BorderLayout()); + p.add(profilesCombo, BorderLayout.CENTER); + p.add(reloadButton, BorderLayout.EAST); + + contentContainer.add(p, gbc); + contentContainer.add(deleteButton, gbc.toggleEol()); + contentContainer.add(saveAsButton, gbc); + contentContainer.add(loadButton, gbc.toggleEol()); + + saveAsButton.setEnabled(false); + deleteButton.setEnabled(false); + loadButton.setEnabled(false); + } + + public void initialize() { + // Load all profiles from the profiles file from disk + profilesCombo.loadProfilesList(); + deleteButton.setEnabled(false); + loadButton.setEnabled(false); + } + + public void reloadProfileList() { + initialize(); + } + + public JProfilesComboBox getProfilesCombo() { + return profilesCombo; + } + + public JButton getLoadButton() { + return loadButton; + } + + public JButton getDeleteButton() { + return deleteButton; + } + + public JButton getSaveAsButton() { + return saveAsButton; + } + + public Profile getSelectedProfile() { + return profilesCombo.getSelectedProfile(); + } + + private class SaveAsProfileListener implements ActionListener { + + JAtlasTree jAtlasTree; + + public SaveAsProfileListener(JAtlasTree atlasTree) { + super(); + jAtlasTree = atlasTree; + } + + public void actionPerformed(ActionEvent e) { + if (!jAtlasTree.testAtlasContentValid()) + return; + Object selObject = profilesCombo.getEditor().getItem(); + String profileName = null; + Profile profile = null; + if (selObject instanceof Profile) { + profile = (Profile) selObject; + profileName = profile.getName(); + } else + profileName = (String) selObject; + + if (profileName.length() == 0) { + JOptionPane.showMessageDialog(null, I18nUtils.localizedStringForKey("lp_atlas_profile_msg_ask_name"), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.ERROR_MESSAGE); + return; + } + + profile = new Profile(profileName); + + if (profile.exists()) { + int response = JOptionPane.showConfirmDialog(null, + String.format(I18nUtils.localizedStringForKey("lp_atlas_profile_msg_overwrite_confirm"), profileName), + I18nUtils.localizedStringForKey("lp_atlas_profile_msg_overwrite_confirm_title"), + JOptionPane.YES_NO_OPTION); + if (response != JOptionPane.YES_OPTION) + return; + } + + if (jAtlasTree.save(profile)) { + reloadProfileList(); + profilesCombo.setSelectedItem(profile); + } + } + } + + private class DeleteProfileListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + profilesCombo.deleteSelectedProfile(); + } + } + + private class ReloadListener implements ActionListener { + + public void actionPerformed(ActionEvent e) { + reloadProfileList(); + } + } + + private class ProfileListListener implements ActionListener { + + public void actionPerformed(ActionEvent e) { + boolean existingProfileSelected = profilesCombo.getSelectedProfile() != null; + loadButton.setEnabled(existingProfileSelected); + deleteButton.setEnabled(existingProfileSelected); + } + } +} diff --git a/src/main/java/mobac/gui/panels/JTileImageParametersPanel.java b/src/main/java/mobac/gui/panels/JTileImageParametersPanel.java new file mode 100644 index 0000000..e634f72 --- /dev/null +++ b/src/main/java/mobac/gui/panels/JTileImageParametersPanel.java @@ -0,0 +1,282 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.panels; + +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; +import java.util.TreeSet; + +import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; + +import mobac.gui.components.JCollapsiblePanel; +import mobac.gui.components.JTileSizeCombo; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.model.AtlasOutputFormat; +import mobac.program.model.Settings; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +public class JTileImageParametersPanel extends JCollapsiblePanel { + + private static final long serialVersionUID = 1L; + private static boolean JPEG_TESTED = false; + + private JCheckBox enableCustomTileProcessingCheckButton; + private JLabel tileSizeWidthLabel; + private JLabel tileSizeHeightLabel; + private JLabel tileImageFormatLabel; + private JTileSizeCombo tileSizeWidth; + private JTileSizeCombo tileSizeHeight; + private JComboBox tileImageFormat; + + private boolean widthEnabled = true; + private boolean heightEnabled = true; + private boolean formatPngEnabled = true; + private boolean formatJpgEnabled = true; + + public JTileImageParametersPanel() { + super(I18nUtils.localizedStringForKey("lp_tile_param_title"), new GridBagLayout()); + setName("TileImageParameters"); + + enableCustomTileProcessingCheckButton = new JCheckBox( + I18nUtils.localizedStringForKey("lp_tile_param_recreate_checkbox_title")); + enableCustomTileProcessingCheckButton.addActionListener(new EnableCustomTileSizeCheckButtonListener()); + enableCustomTileProcessingCheckButton.setToolTipText(I18nUtils + .localizedStringForKey("lp_tile_param_recreate_checkbox_tips")); + + tileSizeWidthLabel = new JLabel(I18nUtils.localizedStringForKey("lp_tile_param_width_title")); + tileSizeWidth = new JTileSizeCombo(); + tileSizeWidth.setToolTipText(I18nUtils.localizedStringForKey("lp_tile_param_width_tips")); + + tileSizeHeightLabel = new JLabel(I18nUtils.localizedStringForKey("lp_tile_param_height_title")); + tileSizeHeight = new JTileSizeCombo(); + tileSizeHeight.setToolTipText(I18nUtils.localizedStringForKey("lp_tile_param_height_tips")); + + tileImageFormatLabel = new JLabel(I18nUtils.localizedStringForKey("lp_tile_param_image_fmt_title")); + tileImageFormat = new JComboBox(new TileFormatComboModel(TileImageFormat.values())); + tileImageFormat.setMaximumRowCount(tileImageFormat.getItemCount()); + tileImageFormat.addActionListener(new TileImageFormatListener()); + + GBC gbc_std = GBC.std().insets(5, 2, 5, 3); + GBC gbc_eol = GBC.eol().insets(5, 2, 5, 3); + + JPanel tileSizePanel = new JPanel(new GridBagLayout()); + tileSizePanel.add(tileSizeWidthLabel, gbc_std); + tileSizePanel.add(tileSizeWidth, gbc_std); + tileSizePanel.add(tileSizeHeightLabel, gbc_std); + tileSizePanel.add(tileSizeHeight, gbc_eol); + JPanel tileColorDepthPanel = new JPanel(); + tileColorDepthPanel.add(tileImageFormatLabel); + tileColorDepthPanel.add(tileImageFormat); + contentContainer.add(enableCustomTileProcessingCheckButton, gbc_eol); + contentContainer.add(tileSizePanel, GBC.eol()); + contentContainer.add(tileColorDepthPanel, GBC.eol()); + } + + public void loadSettings() { + Settings settings = Settings.getInstance(); + enableCustomTileProcessingCheckButton.setSelected(settings.isCustomTileSize()); + updateControlsState(); + tileImageFormat.setSelectedItem(settings.getTileImageFormat()); + tileSizeHeight.setValue(settings.getTileSize().height); + tileSizeWidth.setValue(settings.getTileSize().width); + } + + public void saveSettings() { + Settings settings = Settings.getInstance(); + settings.setCustomTileSize(enableCustomTileProcessingCheckButton.isSelected()); + Dimension tileSize = new Dimension(tileSizeWidth.getValue(), tileSizeHeight.getValue()); + settings.setTileSize(tileSize); + settings.setTileImageFormat((TileImageFormat) tileImageFormat.getSelectedItem()); + } + + public TileImageParameters getSelectedTileImageParameters() { + TileImageParameters customTileParameters = null; + boolean customTileSize = enableCustomTileProcessingCheckButton.isSelected(); + if (customTileSize) { + int width = tileSizeWidth.getValue(); + int height = tileSizeHeight.getValue(); + TileImageFormat format = (mobac.program.model.TileImageFormat) tileImageFormat.getSelectedItem(); + customTileParameters = new TileImageParameters(width, height, format); + } + return customTileParameters; + } + + public void atlasFormatChanged(AtlasOutputFormat newAtlasOutputFormat) { + Class atlasCreatorClass = newAtlasOutputFormat.getMapCreatorClass(); + SupportedParameters params = atlasCreatorClass.getAnnotation(SupportedParameters.class); + if (params != null) { + TreeSet paramNames = new TreeSet(Arrays.asList(params + .names())); + if (paramNames.contains(Name.format)) { + formatPngEnabled = true; + formatJpgEnabled = true; + } else { + formatPngEnabled = paramNames.contains(Name.format_png); + formatJpgEnabled = paramNames.contains(Name.format_jpg); + } + widthEnabled = paramNames.contains(Name.width); + heightEnabled = paramNames.contains(Name.height); + enableCustomTileProcessingCheckButton.setEnabled(true); + } else { + formatPngEnabled = false; + formatJpgEnabled = false; + widthEnabled = false; + heightEnabled = false; + enableCustomTileProcessingCheckButton.setEnabled(false); + } + updateControlsState(); + } + + public void updateControlsState() { + boolean b = false; + if (enableCustomTileProcessingCheckButton.isEnabled()) + b = enableCustomTileProcessingCheckButton.isSelected(); + tileSizeWidth.setEnabled(b && widthEnabled); + tileSizeWidthLabel.setEnabled(b && widthEnabled); + tileSizeHeightLabel.setEnabled(b && heightEnabled); + tileSizeHeight.setEnabled(b && heightEnabled); + boolean formatEnabled = formatJpgEnabled || formatPngEnabled; + tileImageFormatLabel.setEnabled(b && formatEnabled); + tileImageFormat.setEnabled(b && formatEnabled); + if (formatPngEnabled && !formatJpgEnabled) + updateFormatComboModel(TileImageFormat.getPngFormats()); + else if (!formatPngEnabled && formatJpgEnabled) + updateFormatComboModel(TileImageFormat.getJpgFormats()); + else + updateFormatComboModel(TileImageFormat.values()); + } + + private void updateFormatComboModel(TileImageFormat[] values) { + TileFormatComboModel model = (TileFormatComboModel) tileImageFormat.getModel(); + model.changeValues(values); + } + + public String getValidationErrorMessages() { + String errorText = ""; + if (!enableCustomTileProcessingCheckButton.isSelected()) + return errorText; + if (!tileSizeHeight.isValueValid()) + errorText += String.format(I18nUtils.localizedStringForKey("lp_tile_param_msg_valid_height"), + JTileSizeCombo.MIN, JTileSizeCombo.MAX); + + if (!tileSizeWidth.isValueValid()) + errorText += String.format(I18nUtils.localizedStringForKey("lp_tile_param_msg_valid_width"), + JTileSizeCombo.MIN, JTileSizeCombo.MAX); + return errorText; + } + + private class EnableCustomTileSizeCheckButtonListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + updateControlsState(); + } + } + + private class TileImageFormatListener implements ActionListener { + + public void actionPerformed(ActionEvent event) { + if (!tileImageFormat.isEnabled()) + return; + TileImageFormat tif = (TileImageFormat) tileImageFormat.getSelectedItem(); + if (tif == null) + return; + if (!JPEG_TESTED && (tif.getDataWriter() instanceof TileImageJpegDataWriter)) { + if (!TileImageJpegDataWriter.performOpenJDKJpegTest()) + JOptionPane.showMessageDialog(null, "The JPEG image format is not supported by OpenJDK.
" + + "Please select a different tile format.", "Image format not available on OpenJDK", + JOptionPane.ERROR_MESSAGE); + JPEG_TESTED = true; + } else if (tif == TileImageFormat.PNG4Bit || tif == TileImageFormat.PNG8Bit) { + if (Utilities.testJaiColorQuantizerAvailable()) + return; + JOptionPane.showMessageDialog(null, + "This image format is requires additional libraries to be installed:
" + + "Java Advanced Image library (jai_core.jar & jai_codec.jar)
" + + "For more details please see the file README.HTM " + + "in section Requirements.", + "Image format not available - libraries missing", JOptionPane.ERROR_MESSAGE); + tileImageFormat.setSelectedIndex(0); + } + } + } + + private class TileFormatComboModel extends AbstractListModel implements + ComboBoxModel { + + TileImageFormat[] values; + Object selectedObject = null; + + public TileFormatComboModel(TileImageFormat[] values) { + super(); + this.values = values; + if (values.length > 0) + selectedObject = values[0]; + } + + public void changeValues(TileImageFormat[] values) { + this.values = values; + boolean found = false; + for (TileImageFormat format : values) { + if (format.equals(selectedObject)) { + found = true; + break; + } + } + if (!found) + selectedObject = values[0]; + fireContentsChanged(this, -1, -1); + } + + public int getSize() { + return values.length; + } + + public TileImageFormat getElementAt(int index) { + return values[index]; + } + + @Override + public void setSelectedItem(Object anItem) { + if ((selectedObject != null && !selectedObject.equals(anItem)) || selectedObject == null && anItem != null) { + selectedObject = anItem; + fireContentsChanged(this, -1, -1); + } + } + + @Override + public Object getSelectedItem() { + return selectedObject; + } + + } + +} diff --git a/src/main/java/mobac/gui/panels/JTileStoreCoveragePanel.java b/src/main/java/mobac/gui/panels/JTileStoreCoveragePanel.java new file mode 100644 index 0000000..7ba5116 --- /dev/null +++ b/src/main/java/mobac/gui/panels/JTileStoreCoveragePanel.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.panels; + +import java.awt.FlowLayout; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import mobac.gui.components.JCollapsiblePanel; +import mobac.gui.mapview.PreviewMap; +import mobac.gui.mapview.controller.JMapController; +import mobac.gui.mapview.interfaces.MapEventListener; +import mobac.gui.mapview.layer.TileStoreCoverageLayer; +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.model.MercatorPixelCoordinate; +import mobac.utilities.GBC; +import mobac.utilities.I18nUtils; + +public class JTileStoreCoveragePanel extends JCollapsiblePanel implements MapEventListener, ActionListener { + + //MP-add + private static final long serialVersionUID = 1L; + + private JButton showCoverage; + private JButton hideCoverage; + private JComboBox layerSelector; + private JComboBox zoomCombo; + private PreviewMap mapViewer; + + public JTileStoreCoveragePanel(PreviewMap mapViewer) { + super(I18nUtils.localizedStringForKey("lp_tile_store_title")); + contentContainer.setLayout(new GridBagLayout()); + this.mapViewer = mapViewer; + + showCoverage = new JButton(I18nUtils.localizedStringForKey("lp_tile_store_show_coverage_btn_title")); + showCoverage.addActionListener(this); + showCoverage.setToolTipText(I18nUtils.localizedStringForKey("lp_tile_store_show_coverage_btn_tips")); + hideCoverage = new JButton(I18nUtils.localizedStringForKey("lp_tile_store_hide_coverage_btn_title")); + hideCoverage.addActionListener(this); + hideCoverage.setEnabled(false); + zoomCombo = new JComboBox(); + zoomCombo.setToolTipText(I18nUtils.localizedStringForKey("lp_tile_store_zoom_combo_tips")); + titlePanel.setToolTipText(I18nUtils.localizedStringForKey("lp_tile_store_title_tips")); + layerSelector = new JComboBox(); + + GBC gbc_eol = GBC.eol().insets(2, 2, 2, 2); + GBC gbc_std = GBC.std().insets(2, 2, 2, 2); + + contentContainer.add(new JLabel(I18nUtils.localizedStringForKey("lp_tile_store_zoom_title")), gbc_std); + contentContainer.add(zoomCombo, gbc_eol); + contentContainer.add(new JLabel(I18nUtils.localizedStringForKey("lp_tile_store_layer_title")), gbc_std); + contentContainer.add(layerSelector, gbc_eol); + contentContainer.add(showCoverage, gbc_eol.fillH()); + contentContainer.add(hideCoverage, gbc_eol.fillH()); + mapSourceChanged(mapViewer.getMapSource()); + mapViewer.addMapEventListener(this); + } + + public void actionPerformed(ActionEvent e) { + if (hideCoverage.equals(e.getSource())) { + TileStoreCoverageLayer.removeCacheCoverageLayers(); + mapViewer.repaint(); + hideCoverage.setEnabled(false); + return; + } + Integer zoom = (Integer) zoomCombo.getSelectedItem(); + if (zoom == null) + return; + TileStoreCoverageLayer.removeCacheCoverageLayers(); + mapViewer.repaint(); + TileStoreCoverageLayer tscl = new TileStoreCoverageLayer(mapViewer, + (MapSource) layerSelector.getSelectedItem(), zoom); + mapViewer.mapLayers.add(tscl); + hideCoverage.setEnabled(true); + } + + public void gridZoomChanged(int newGridZoomLevel) { + } + + public void mapSourceChanged(MapSource newMapSource) { + TileStoreCoverageLayer.removeCacheCoverageLayers(); + hideCoverage.setEnabled(false); + Integer selZoom = (Integer) zoomCombo.getSelectedItem(); + if (selZoom == null) + selZoom = new Integer(8); + int zoomLevels = Math.max(0, newMapSource.getMaxZoom() - newMapSource.getMinZoom() + 1); + Integer[] items = new Integer[zoomLevels]; + int zoom = newMapSource.getMinZoom(); + for (int i = 0; i < items.length; i++) { + items[i] = new Integer(zoom++); + } + zoomCombo.setModel(new DefaultComboBoxModel(items)); + zoomCombo.setMaximumRowCount(10); + zoomCombo.setSelectedItem(selZoom); + MapSource[] layers; + if (newMapSource instanceof AbstractMultiLayerMapSource) { + layers = ((AbstractMultiLayerMapSource) newMapSource).getLayerMapSources(); + layerSelector.setEnabled(true); + } else { + layers = new MapSource[] { newMapSource }; + layerSelector.setEnabled(false); + } + layerSelector.setModel(new DefaultComboBoxModel(layers)); + layerSelector.setSelectedIndex(0); + } + + public void selectNextMapSource() { + } + + public void selectPreviousMapSource() { + } + + public void selectionChanged(MercatorPixelCoordinate max, MercatorPixelCoordinate min) { + } + + public void zoomChanged(int newZoomLevel) { + } + + public void mapSelectionControllerChanged(JMapController newMapController) { + } + +} diff --git a/src/main/java/mobac/gui/settings/SettingsGUI.java b/src/main/java/mobac/gui/settings/SettingsGUI.java new file mode 100644 index 0000000..b9c175a --- /dev/null +++ b/src/main/java/mobac/gui/settings/SettingsGUI.java @@ -0,0 +1,970 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.settings; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; +import java.util.Vector; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.xml.bind.JAXBException; + +import mobac.StartMOBAC; +import mobac.exceptions.UpdateFailedException; +import mobac.gui.MainGUI; +import mobac.gui.actions.OpenInWebbrowser; +import mobac.gui.components.JDirectoryChooser; +import mobac.gui.components.JMapSizeCombo; +import mobac.gui.components.JTimeSlider; +import mobac.mapsources.DefaultMapSourcesManager; +import mobac.mapsources.MapSourcesManager; +import mobac.mapsources.loader.MapPackManager; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.interfaces.MapSource; +import mobac.program.model.MapSourcesListModel; +import mobac.program.model.ProxyType; +import mobac.program.model.Settings; +import mobac.program.model.UnitSystem; +import mobac.program.tilestore.TileStore; +import mobac.utilities.GBC; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +public class SettingsGUI extends JDialog { + private static final long serialVersionUID = -5227934684609357198L; + + public static Logger log = Logger.getLogger(SettingsGUI.class); + + private static final Integer[] THREADCOUNT_LIST = { 1, 2, 4, 6 }; + + private static final long MBIT1 = 1000000 / 8; + + private enum Bandwidth { + UNLIMITED(I18nUtils.localizedStringForKey("set_net_bandwidth_unlimited"), 0), // + MBit1("1 MBit", MBIT1), // + MBit5("5 MBit", MBIT1 * 5), // + MBit10("10 MBit", MBIT1 * 10), // + MBit15("15 MBit", MBIT1 * 15), // + MBit20("20 MBit", MBIT1 * 20); + + public final long limit; + public final String description; + + private Bandwidth(String description, long limit) { + this.description = description; + this.limit = limit; + } + + @Override + public String toString() { + return description; + } + }; + + private enum SupportLocale { + SupportLocaleEn(new Locale("en"), "English"), // default + SupportLocaleFrFR(new Locale("fr", "FR"), "Français"), // French + SupportLocaleJaJP(new Locale("ja", "JP"), "日本語"), // Japanese + SupportLocaleZhCN(new Locale("zh", "CN"), "简体中文"), // Chinese (simplified) + SupportLocaleZhTW(new Locale("zh", "TW"), "繁體中文"); // Chinese (Taiwan) + + private final Locale locale; + private final String displayName; + + private SupportLocale(Locale locale, String displayName) { + this.locale = locale; + this.displayName = displayName; + } + + public static SupportLocale localeOf(String lang, String contry) { + for (SupportLocale l : SupportLocale.values()) { + if (l.locale.getLanguage().equals(lang) && l.locale.getCountry().equals(contry)) { + return l; + } + } + return SupportLocaleEn; + } + + @Override + public String toString() { + return displayName; + } + }; + + private final Settings settings = Settings.getInstance(); + + private JComboBox unitSystem; + + private JComboBox languageCombo; + + private JButton mapSourcesOnlineUpdate; + private JTextField osmHikingTicket; + + private SettingsGUITileStore tileStoreTab; + + private JTimeSlider defaultExpirationTime; + private JTimeSlider minExpirationTime; + private JTimeSlider maxExpirationTime; + + private JMapSizeCombo mapSize; + + private JSpinner mapOverlapTiles; + + private JTextField atlasOutputDirectory; + + private JComboBox threadCount; + private JComboBox bandwidth; + + private JComboBox proxyType; + private JTextField proxyHost; + private JTextField proxyPort; + + private JTextField proxyUserName; + private JTextField proxyPassword; + + private JCheckBox ignoreDlErrors; + + private JButton okButton; + private JButton cancelButton; + + private JTabbedPane tabbedPane; + + private JList enabledMapSources; + + private MapSourcesListModel enabledMapSourcesModel; + + private JList disabledMapSources; + + private MapSourcesListModel disabledMapSourcesModel; + + private final SettingsGUIPaper paperAtlas; + private final SettingsGUIWgsGrid display; + + public static void showSettingsDialog(final JFrame owner) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + new SettingsGUI(owner); + } + }); + } + + private SettingsGUI(JFrame owner) { + super(owner); + setIconImages(MainGUI.MOBAC_ICONS); + GUIExceptionHandler.registerForCurrentThread(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setModal(true); + setMinimumSize(new Dimension(300, 300)); + + paperAtlas = new SettingsGUIPaper(); + display = new SettingsGUIWgsGrid(); + + createJFrame(); + createTabbedPane(); + createJButtons(); + loadSettings(); + addListeners(); + pack(); + // don't allow shrinking, but allow enlarging + setMinimumSize(getSize()); + Dimension dScreen = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation((dScreen.width - getWidth()) / 2, (dScreen.height - getHeight()) / 2); + setVisible(true); + } + + private void createJFrame() { + setLayout(new BorderLayout()); + setTitle(I18nUtils.localizedStringForKey("set_title")); + } + + // Create tabbed pane + public void createTabbedPane() { + tabbedPane = new JTabbedPane(); + tabbedPane.setBounds(0, 0, 492, 275); + addDisplaySettingsPanel(); + try { + addMapSourceSettingsPanel(); + } catch (URISyntaxException e) { + log.error("", e); + } + addMapSourceManagerPanel(); + addTileUpdatePanel(); + tileStoreTab = new SettingsGUITileStore(this); + addMapSizePanel(); + addDirectoriesPanel(); + addNetworkPanel(); + tabbedPane.addTab(paperAtlas.getName(), paperAtlas); + + add(tabbedPane, BorderLayout.CENTER); + } + + private JPanel createNewTab(String tabTitle) { + JPanel tabPanel = new JPanel(); + addTab(tabTitle, tabPanel); + return tabPanel; + } + + protected void addTab(String tabTitle, JPanel tabPanel) { + tabPanel.setName(tabTitle); + tabPanel.setBorder(new EmptyBorder(5, 5, 5, 5)); + tabbedPane.add(tabPanel, tabTitle); + } + + private void addDisplaySettingsPanel() { + JPanel tab = createNewTab(I18nUtils.localizedStringForKey("set_display_title")); + tab.setLayout(new GridBagLayout()); + + JPanel unitSystemPanel = new JPanel(new GridBagLayout()); + unitSystemPanel + .setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_display_unit_system_title"))); + + // Language Panel + JPanel languagePanel = new JPanel(new GridBagLayout()); + languagePanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_display_language"))); + languageCombo = new JComboBox(SupportLocale.values()); + languageCombo.setToolTipText(I18nUtils.localizedStringForKey("set_display_language_choose_tips")); + languageCombo.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + + Locale locale = ((SupportLocale) languageCombo.getSelectedItem()).locale; + String currentLocaleStr = "" + settings.localeLanguage + settings.localeCountry; + String LocaleStr = "" + locale.getLanguage() + locale.getCountry(); + if (!currentLocaleStr.equals(LocaleStr) && isVisible()) { + settings.localeLanguage = locale.getLanguage(); + settings.localeCountry = locale.getCountry(); + + int result = JOptionPane.showConfirmDialog(null, + I18nUtils.localizedStringForKey("set_display_language_restart_desc"), + I18nUtils.localizedStringForKey("set_display_language_msg_title"), + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + I18nUtils.updateLocalizedStringFormSettings(); + if (result == JOptionPane.YES_OPTION) { + applySettings(); + + try { + final String javaBin = System.getProperty("java.home") + File.separator + "bin" + + File.separator + "java"; + final File currentJar = new File(SettingsGUI.class.getProtectionDomain().getCodeSource() + .getLocation().toURI()); + + /* is it a jar file? */ + if (currentJar.getName().endsWith(".jar")) { + /* Build command: java -jar application.jar */ + + Runtime r = Runtime.getRuntime(); + long maxMem = r.maxMemory(); + final ArrayList command = new ArrayList(); + command.add(javaBin); + command.add("-jar"); + command.add("-Xms64m"); + if ((Long.MAX_VALUE == maxMem)) { + command.add("-Xmx1024M"); + } else { + command.add("-Xmx" + (maxMem / 1048576) + "M"); + } + command.add(currentJar.getPath()); + + log.debug("restarting MOBAC using the following command: \n\t" + + Arrays.toString(command.toArray())); + final ProcessBuilder builder = new ProcessBuilder(command); + builder.start(); + } + } catch (Exception ex) { + + } + System.exit(0); + } + } + } + }); + + languagePanel.add(new JLabel(I18nUtils.localizedStringForKey("set_display_language_choose")), GBC.std()); + languagePanel.add(languageCombo, GBC.std()); + languagePanel.add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL)); + + UnitSystem[] us = UnitSystem.values(); + unitSystem = new JComboBox(us); + unitSystemPanel + .add(new JLabel(I18nUtils.localizedStringForKey("set_display_unit_system_scale_bar")), GBC.std()); + unitSystemPanel.add(unitSystem, GBC.std()); + unitSystemPanel.add(Box.createHorizontalGlue(), GBC.eol().fill(GBC.HORIZONTAL)); + tab.add(unitSystemPanel, GBC.eol().fill(GBC.HORIZONTAL)); + tab.add(display, GBC.eol().fill(GBC.HORIZONTAL)); + tab.add(languagePanel, GBC.eol().fill(GBC.HORIZONTAL)); + tab.add(Box.createVerticalGlue(), GBC.std().fill(GBC.VERTICAL)); + } + + private void addMapSourceSettingsPanel() throws URISyntaxException { + + JPanel tab = createNewTab(I18nUtils.localizedStringForKey("set_mapsrc_config_title")); + tab.setLayout(new GridBagLayout()); + + JPanel updatePanel = new JPanel(new GridBagLayout()); + updatePanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_mapsrc_config_online_update"))); + + mapSourcesOnlineUpdate = new JButton(I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_btn")); + mapSourcesOnlineUpdate.addActionListener(new MapPacksOnlineUpdateAction()); + updatePanel.add(mapSourcesOnlineUpdate, GBC.std()); + + JPanel osmHikingPanel = new JPanel(new GridBagLayout()); + osmHikingPanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_mapsrc_config_osmhiking"))); + + osmHikingTicket = new JTextField(20); + + osmHikingPanel.add(new JLabel(I18nUtils.localizedStringForKey("set_mapsrc_config_osmhiking_purchased")), + GBC.std()); + osmHikingPanel.add(osmHikingTicket, GBC.std().insets(2, 0, 10, 0)); + JLabel osmHikingTicketUrl = new JLabel(I18nUtils.localizedStringForKey("set_mapsrc_config_osmhiking_howto")); + osmHikingTicketUrl.addMouseListener(new OpenInWebbrowser(I18nUtils + .localizedStringForKey("set_mapsrc_config_osmhiking_howto_url"))); + osmHikingPanel.add(osmHikingTicketUrl, GBC.eol()); + + tab.add(updatePanel, GBC.eol().fill(GBC.HORIZONTAL)); + tab.add(osmHikingPanel, GBC.eol().fill(GBC.HORIZONTAL)); + tab.add(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL)); + } + + private void addMapSourceManagerPanel() { + JPanel tab = createNewTab(I18nUtils.localizedStringForKey("set_mapsrc_mgr_title")); + tab.setLayout(new GridBagLayout()); + + JPanel leftPanel = new JPanel(new BorderLayout()); + leftPanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_mapsrc_mgr_title_enabled"))); + + JPanel centerPanel = new JPanel(new GridBagLayout()); + JPanel rightPanel = new JPanel(new BorderLayout()); + rightPanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_mapsrc_mgr_title_disabled"))); + + JButton up = new JButton(Utilities.loadResourceImageIcon("arrow_blue_up.png")); + up.setToolTipText(I18nUtils.localizedStringForKey("set_mapsrc_mgr_move_up_tips")); + JButton down = new JButton(Utilities.loadResourceImageIcon("arrow_blue_down.png")); + down.setToolTipText(I18nUtils.localizedStringForKey("set_mapsrc_mgr_move_down_tips")); + JButton toLeft = new JButton(Utilities.loadResourceImageIcon("arrow_blue_left.png")); + toLeft.setToolTipText(I18nUtils.localizedStringForKey("set_mapsrc_mgr_move_left_tips")); + JButton toRight = new JButton(Utilities.loadResourceImageIcon("arrow_blue_right.png")); + toRight.setToolTipText(I18nUtils.localizedStringForKey("set_mapsrc_mgr_move_right_tips")); + Insets buttonInsets = new Insets(4, 4, 4, 4); + Dimension buttonDimension = new Dimension(40, 40); + up.setPreferredSize(buttonDimension); + down.setPreferredSize(buttonDimension); + toLeft.setPreferredSize(buttonDimension); + toRight.setPreferredSize(buttonDimension); + up.setMargin(buttonInsets); + down.setMargin(buttonInsets); + toLeft.setMargin(buttonInsets); + toRight.setMargin(buttonInsets); + + toLeft.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + int[] idx = disabledMapSources.getSelectedIndices(); + for (int i = 0; i < idx.length; i++) { + MapSource ms = disabledMapSourcesModel.removeElement(idx[i] - i); + enabledMapSourcesModel.addElement(ms); + } + } + }); + toRight.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + int[] idx = enabledMapSources.getSelectedIndices(); + for (int i = 0; i < idx.length; i++) { + MapSource ms = enabledMapSourcesModel.removeElement(idx[i] - i); + disabledMapSourcesModel.addElement(ms); + } + disabledMapSourcesModel.sort(); + } + }); + up.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + int[] idx = enabledMapSources.getSelectedIndices(); + if (idx.length == 0) + return; + for (int i = 0; i < idx.length; i++) { + int index = idx[i]; + if (index == 0) + return; + if (enabledMapSourcesModel.moveUp(index)) + idx[i]--; + } + enabledMapSources.setSelectedIndices(idx); + enabledMapSources.ensureIndexIsVisible(idx[0]); + } + }); + down.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + int[] idx = enabledMapSources.getSelectedIndices(); + if (idx.length == 0) + return; + for (int i = idx.length - 1; i >= 0; i--) { + int index = idx[i]; + if (index == enabledMapSourcesModel.getSize() - 1) + return; + if (enabledMapSourcesModel.moveDown(index)) + idx[i]++; + } + enabledMapSources.setSelectedIndices(idx); + enabledMapSources.ensureIndexIsVisible(idx[idx.length - 1]); + } + }); + GBC buttonGbc = GBC.eol(); + centerPanel.add(Box.createVerticalStrut(25), GBC.eol()); + centerPanel.add(toLeft, buttonGbc); + centerPanel.add(toRight, buttonGbc); + centerPanel.add(up, buttonGbc); + centerPanel.add(down, buttonGbc); + centerPanel.add(Box.createVerticalGlue(), GBC.std().fill()); + + MapSourcesManager msManager = MapSourcesManager.getInstance(); + + enabledMapSourcesModel = new MapSourcesListModel(msManager.getEnabledOrderedMapSources()); + enabledMapSources = new JList(enabledMapSourcesModel); + JScrollPane leftScrollPane = new JScrollPane(enabledMapSources, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + leftPanel.add(leftScrollPane, BorderLayout.CENTER); + + disabledMapSourcesModel = new MapSourcesListModel(msManager.getDisabledMapSources()); + disabledMapSourcesModel.sort(); + disabledMapSources = new JList(disabledMapSourcesModel); + JScrollPane rightScrollPane = new JScrollPane(disabledMapSources, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + rightPanel.add(rightScrollPane, BorderLayout.CENTER); + + JPanel mapSourcesInnerPanel = new JPanel(); + + Color c = UIManager.getColor("List.background"); + mapSourcesInnerPanel.setBackground(c); + + GBC lr = GBC.std().fill(); + lr.weightx = 1.0; + + tab.add(leftPanel, lr); + tab.add(centerPanel, GBC.std().fill(GBC.VERTICAL)); + tab.add(rightPanel, lr); + } + + private void addTileUpdatePanel() { + JPanel backGround = createNewTab(I18nUtils.localizedStringForKey("set_tile_update_title")); + backGround.setLayout(new GridBagLayout()); + + ChangeListener sliderChangeListener = new ChangeListener() { + + public void stateChanged(ChangeEvent e) { + JTimeSlider slider = ((JTimeSlider) e.getSource()); + long x = slider.getTimeSecondsValue(); + JPanel panel = (JPanel) slider.getParent(); + TitledBorder tb = (TitledBorder) panel.getBorder(); + tb.setTitle(panel.getName() + ": " + Utilities.formatDurationSeconds(x)); + panel.repaint(); + } + }; + GBC gbc_ef = GBC.eol().fill(GBC.HORIZONTAL); + + JPanel defaultExpirationPanel = new JPanel(new GridBagLayout()); + defaultExpirationPanel.setName(I18nUtils.localizedStringForKey("set_tile_update_default_expiration")); + defaultExpirationPanel.setBorder(createSectionBorder("")); + defaultExpirationTime = new JTimeSlider(); + defaultExpirationTime.addChangeListener(sliderChangeListener); + JLabel descr = new JLabel(I18nUtils.localizedStringForKey("set_tile_update_default_expiration_desc"), + JLabel.CENTER); + + defaultExpirationPanel.add(descr, gbc_ef); + defaultExpirationPanel.add(defaultExpirationTime, gbc_ef); + + JPanel maxExpirationPanel = new JPanel(new BorderLayout()); + maxExpirationPanel.setName(I18nUtils.localizedStringForKey("set_tile_update_max_expiration")); + maxExpirationPanel.setBorder(createSectionBorder("")); + maxExpirationTime = new JTimeSlider(); + maxExpirationTime.addChangeListener(sliderChangeListener); + maxExpirationPanel.add(maxExpirationTime, BorderLayout.CENTER); + + JPanel minExpirationPanel = new JPanel(new BorderLayout()); + minExpirationPanel.setName(I18nUtils.localizedStringForKey("set_tile_update_min_expiration")); + minExpirationPanel.setBorder(createSectionBorder("")); + minExpirationTime = new JTimeSlider(); + minExpirationTime.addChangeListener(sliderChangeListener); + minExpirationPanel.add(minExpirationTime, BorderLayout.CENTER); + + descr = new JLabel(I18nUtils.localizedStringForKey("set_tile_update_desc"), JLabel.CENTER); + + backGround.add(descr, gbc_ef); + backGround.add(defaultExpirationPanel, gbc_ef); + backGround.add(minExpirationPanel, gbc_ef); + backGround.add(maxExpirationPanel, gbc_ef); + backGround.add(Box.createVerticalGlue(), GBC.std().fill()); + } + + private void addMapSizePanel() { + JPanel backGround = createNewTab(I18nUtils.localizedStringForKey("set_map_size_title")); + backGround.setLayout(new GridBagLayout()); + mapSize = new JMapSizeCombo(); + mapSize.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + log.trace("Map size: " + mapSize.getValue()); + } + }); + + JLabel mapSizeLabel = new JLabel(I18nUtils.localizedStringForKey("set_map_size_max_size_of_rect")); + JLabel mapSizeText = new JLabel(I18nUtils.localizedStringForKey("set_map_size_desc")); + + mapOverlapTiles = new JSpinner(new SpinnerNumberModel(0, 0, 5, 1)); + + JLabel mapOverlapTilesLabel = new JLabel(I18nUtils.localizedStringForKey("set_map_size_overlap_tiles")); + + JPanel leftPanel = new JPanel(new GridBagLayout()); + leftPanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_map_size_settings"))); + + GBC gbc = GBC.eol().insets(0, 5, 0, 5); + leftPanel.add(mapSizeLabel, GBC.std()); + leftPanel.add(mapSize, GBC.eol()); + leftPanel.add(mapOverlapTilesLabel, GBC.std()); + leftPanel.add(mapOverlapTiles, GBC.eol()); + leftPanel.add(mapSizeText, gbc.fill(GBC.HORIZONTAL)); + leftPanel.add(Box.createVerticalGlue(), GBC.std().fill(GBC.VERTICAL)); + + backGround.add(leftPanel, GBC.std().fill(GBC.HORIZONTAL).anchor(GBC.NORTHEAST)); + backGround.add(Box.createVerticalGlue(), GBC.std().fill(GBC.VERTICAL)); + } + + private void addDirectoriesPanel() { + JPanel backGround = createNewTab(I18nUtils.localizedStringForKey("set_directory_title")); + backGround.setLayout(new GridBagLayout()); + JPanel atlasOutputDirPanel = new JPanel(new GridBagLayout()); + atlasOutputDirPanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_directory_output"))); + + atlasOutputDirectory = new JTextField(); + atlasOutputDirectory.setToolTipText(String.format(I18nUtils.localizedStringForKey("set_directory_output_tips"), + settings.getAtlasOutputDirectory())); + JButton selectAtlasOutputDirectory = new JButton(I18nUtils.localizedStringForKey("set_directory_output_select")); + selectAtlasOutputDirectory.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + JDirectoryChooser dc = new JDirectoryChooser(); + dc.setCurrentDirectory(settings.getAtlasOutputDirectory()); + if (dc.showDialog(SettingsGUI.this, + I18nUtils.localizedStringForKey("set_directory_output_select_dlg_title")) != JFileChooser.APPROVE_OPTION) + return; + atlasOutputDirectory.setText(dc.getSelectedFile().getAbsolutePath()); + } + }); + + atlasOutputDirPanel.add(atlasOutputDirectory, GBC.std().fillH()); + atlasOutputDirPanel.add(selectAtlasOutputDirectory, GBC.std()); + + backGround.add(atlasOutputDirPanel, GBC.eol().fillH()); + backGround.add(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL)); + } + + private void addNetworkPanel() { + JPanel backGround = createNewTab(I18nUtils.localizedStringForKey("set_net_title")); + backGround.setLayout(new GridBagLayout()); + GBC gbc_eolh = GBC.eol().fill(GBC.HORIZONTAL); + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_net_connection"))); + threadCount = new JComboBox(THREADCOUNT_LIST); + threadCount.setMaximumRowCount(THREADCOUNT_LIST.length); + panel.add(threadCount, GBC.std().insets(5, 5, 5, 5).anchor(GBC.EAST)); + panel.add(new JLabel(I18nUtils.localizedStringForKey("set_net_connection_desc")), GBC.eol() + .fill(GBC.HORIZONTAL)); + + bandwidth = new JComboBox(Bandwidth.values()); + bandwidth.setMaximumRowCount(bandwidth.getItemCount()); + panel.add(bandwidth, GBC.std().insets(5, 5, 5, 5)); + panel.add(new JLabel(I18nUtils.localizedStringForKey("set_net_bandwidth_desc")), GBC.eol().fill(GBC.HORIZONTAL)); + + backGround.add(panel, gbc_eolh); + + // panel = new JPanel(new GridBagLayout()); + // panel.setBorder(createSectionBorder("HTTP User-Agent")); + // backGround.add(panel, gbc_eolh); + + panel = new JPanel(new GridBagLayout()); + panel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_net_proxy"))); + final JLabel proxyTypeLabel = new JLabel(I18nUtils.localizedStringForKey("set_net_proxy_settings")); + proxyType = new JComboBox(ProxyType.values()); + proxyType.setSelectedItem(settings.getProxyType()); + + final JLabel proxyHostLabel = new JLabel(I18nUtils.localizedStringForKey("set_net_proxy_host")); + proxyHost = new JTextField(settings.getCustomProxyHost()); + + final JLabel proxyPortLabel = new JLabel(I18nUtils.localizedStringForKey("set_net_proxy_port")); + proxyPort = new JTextField(settings.getCustomProxyPort()); + + final JLabel proxyUserNameLabel = new JLabel(I18nUtils.localizedStringForKey("set_net_proxy_username")); + proxyUserName = new JTextField(settings.getCustomProxyUserName()); + + final JLabel proxyPasswordLabel = new JLabel(I18nUtils.localizedStringForKey("set_net_proxy_password")); + proxyPassword = new JTextField(settings.getCustomProxyPassword()); + + ActionListener al = new ActionListener() { + + public void actionPerformed(ActionEvent e) { + boolean b = ProxyType.CUSTOM.equals(proxyType.getSelectedItem()); + boolean c = ProxyType.CUSTOM_W_AUTH.equals(proxyType.getSelectedItem()); + proxyHost.setEnabled(b || c); + proxyPort.setEnabled(b || c); + proxyHostLabel.setEnabled(b || c); + proxyPortLabel.setEnabled(b || c); + proxyUserName.setEnabled(c); + proxyPassword.setEnabled(c); + proxyUserNameLabel.setEnabled(c); + proxyPasswordLabel.setEnabled(c); + } + }; + al.actionPerformed(null); + proxyType.addActionListener(al); + + panel.add(proxyTypeLabel, GBC.std()); + panel.add(proxyType, gbc_eolh.insets(5, 2, 5, 2)); + + panel.add(proxyHostLabel, GBC.std()); + panel.add(proxyHost, gbc_eolh); + + panel.add(proxyPortLabel, GBC.std()); + panel.add(proxyPort, gbc_eolh); + + panel.add(proxyUserNameLabel, GBC.std()); + panel.add(proxyUserName, gbc_eolh); + + panel.add(proxyPasswordLabel, GBC.std()); + panel.add(proxyPassword, gbc_eolh); + + backGround.add(panel, GBC.eol().fillH()); + + ignoreDlErrors = new JCheckBox(I18nUtils.localizedStringForKey("set_net_default_ignore_error"), + settings.ignoreDlErrors); + JPanel jPanel = new JPanel(new GridBagLayout()); + jPanel.setBorder(createSectionBorder(I18nUtils.localizedStringForKey("set_net_default"))); + jPanel.add(ignoreDlErrors, GBC.std()); + jPanel.add(Box.createHorizontalGlue(), GBC.eol().fillH()); + backGround.add(jPanel, GBC.eol().fillH()); + + backGround.add(Box.createVerticalGlue(), GBC.eol().fill(GBC.VERTICAL)); + } + + public void createJButtons() { + JPanel buttonPanel = new JPanel(new GridBagLayout()); + okButton = new JButton(I18nUtils.localizedStringForKey("OK")); + cancelButton = new JButton(I18nUtils.localizedStringForKey("Cancel")); + + GBC gbc = GBC.std().insets(5, 5, 5, 5); + buttonPanel.add(okButton, gbc); + buttonPanel.add(cancelButton, gbc); + add(buttonPanel, BorderLayout.SOUTH); + } + + private void loadSettings() { + Settings s = settings; + + unitSystem.setSelectedItem(s.unitSystem); + tileStoreTab.tileStoreEnabled.setSelected(s.tileStoreEnabled); + + // language + languageCombo.setSelectedItem(SupportLocale.localeOf(s.localeLanguage, s.localeCountry)); + + mapSize.setValue(s.maxMapSize); + mapOverlapTiles.setValue(s.mapOverlapTiles); + + atlasOutputDirectory.setText(s.getAtlasOutputDirectoryString()); + + long limit = s.getBandwidthLimit(); + for (Bandwidth b : Bandwidth.values()) { + if (limit <= b.limit) { + bandwidth.setSelectedItem(b); + break; + } + } + + int index = Arrays.binarySearch(THREADCOUNT_LIST, s.downloadThreadCount); + if (index < 0) { + if (s.downloadThreadCount > THREADCOUNT_LIST[THREADCOUNT_LIST.length - 1]) + index = THREADCOUNT_LIST.length - 1; + else + index = 0; + } + threadCount.setSelectedIndex(index); + + defaultExpirationTime.setTimeMilliValue(s.tileDefaultExpirationTime); + maxExpirationTime.setTimeMilliValue(s.tileMaxExpirationTime); + minExpirationTime.setTimeMilliValue(s.tileMinExpirationTime); + + osmHikingTicket.setText(s.osmHikingTicket); + + ignoreDlErrors.setSelected(s.ignoreDlErrors); + + paperAtlas.loadSettings(s); + display.loadSettings(s); + } + + /** + * Reads the user defined settings from the gui and updates the {@link Settings} values according to the read gui + * settings. + */ + private void applySettings() { + Settings s = settings; + + s.unitSystem = (UnitSystem) unitSystem.getSelectedItem(); + s.tileStoreEnabled = tileStoreTab.tileStoreEnabled.isSelected(); + s.tileDefaultExpirationTime = defaultExpirationTime.getTimeMilliValue(); + s.tileMinExpirationTime = minExpirationTime.getTimeMilliValue(); + s.tileMaxExpirationTime = maxExpirationTime.getTimeMilliValue(); + s.maxMapSize = mapSize.getValue(); + s.mapOverlapTiles = (Integer) mapOverlapTiles.getValue(); + + Locale locale = ((SupportLocale) languageCombo.getSelectedItem()).locale; + s.localeLanguage = locale.getLanguage(); + s.localeCountry = locale.getCountry(); + + s.setAtlasOutputDirectory(atlasOutputDirectory.getText()); + int threads = ((Integer) threadCount.getSelectedItem()).intValue(); + s.downloadThreadCount = threads; + + s.setBandwidthLimit(((Bandwidth) bandwidth.getSelectedItem()).limit); + + s.setProxyType((ProxyType) proxyType.getSelectedItem()); + s.setCustomProxyHost(proxyHost.getText()); + s.setCustomProxyPort(proxyPort.getText()); + s.setCustomProxyUserName(proxyUserName.getText()); + s.setCustomProxyPassword(proxyPassword.getText()); + + s.applyProxySettings(); + + Vector disabledMaps = new Vector(); + for (MapSource ms : disabledMapSourcesModel.getVector()) { + disabledMaps.add(ms.getName()); + } + s.mapSourcesDisabled = disabledMaps; + + Vector enabledMaps = new Vector(); + for (MapSource ms : enabledMapSourcesModel.getVector()) { + enabledMaps.add(ms.getName()); + } + s.mapSourcesEnabled = enabledMaps; + + s.ignoreDlErrors = ignoreDlErrors.isSelected(); + + paperAtlas.applySettings(s); + display.applySettings(s); + + if (MainGUI.getMainGUI() == null) + return; + + MainGUI.getMainGUI().updateMapSourcesList(); + + s.osmHikingTicket = osmHikingTicket.getText().trim(); + try { + MainGUI.getMainGUI().checkAndSaveSettings(); + } catch (Exception e) { + log.error("Error saving settings to file", e); + JOptionPane.showMessageDialog(null, String.format(I18nUtils.localizedStringForKey("set_error_saving_msg"), + e.getClass().getSimpleName()), I18nUtils.localizedStringForKey("set_error_saving_title"), + JOptionPane.ERROR_MESSAGE); + } + + MainGUI.getMainGUI().previewMap.repaint(); + } + + private void addListeners() { + + addWindowListener(new WindowCloseListener()); + + okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + applySettings(); + // Close the dialog window + SettingsGUI.this.dispose(); + } + }); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + SettingsGUI.this.dispose(); + } + }); + + tabbedPane.addChangeListener(new ChangeListener() { + + public void stateChanged(ChangeEvent e) { + if (tabbedPane.getSelectedComponent() == null) + return; + // First time the tile store tab is selected start updating the tile store information + if (tabbedPane.getSelectedComponent() == tileStoreTab) { + // if ("Tile store".equals(tabbedPane.getSelectedComponent().getName())) { + tabbedPane.removeChangeListener(this); + tileStoreTab.updateTileStoreInfoPanelAsync(null); + } + } + }); + + KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false); + Action escapeAction = new AbstractAction() { + private static final long serialVersionUID = 1L; + + public void actionPerformed(ActionEvent e) { + SettingsGUI.this.dispose(); + } + }; + getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeKeyStroke, "ESCAPE"); + getRootPane().getActionMap().put("ESCAPE", escapeAction); + } + + public static final TitledBorder createSectionBorder(String title) { + TitledBorder tb = BorderFactory.createTitledBorder(title); + Border border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); + Border margin = new EmptyBorder(3, 3, 3, 3); + tb.setBorder(new CompoundBorder(border, margin)); + return tb; + } + + private class WindowCloseListener extends WindowAdapter { + + @Override + public void windowClosed(WindowEvent event) { + // On close we check if the tile store information retrieval thread + // is still running and if yes we interrupt it + tileStoreTab.stopThread(); + } + + } + + private class MapPacksOnlineUpdateAction implements ActionListener { + + public void actionPerformed(ActionEvent event) { + // try { + // boolean result = MapSourcesUpdater.mapsourcesOnlineUpdate(); + // String msg = (result) ? "Online update successfull" : "No new update avilable"; + // DateFormat df = DateFormat.getDateTimeInstance(); + // Date date = MapSourcesUpdater.getMapSourcesDate(System.getProperties()); + // msg += "\nCurrent map source date: " + df.format(date); + // JOptionPane.showMessageDialog(SettingsGUI.this, msg); + // if (result) + // MainGUI.getMainGUI().refreshPreviewMap(); + // } catch (MapSourcesUpdateException e) { + // JOptionPane.showMessageDialog(SettingsGUI.this, e.getMessage(), "Mapsources online update failed", + // JOptionPane.ERROR_MESSAGE); + // } + MapPackManager mpm; + try { + mpm = new MapPackManager(Settings.getInstance().getMapSourcesDirectory()); + int result = mpm.updateMapPacks(); + switch (result) { + case -1: + JOptionPane.showMessageDialog(SettingsGUI.this, + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_msg_outdate"), + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_no_update"), + JOptionPane.ERROR_MESSAGE); + break; + case 0: + JOptionPane.showMessageDialog(SettingsGUI.this, + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_msg_noneed"), + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_no_update"), + JOptionPane.INFORMATION_MESSAGE); + break; + default: + JOptionPane.showMessageDialog(SettingsGUI.this, String.format( + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_msg_done"), result), + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_done"), + JOptionPane.INFORMATION_MESSAGE); + } + } catch (UpdateFailedException e) { + JOptionPane.showMessageDialog(SettingsGUI.this, e.getMessage(), + I18nUtils.localizedStringForKey("set_mapsrc_config_online_update_failed"), + JOptionPane.ERROR_MESSAGE); + } catch (Exception e) { + Settings.getInstance().mapSourcesUpdate.etag = null; + GUIExceptionHandler.processException(e); + } + } + } + + public static void main(String[] args) { + Logging.configureConsoleLogging(Level.TRACE); + ProgramInfo.initialize(); + DefaultMapSourcesManager.initialize(); + TileStore.initialize(); + StartMOBAC.setLookAndFeel(); + + try { + Settings.load(); + } catch (JAXBException e1) { + e1.printStackTrace(); + } + new SettingsGUI(null); + Runtime.getRuntime().addShutdownHook(new Thread() { + + @Override + public void run() { + try { + Settings.save(); + } catch (JAXBException e) { + e.printStackTrace(); + } + } + + }); + } +} diff --git a/src/main/java/mobac/gui/settings/SettingsGUIPaper.java b/src/main/java/mobac/gui/settings/SettingsGUIPaper.java new file mode 100644 index 0000000..0ee7411 --- /dev/null +++ b/src/main/java/mobac/gui/settings/SettingsGUIPaper.java @@ -0,0 +1,478 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.settings; + +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.io.File; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.border.TitledBorder; +import javax.swing.filechooser.FileFilter; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import mobac.gui.mapview.WgsGrid.WgsDensity; +import mobac.program.model.PaperSize; +import mobac.program.model.PaperSize.Format; +import mobac.program.model.Settings; +import mobac.program.model.SettingsPaperAtlas; +import mobac.program.model.UnitSystem; +import mobac.utilities.GBCTable; +import mobac.utilities.I18nUtils; + +public class SettingsGUIPaper extends JPanel { + private static final long serialVersionUID = -8265562215604604074L; + + private static void setModel(SpinnerNumberModel model, double min, double max, Double step) { + model.setMaximum(max); + model.setMinimum(min); + model.setStepSize(step); + double value = model.getNumber().doubleValue(); + value = Math.max(Math.min(value, max), min); + model.setValue(value); + } + + private static void setEditor(JSpinner jSpinner, String pattern) { + jSpinner.setEditor(new JSpinner.NumberEditor(jSpinner, pattern)); + } + + private static JPanel createSection(TitledBorder border) { + JPanel jPanel = new JPanel(new GridBagLayout()); + jPanel.setBorder(border); + return jPanel; + } + + private final JButton jButtonDefaults = new JButton(); + private final JButton jButtonExport = new JButton(); + private final JButton jButtonImport = new JButton(); + + private final JCheckBox jCheckBoxCompass = new JCheckBox(); + private final JCheckBox jCheckBoxLandscape = new JCheckBox(); + private final JCheckBox jCheckBoxPageNumbers = new JCheckBox("", true); + private final JCheckBox jCheckBoxScaleBar = new JCheckBox(); + private final JCheckBox jCheckBoxWgsGrid = new JCheckBox("", true); + + private final JFileChooser jFileChooser = new JFileChooser(); + + private final TitledBorder titledBorderActions = SettingsGUI.createSectionBorder(""); + private final TitledBorder titledBorderAdditions = SettingsGUI.createSectionBorder(""); + private final TitledBorder titledBorderAdvanced = SettingsGUI.createSectionBorder(""); + private final TitledBorder titledBorderMargins = SettingsGUI.createSectionBorder(""); + private final TitledBorder titledBorderSize = SettingsGUI.createSectionBorder(""); + + private final JPanel jPanelActions = createSection(titledBorderActions), + jPanelAdditions = createSection(titledBorderAdditions), + jPanelAdvanced = createSection(titledBorderAdvanced), jPanelMargins = createSection(titledBorderMargins), + jPanelSize = createSection(titledBorderSize); + + private final JComboBox jComboBoxFormat = new JComboBox(Format.values()); + private final JComboBox jComboBoxWgsDensity = new JComboBox(WgsDensity.values()); + + private final JRadioButton jRadioButtonCustom = new JRadioButton("", true); + private final JRadioButton jRadioButtonDefault = new JRadioButton("", true); + private final JRadioButton jRadioButtonSelection = new JRadioButton("", true); + + private final SpinnerNumberModel modelCompression = new SpinnerNumberModel(SettingsPaperAtlas.COMPRESSION_DEFAULT, + SettingsPaperAtlas.COMPRESSION_MIN, SettingsPaperAtlas.COMPRESSION_MAX, 1); + + private final SpinnerNumberModel modelCrop = new SpinnerNumberModel(SettingsPaperAtlas.CROP_DEFAULT, + SettingsPaperAtlas.CROP_MIN, SettingsPaperAtlas.CROP_MAX, 1), modelDpi = new SpinnerNumberModel( + SettingsPaperAtlas.DPI_DEFAULT, SettingsPaperAtlas.DPI_MIN, SettingsPaperAtlas.DPI_MAX, 1); + + private final SpinnerNumberModel modelHeight = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + private final SpinnerNumberModel modelWidth = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + + private final SpinnerNumberModel modelMarginBottom = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + private final SpinnerNumberModel modelMarginLeft = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + private final SpinnerNumberModel modelMarginRight = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + private final SpinnerNumberModel modelMarginTop = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + + private final SpinnerNumberModel modelOverlap = new SpinnerNumberModel(0.0, 0.0, 0.0, 1.0); + + private final JSpinner jSpinnerDpi = new JSpinner(modelDpi), jSpinnerWidth = new JSpinner(modelWidth), + jSpinnerHeight = new JSpinner(modelHeight), jSpinnerMarginTop = new JSpinner(modelMarginTop), + jSpinnerMarginLeft = new JSpinner(modelMarginLeft), jSpinnerMarginBottom = new JSpinner(modelMarginBottom), + jSpinnerMarginRight = new JSpinner(modelMarginRight), jSpinnerOverlap = new JSpinner(modelOverlap), + jSpinnerCrop = new JSpinner(modelCrop), jSpinnerCompression = new JSpinner(modelCompression); + + private final JLabel jLabelCompression = new JLabel(), jLabelDpi = new JLabel(), jLabelWidth = new JLabel(), + jLabelHeight = new JLabel(), jLabelMarginTop = new JLabel(), jLabelMarginLeft = new JLabel(), + jLabelMarginBottom = new JLabel(), jLabelMarginRight = new JLabel(), jLabelOverlap = new JLabel(), + jLabelCrop = new JLabel(); + + private String importError, exportError, errorReason, errorTitle, xmlFileFilter; + + private UnitSystem unitSystem; + + public SettingsGUIPaper() { + super(new GridBagLayout()); + jSpinnerCrop.setEditor(new JSpinner.NumberEditor(jSpinnerCrop, "#0'%'")); + setUnitSystem(UnitSystem.Metric); + i18n(); + jFileChooser.setFileFilter(new FileFilter() { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().endsWith(".xml"); + } + + @Override + public String getDescription() { + return xmlFileFilter; + } + }); + jButtonImport.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + importFromXml(); + } + }); + jButtonExport.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + exportToXml(); + } + }); + jButtonDefaults.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + resetToDefaults(); + } + }); + jComboBoxFormat.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + Format format = (Format) jComboBoxFormat.getSelectedItem(); + double width = unitSystem.pointsToUnits(format.width); + double height = unitSystem.pointsToUnits(format.height); + modelWidth.setValue(width); + modelHeight.setValue(height); + } + }); + jCheckBoxWgsGrid.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + boolean enabled = e.getStateChange() != ItemEvent.DESELECTED; + jComboBoxWgsDensity.setEnabled(enabled); + } + }); + jRadioButtonCustom.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + boolean enabled = e.getStateChange() != ItemEvent.DESELECTED; + jLabelWidth.setEnabled(enabled); + jLabelHeight.setEnabled(enabled); + jSpinnerWidth.setEnabled(enabled); + jSpinnerHeight.setEnabled(enabled); + } + }); + jRadioButtonDefault.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + boolean enabled = e.getStateChange() != ItemEvent.DESELECTED; + jComboBoxFormat.setEnabled(enabled); + jCheckBoxLandscape.setEnabled(enabled); + } + }); + + ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(jRadioButtonSelection); + buttonGroup.add(jRadioButtonDefault); + buttonGroup.add(jRadioButtonCustom); + + GBCTable gbc = new GBCTable(); + + jPanelSize.add(jRadioButtonDefault, gbc.begin()); + jPanelSize.add(jRadioButtonCustom, gbc.incY()); + jPanelSize.add(jRadioButtonSelection, gbc.incY()); + jPanelSize.add(jComboBoxFormat, gbc.incX()); + jPanelSize.add(jLabelWidth, gbc.incY()); + jPanelSize.add(jLabelHeight, gbc.incY()); + jPanelSize.add(jCheckBoxLandscape, gbc.incX()); + jPanelSize.add(jSpinnerWidth, gbc.incY()); + jPanelSize.add(jSpinnerHeight, gbc.incY()); + jPanelSize.add(Box.createGlue(), gbc.incX().gridheight(3).fill()); + + jPanelMargins.add(jLabelMarginTop, gbc.begin()); + jPanelMargins.add(jLabelMarginBottom, gbc.incY()); + jPanelMargins.add(jSpinnerMarginTop, gbc.incX()); + jPanelMargins.add(jSpinnerMarginBottom, gbc.incY()); + jPanelMargins.add(jLabelMarginLeft, gbc.incX()); + jPanelMargins.add(jLabelMarginRight, gbc.incY()); + jPanelMargins.add(jSpinnerMarginLeft, gbc.incX()); + jPanelMargins.add(jSpinnerMarginRight, gbc.incY()); + jPanelMargins.add(Box.createHorizontalGlue(), gbc.incX().fillH()); + + jPanelAdditions.add(jCheckBoxWgsGrid, gbc.begin()); + jPanelAdditions.add(jCheckBoxPageNumbers, gbc.incY().gridwidth(2)); + jPanelAdditions.add(jCheckBoxScaleBar, gbc.incY()); + jPanelAdditions.add(jComboBoxWgsDensity, gbc.incX()); + gbc.incY(); + jPanelAdditions.add(jCheckBoxCompass, gbc.incY()); + jPanelAdditions.add(Box.createHorizontalGlue(), gbc.incX().fillH()); + + jPanelAdvanced.add(jLabelDpi, gbc.begin()); + jPanelAdvanced.add(jLabelCompression, gbc.incY()); + jPanelAdvanced.add(jSpinnerDpi, gbc.incX()); + jPanelAdvanced.add(jSpinnerCompression, gbc.incY()); + jPanelAdvanced.add(jLabelOverlap, gbc.incX()); + jPanelAdvanced.add(jLabelCrop, gbc.incY()); + jPanelAdvanced.add(jSpinnerOverlap, gbc.incX()); + jPanelAdvanced.add(jSpinnerCrop, gbc.incY()); + jPanelAdvanced.add(Box.createHorizontalGlue(), gbc.incX().fillH()); + + jPanelActions.add(jButtonImport, gbc.begin()); + jPanelActions.add(jButtonExport, gbc.incX()); + jPanelActions.add(jButtonDefaults, gbc.incX()); + jPanelActions.add(Box.createHorizontalGlue(), gbc.incX().fillH()); + + gbc = new GBCTable(0); + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + add(jPanelSize, gbc.begin()); + add(jPanelMargins, gbc.incY()); + add(jPanelAdditions, gbc.incX().fillH()); + add(jPanelAdvanced, gbc.incY().fillH()); + add(jPanelActions, gbc.begin(1, 3).gridwidth(2).fillH()); + add(Box.createGlue(), gbc.incY().gridwidth(2).fill()); + } + + private void importFromXml() { + int state = jFileChooser.showOpenDialog(SettingsGUIPaper.this); + if (state == JFileChooser.APPROVE_OPTION) { + File file = jFileChooser.getSelectedFile(); + JAXBContext context; + try { + context = JAXBContext.newInstance(SettingsPaperAtlas.class); + Unmarshaller um = context.createUnmarshaller(); + SettingsPaperAtlas s = (SettingsPaperAtlas) um.unmarshal(file); + loadSettings(s); + } catch (JAXBException ex) { + String text = importError + file.getName() + "\n" + errorReason + ex.getMessage(); + JOptionPane.showMessageDialog(SettingsGUIPaper.this, text, errorTitle, JOptionPane.ERROR_MESSAGE); + } + } + } + + private void exportToXml() { + int state = jFileChooser.showSaveDialog(SettingsGUIPaper.this); + if (state == JFileChooser.APPROVE_OPTION) { + File file = jFileChooser.getSelectedFile(); + JAXBContext context; + try { + context = JAXBContext.newInstance(SettingsPaperAtlas.class); + Marshaller m = context.createMarshaller(); + SettingsPaperAtlas s = new SettingsPaperAtlas(); + applySettings(s); + m.marshal(s, file); + } catch (JAXBException ex) { + String text = exportError + file.getName() + "\n" + errorReason + ex.getMessage(); + JOptionPane.showMessageDialog(SettingsGUIPaper.this, text, errorTitle, JOptionPane.ERROR_MESSAGE); + } + } + } + + private void resetToDefaults() { + loadSettings(new SettingsPaperAtlas()); + } + + private void i18n() { + setName(I18nUtils.localizedStringForKey("set_paper_title")); + titledBorderActions.setTitle(I18nUtils.localizedStringForKey("set_paper_actions")); + titledBorderAdditions.setTitle(I18nUtils.localizedStringForKey("set_paper_additions")); + titledBorderAdvanced.setTitle(I18nUtils.localizedStringForKey("set_paper_advanced")); + titledBorderMargins.setTitle(I18nUtils.localizedStringForKey("set_paper_margins")); + titledBorderSize.setTitle(I18nUtils.localizedStringForKey("set_paper_size")); + jComboBoxFormat.setToolTipText(I18nUtils.localizedStringForKey("set_paper_size_default_format_tips")); + jComboBoxWgsDensity.setToolTipText(I18nUtils.localizedStringForKey("set_paper_wgs_grid_density_tips")); + jRadioButtonSelection.setText(I18nUtils.localizedStringForKey("set_paper_size_selection")); + jRadioButtonSelection.setToolTipText(I18nUtils.localizedStringForKey("set_paper_size_selection_tips")); + jRadioButtonDefault.setText(I18nUtils.localizedStringForKey("set_paper_size_default")); + jRadioButtonDefault.setToolTipText(I18nUtils.localizedStringForKey("set_paper_size_default_tips")); + jRadioButtonCustom.setText(I18nUtils.localizedStringForKey("set_paper_size_custom")); + jRadioButtonCustom.setToolTipText(I18nUtils.localizedStringForKey("set_paper_size_custom_tips")); + jCheckBoxScaleBar.setText(I18nUtils.localizedStringForKey("set_paper_scale_bar")); + jCheckBoxScaleBar.setToolTipText(I18nUtils.localizedStringForKey("set_paper_scale_tips")); + jCheckBoxCompass.setText(I18nUtils.localizedStringForKey("set_paper_compass")); + jCheckBoxCompass.setToolTipText(I18nUtils.localizedStringForKey("set_paper_compass_tips")); + jCheckBoxLandscape.setText(I18nUtils.localizedStringForKey("set_paper_size_default_landscape")); + jCheckBoxLandscape.setToolTipText(I18nUtils.localizedStringForKey("set_paper_size_default_landscape_tips")); + jCheckBoxWgsGrid.setText(I18nUtils.localizedStringForKey(I18nUtils.localizedStringForKey("set_paper_wgs_grid"))); + jCheckBoxWgsGrid.setToolTipText(I18nUtils.localizedStringForKey("set_paper_wgs_grid_tips")); + jCheckBoxPageNumbers.setText(I18nUtils.localizedStringForKey("set_paper_paper_nubmer")); + jCheckBoxPageNumbers.setToolTipText(I18nUtils.localizedStringForKey("set_paper_paper_nubmer_tips")); + jLabelCompression.setText(I18nUtils.localizedStringForKey("set_paper_advanced_compression")); + String compression = I18nUtils.localizedStringForKey("set_paper_advanced_compression_tips"); + jLabelCompression.setToolTipText(compression); + jSpinnerCompression.setToolTipText(compression); + jLabelDpi.setText(I18nUtils.localizedStringForKey("set_paper_advanced_dpi")); + String dpi = I18nUtils.localizedStringForKey("set_paper_advanced_dpi_tips"); + jLabelDpi.setToolTipText(dpi); + jSpinnerDpi.setToolTipText(dpi); + jLabelWidth.setText(I18nUtils.localizedStringForKey("set_paper_size_custom_width")); + String width = I18nUtils.localizedStringForKey("set_paper_size_custom_width_tips"); + jLabelWidth.setToolTipText(width); + jSpinnerWidth.setToolTipText(width); + jLabelHeight.setText(I18nUtils.localizedStringForKey("set_paper_size_custom_height")); + String height = I18nUtils.localizedStringForKey("set_paper_size_custom_height_tips"); + jLabelHeight.setToolTipText(height); + jSpinnerHeight.setToolTipText(height); + String margin = I18nUtils.localizedStringForKey("set_paper_margins_tips"); + jLabelMarginTop.setText(I18nUtils.localizedStringForKey("set_paper_margins_top")); + jLabelMarginTop.setToolTipText(margin); + jSpinnerMarginTop.setToolTipText(margin); + jLabelMarginLeft.setText(I18nUtils.localizedStringForKey("set_paper_margins_left")); + jLabelMarginLeft.setToolTipText(margin); + jSpinnerMarginLeft.setToolTipText(margin); + jLabelMarginBottom.setText(I18nUtils.localizedStringForKey("set_paper_margins_bottom")); + jLabelMarginBottom.setToolTipText(margin); + jSpinnerMarginBottom.setToolTipText(margin); + jLabelMarginRight.setText(I18nUtils.localizedStringForKey("set_paper_margins_right")); + jLabelMarginRight.setToolTipText(margin); + jSpinnerMarginRight.setToolTipText(margin); + jLabelOverlap.setText(I18nUtils.localizedStringForKey("set_paper_advanced_overlap")); + String overlap = I18nUtils.localizedStringForKey("set_paper_advanced_overlap_tips"); + jLabelOverlap.setToolTipText(overlap); + jSpinnerOverlap.setToolTipText(overlap); + jLabelCrop.setText(I18nUtils.localizedStringForKey("set_paper_advanced_crop")); + String crop = I18nUtils.localizedStringForKey("set_paper_advanced_crop_tips"); + jLabelCrop.setToolTipText(crop); + jSpinnerCrop.setToolTipText(crop); + jButtonImport.setText(I18nUtils.localizedStringForKey("set_paper_actions_import_xml")); + jButtonImport.setToolTipText(I18nUtils.localizedStringForKey("set_paper_actions_import_xml_tip")); + jButtonExport.setText(I18nUtils.localizedStringForKey("set_paper_actions_export_xml")); + jButtonExport.setToolTipText(I18nUtils.localizedStringForKey("set_paper_actions_export_xml_tip")); + jButtonDefaults.setText(I18nUtils.localizedStringForKey("set_paper_actions_restore_default")); + jButtonDefaults.setToolTipText(I18nUtils.localizedStringForKey("set_paper_actions_restore_default_tips")); + importError = I18nUtils.localizedStringForKey("set_paper_actions_error_import"); + exportError = I18nUtils.localizedStringForKey("set_paper_actions_error_export"); + errorReason = I18nUtils.localizedStringForKey("set_paper_actions_error_reason"); + errorTitle = I18nUtils.localizedStringForKey("set_paper_actions_error_title"); + xmlFileFilter = I18nUtils.localizedStringForKey("set_paper_actions_xml_filter"); + } + + private void setUnitSystem(UnitSystem unitSystem) { + if (unitSystem.equals(this.unitSystem)) + return; + this.unitSystem = unitSystem; + Double step = 0.1d; + double min, max; + min = unitSystem.pointsToUnits(SettingsPaperAtlas.MARGIN_MIN); + max = unitSystem.pointsToUnits(SettingsPaperAtlas.MARGIN_MAX); + setModel(modelMarginBottom, min, max, step); + setModel(modelMarginLeft, min, max, step); + setModel(modelMarginRight, min, max, step); + setModel(modelMarginTop, min, max, step); + min = unitSystem.pointsToUnits(SettingsPaperAtlas.PAPER_SIZE_MIN); + max = unitSystem.pointsToUnits(SettingsPaperAtlas.PAPER_SIZE_MAX); + setModel(modelWidth, min, max, step); + setModel(modelHeight, min, max, step); + min = unitSystem.pointsToUnits(SettingsPaperAtlas.OVERLAP_MIN); + max = unitSystem.pointsToUnits(SettingsPaperAtlas.OVERLAP_MAX); + setModel(modelOverlap, min, max, step); + String pattern = "#0.00 " + unitSystem.unitTiny; + setEditor(jSpinnerWidth, pattern); + setEditor(jSpinnerHeight, pattern); + setEditor(jSpinnerMarginTop, pattern); + setEditor(jSpinnerMarginLeft, pattern); + setEditor(jSpinnerMarginBottom, pattern); + setEditor(jSpinnerMarginRight, pattern); + setEditor(jSpinnerOverlap, pattern); + } + + private PaperSize getPaperSize() { + if (jRadioButtonDefault.isSelected()) { + Format format = (Format) jComboBoxFormat.getSelectedItem(); + boolean landscape = jCheckBoxLandscape.isSelected(); + return new PaperSize(format, landscape); + } + if (jRadioButtonCustom.isSelected()) { + double width = modelWidth.getNumber().doubleValue(); + double height = modelHeight.getNumber().doubleValue(); + width = unitSystem.unitsToPoints(width); + height = unitSystem.unitsToPoints(height); + return new PaperSize(width, height); + } + return null; + } + + private void setPaperSize(PaperSize paperSize) { + if (paperSize == null) { + jRadioButtonSelection.setSelected(true); + return; + } + if (paperSize.format != null) { + jRadioButtonDefault.setSelected(true); + jComboBoxFormat.setSelectedIndex(paperSize.format.ordinal()); + jCheckBoxLandscape.setSelected(paperSize.landscape); + } else { + jRadioButtonCustom.setSelected(true); + } + } + + public void loadSettings(Settings s) { + setUnitSystem(s.unitSystem); + loadSettings(s.paperAtlas); + } + + public void loadSettings(SettingsPaperAtlas s) { + setPaperSize(s.paperSize); + modelMarginTop.setValue(unitSystem.pointsToUnits(s.marginTop)); + modelMarginLeft.setValue(unitSystem.pointsToUnits(s.marginLeft)); + modelMarginBottom.setValue(unitSystem.pointsToUnits(s.marginBottom)); + modelMarginRight.setValue(unitSystem.pointsToUnits(s.marginRight)); + jCheckBoxScaleBar.setSelected(s.scaleBar); + jCheckBoxCompass.setSelected(s.compass); + jComboBoxWgsDensity.setSelectedItem(s.wgsDensity); + jCheckBoxWgsGrid.setSelected(s.wgsEnabled); + jCheckBoxPageNumbers.setSelected(s.pageNumbers); + modelCrop.setValue(s.crop); + modelOverlap.setValue(unitSystem.pointsToUnits(s.overlap)); + modelCompression.setValue(s.compression); + modelDpi.setValue(s.dpi); + } + + public void applySettings(Settings s) { + applySettings(s.paperAtlas); + } + + public void applySettings(SettingsPaperAtlas s) { + s.paperSize = getPaperSize(); + s.marginTop = unitSystem.unitsToPoints(modelMarginTop.getNumber().doubleValue()); + s.marginLeft = unitSystem.unitsToPoints(modelMarginLeft.getNumber().doubleValue()); + s.marginBottom = unitSystem.unitsToPoints(modelMarginBottom.getNumber().doubleValue()); + s.marginRight = unitSystem.unitsToPoints(modelMarginRight.getNumber().doubleValue()); + s.scaleBar = jCheckBoxScaleBar.isSelected(); + s.compass = jCheckBoxCompass.isSelected(); + s.wgsDensity = (WgsDensity) jComboBoxWgsDensity.getSelectedItem(); + s.wgsEnabled = jCheckBoxWgsGrid.isSelected(); + s.pageNumbers = jCheckBoxPageNumbers.isSelected(); + s.crop = modelCrop.getNumber().intValue(); + s.overlap = unitSystem.unitsToPoints(modelOverlap.getNumber().doubleValue()); + s.compression = modelCompression.getNumber().intValue(); + s.dpi = modelDpi.getNumber().intValue(); + } +} diff --git a/src/main/java/mobac/gui/settings/SettingsGUITileStore.java b/src/main/java/mobac/gui/settings/SettingsGUITileStore.java new file mode 100644 index 0000000..1a8b7a4 --- /dev/null +++ b/src/main/java/mobac/gui/settings/SettingsGUITileStore.java @@ -0,0 +1,261 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.settings; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; +import javax.swing.border.BevelBorder; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreInfo; +import mobac.program.tilestore.berkeleydb.DelayedInterruptThread; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +public class SettingsGUITileStore extends JPanel { + + public final JCheckBox tileStoreEnabled; + private final JPanel tileStoreInfoPanel; + + private List tileStoreInfoList = new LinkedList(); + private JLabel totalTileCountLabel; + private JLabel totalTileSizeLabel; + + protected DelayedInterruptThread tileStoreAsyncThread = null; + + public SettingsGUITileStore(SettingsGUI gui) { + super(); + + gui.addTab(I18nUtils.localizedStringForKey("set_tile_store_title"), this); + + tileStoreEnabled = new JCheckBox(I18nUtils.localizedStringForKey("set_tile_store_enable_checkbox")); + + JPanel tileStorePanel = new JPanel(new BorderLayout()); + tileStorePanel.setBorder(SettingsGUI.createSectionBorder(I18nUtils.localizedStringForKey("set_tile_store_settings"))); + tileStorePanel.add(tileStoreEnabled, BorderLayout.CENTER); + tileStoreInfoPanel = new JPanel(new GridBagLayout()); + // tileStoreInfoPanel.setBorder(createSectionBorder("Information")); + + prepareTileStoreInfoPanel(); + + setLayout(new BorderLayout()); + add(tileStorePanel, BorderLayout.NORTH); + JScrollPane scrollPane = new JScrollPane(tileStoreInfoPanel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + tileStoreInfoPanel.setMinimumSize(new Dimension(200, 300)); + // scrollPane.setMinimumSize(new Dimension(100, 100)); + scrollPane.setPreferredSize(new Dimension(520, 100)); + scrollPane.setBorder(SettingsGUI.createSectionBorder(I18nUtils.localizedStringForKey("set_tile_store_information"))); + + add(scrollPane, BorderLayout.CENTER); + } + + /** + * + * @param updateStoreName + * name of the tile store to update or null in case of all tile stores to be updated + */ + private void updateTileStoreInfoPanel(String updateStoreName) { + try { + TileStore tileStore = TileStore.getInstance(); + + long totalTileCount = 0; + long totalTileSize = 0; + for (final TileSourceInfoComponents info : tileStoreInfoList) { + String storeName = info.name; + Utilities.checkForInterruption(); + int count; + long size; + if (updateStoreName == null || info.name.equals(updateStoreName)) { + TileStoreInfo tsi = tileStore.getStoreInfo(storeName); + count = tsi.getTileCount(); + size = tsi.getStoreSize(); + info.count = count; + info.size = size; + final String mapTileCountText = (count < 0) ? "??" : Integer.toString(count); + final String mapTileSizeText = Utilities.formatBytes(size); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + info.countLabel.setText("" + mapTileCountText + ""); + info.sizeLabel.setText("" + mapTileSizeText + ""); + } + }); + } else { + count = info.count; + size = info.size; + } + totalTileCount += count; + totalTileSize += size; + } + final String totalTileCountText = "" + Long.toString(totalTileCount) + ""; + final String totalTileSizeText = "" + Utilities.formatBytes(totalTileSize) + ""; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + totalTileCountLabel.setText(totalTileCountText); + totalTileSizeLabel.setText(totalTileSizeText); + } + }); + } catch (InterruptedException e) { + SettingsGUI.log.debug("Tile store information retrieval was canceled"); + } + + } + + synchronized void updateTileStoreInfoPanelAsync(final String storeName) { + if (tileStoreAsyncThread != null) + return; // An update is currently running + tileStoreAsyncThread = new DelayedInterruptThread("TileStoreInfoRetriever") { + + @Override + public void run() { + if (storeName == null) + SettingsGUI.log.debug("Updating tilestore information in background"); + else + SettingsGUI.log.debug("Updating tilestore information for \"" + storeName + "\" in background"); + updateTileStoreInfoPanel(storeName); + SettingsGUI.log.debug("Updating tilestore information finished"); + tileStoreAsyncThread = null; + } + }; + tileStoreAsyncThread.start(); + } + + private void prepareTileStoreInfoPanel() { + + final GridBagConstraints gbc_mapSource = new GridBagConstraints(); + gbc_mapSource.insets = new Insets(5, 10, 5, 10); + gbc_mapSource.anchor = GridBagConstraints.WEST; + final GridBagConstraints gbc_mapTiles = new GridBagConstraints(); + gbc_mapTiles.insets = gbc_mapSource.insets; + gbc_mapTiles.anchor = GridBagConstraints.EAST; + final GridBagConstraints gbc_eol = new GridBagConstraints(); + gbc_eol.gridwidth = GridBagConstraints.REMAINDER; + + TileStore tileStore = TileStore.getInstance(); + MapSourcesManager mapSourcesManager = MapSourcesManager.getInstance(); + + tileStoreInfoPanel.add(new JLabel(I18nUtils.localizedStringForKey("set_tile_store_info_mapsrc")), gbc_mapSource); + tileStoreInfoPanel.add(new JLabel(I18nUtils.localizedStringForKey("set_tile_store_info_tiles")), gbc_mapTiles); + tileStoreInfoPanel.add(new JLabel(I18nUtils.localizedStringForKey("set_tile_store_info_size")), gbc_eol); + + ImageIcon trash = Utilities.loadResourceImageIcon("trash.png"); + + for (String name : tileStore.getAllStoreNames()) { + String mapTileCountText = " ? "; + String mapTileSizeText = " ? "; + MapSource mapSource = mapSourcesManager.getSourceByName(name); + final JLabel mapSourceNameLabel; + if (mapSource != null) + mapSourceNameLabel = new JLabel(name); + else + mapSourceNameLabel = new JLabel(name + I18nUtils.localizedStringForKey("set_tile_store_info_disabled_subfix")); + final JLabel mapTileCountLabel = new JLabel(mapTileCountText); + final JLabel mapTileSizeLabel = new JLabel(mapTileSizeText); + final JButton deleteButton = new JButton(trash); + TileSourceInfoComponents info = new TileSourceInfoComponents(); + info.name = name; + info.countLabel = mapTileCountLabel; + info.sizeLabel = mapTileSizeLabel; + tileStoreInfoList.add(info); + deleteButton.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + deleteButton.setToolTipText(String.format(I18nUtils.localizedStringForKey("set_tile_store_info_delete_tips"), name)); + deleteButton.addActionListener(new ClearTileCacheAction(name)); + + tileStoreInfoPanel.add(mapSourceNameLabel, gbc_mapSource); + tileStoreInfoPanel.add(mapTileCountLabel, gbc_mapTiles); + tileStoreInfoPanel.add(mapTileSizeLabel, gbc_mapTiles); + tileStoreInfoPanel.add(deleteButton, gbc_eol); + } + JSeparator hr = new JSeparator(JSeparator.HORIZONTAL); + hr.setBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.fill = GridBagConstraints.HORIZONTAL; + tileStoreInfoPanel.add(hr, gbc); + + JLabel totalMapLabel = new JLabel(I18nUtils.localizedStringForKey("set_tile_store_info_total")); + totalTileCountLabel = new JLabel("??"); + totalTileSizeLabel = new JLabel("??"); + tileStoreInfoPanel.add(totalMapLabel, gbc_mapSource); + tileStoreInfoPanel.add(totalTileCountLabel, gbc_mapTiles); + tileStoreInfoPanel.add(totalTileSizeLabel, gbc_mapTiles); + } + + public void stopThread() { + Thread t = tileStoreAsyncThread; + if (t != null) + t.interrupt(); + } + + private static class TileSourceInfoComponents { + JLabel sizeLabel; + JLabel countLabel; + String name; + + int count = -1; + long size = 0; + } + + private class ClearTileCacheAction implements ActionListener { + + String storeName; + + public ClearTileCacheAction(String storeName) { + this.storeName = storeName; + } + + public void actionPerformed(ActionEvent e) { + final JButton b = (JButton) e.getSource(); + b.setEnabled(false); + b.setToolTipText(I18nUtils.localizedStringForKey("set_tile_store_info_deleteing_tips")); + Thread t = new DelayedInterruptThread("TileStore_" + storeName + "_DeleteThread") { + + @Override + public void run() { + try { + TileStore ts = TileStore.getInstance(); + ts.clearStore(storeName); + SettingsGUITileStore.this.updateTileStoreInfoPanelAsync(storeName); + SettingsGUITileStore.this.repaint(); + } catch (Exception e) { + SettingsGUI.log.error("An error occured while cleaning tile cache: ", e); + } + } + }; + t.start(); + } + } +} diff --git a/src/main/java/mobac/gui/settings/SettingsGUIWgsGrid.java b/src/main/java/mobac/gui/settings/SettingsGUIWgsGrid.java new file mode 100644 index 0000000..44e13b8 --- /dev/null +++ b/src/main/java/mobac/gui/settings/SettingsGUIWgsGrid.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.gui.settings; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; + +import mobac.gui.dialogs.FontChooser; +import mobac.program.model.Settings; +import mobac.program.model.SettingsWgsGrid; +import mobac.utilities.GBCTable; +import mobac.utilities.I18nUtils; + +public class SettingsGUIWgsGrid extends JPanel { + + private static final long serialVersionUID = -3067609813682787669L; + + private final FontChooser fontChooser = new FontChooser(); + + private final JButton jButtonFont = new JButton(FontChooser.encodeFont(FontChooser.DEFAULT)); + + private final JCheckBox jCheckBoxCompressLabels = new JCheckBox(); + + private final JPanel jPanelColor = new JPanel(); + + private final SpinnerNumberModel modelWidth = new SpinnerNumberModel(0.5d, 0.5d, 5.0d, 0.5d); + + private final JSpinner jSpinnerWidth = new JSpinner(modelWidth); + + private JLabel jLabelColor = new JLabel(), jLabelFont = new JLabel(), jLabelWidth = new JLabel(); + + private String title; + + public SettingsGUIWgsGrid() { + super(new GridBagLayout()); + i18n(); + + jButtonFont.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + fontChooser.show(); + if (fontChooser.wasCanceled()) { + return; + } + String text = FontChooser.encodeFont(fontChooser.getFont()); + jButtonFont.setText(text); + } + }); + + jPanelColor.setPreferredSize(new Dimension(64, 18)); + jPanelColor.setOpaque(true); + jPanelColor.setBorder(BorderFactory.createEtchedBorder()); + jPanelColor.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + Color color = jPanelColor.getBackground(); + color = JColorChooser.showDialog(jPanelColor, title, color); + if (color != null) { + jPanelColor.setBackground(color); + } + } + + public void mouseEntered(MouseEvent e) { + jPanelColor.setBorder(BorderFactory.createRaisedBevelBorder()); + } + + public void mouseExited(MouseEvent e) { + jPanelColor.setBorder(BorderFactory.createEtchedBorder()); + } + }); + + GBCTable gbc = new GBCTable(); + add(jLabelColor, gbc.begin()); + add(jLabelWidth, gbc.incY()); + add(jPanelColor, gbc.incX()); + add(jSpinnerWidth, gbc.incY()); + add(jLabelFont, gbc.incX()); + add(jCheckBoxCompressLabels, gbc.incY().gridwidth(3)); + add(jButtonFont, gbc.incX()); + add(Box.createHorizontalGlue(), gbc.incX().fillH()); + } + + public void i18n() { + jCheckBoxCompressLabels.setText(I18nUtils.localizedStringForKey("set_display_grid_compress")); + jCheckBoxCompressLabels.setToolTipText(I18nUtils.localizedStringForKey("set_display_grid_compress_tips")); + setBorder(SettingsGUI.createSectionBorder(I18nUtils.localizedStringForKey("set_display_grid"))); + title = I18nUtils.localizedStringForKey("set_display_grid_title");//TODO: recovery + jLabelWidth.setText(I18nUtils.localizedStringForKey("set_display_grid_width")); + String width = I18nUtils.localizedStringForKey("set_display_grid_width_tips"); + jLabelWidth.setToolTipText(width); + jSpinnerWidth.setToolTipText(width); + jLabelColor.setText(I18nUtils.localizedStringForKey("set_display_grid_color")); + String color = I18nUtils.localizedStringForKey("set_display_grid_color_tips"); + jLabelColor.setToolTipText(color); + jPanelColor.setToolTipText(color); + jLabelFont.setText(I18nUtils.localizedStringForKey("set_display_grid_font")); + String font = I18nUtils.localizedStringForKey("set_display_grid_font_tips"); + jLabelFont.setToolTipText(font); + jButtonFont.setToolTipText(font); + } + + public void applySettings(Settings s) { + applySettings(s.wgsGrid); + } + + public void applySettings(SettingsWgsGrid s) { + s.compressLabels = jCheckBoxCompressLabels.isSelected(); + s.font = fontChooser.getFont(); + s.color = jPanelColor.getBackground(); + s.width = modelWidth.getNumber().floatValue(); + } + + public void loadSettings(Settings s) { + loadSettings(s.wgsGrid); + } + + public void loadSettings(SettingsWgsGrid s) { + jCheckBoxCompressLabels.setSelected(s.compressLabels); + fontChooser.setFont(s.font); + jButtonFont.setText(FontChooser.encodeFont(s.font)); + jPanelColor.setBackground(s.color); + modelWidth.setValue((double) s.width); + } +} diff --git a/src/main/java/mobac/mapsources/AbstractHttpMapSource.java b/src/main/java/mobac/mapsources/AbstractHttpMapSource.java new file mode 100644 index 0000000..263b9ae --- /dev/null +++ b/src/main/java/mobac/mapsources/AbstractHttpMapSource.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapSourceInitializationException; +import mobac.exceptions.TileException; +import mobac.gui.mapview.JMapViewer; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.download.TileDownLoader; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSourceListener; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreEntry; + +import org.apache.log4j.Logger; + +/** + * Abstract base class for map sources. + */ +public abstract class AbstractHttpMapSource implements HttpMapSource { + + protected Logger log; + private boolean initialized = false; + protected String name; + protected int minZoom; + protected int maxZoom; + protected TileImageType tileType; + protected HttpMapSource.TileUpdate tileUpdate; + protected MapSpace mapSpace = MercatorPower2MapSpace.INSTANCE_256; + protected MapSourceLoaderInfo loaderInfo = null; + + public AbstractHttpMapSource(String name, int minZoom, int maxZoom, TileImageType tileType) { + this(name, minZoom, maxZoom, tileType, HttpMapSource.TileUpdate.None); + } + + /** + * Do not use - for JAXB only + */ + protected AbstractHttpMapSource() { + } + + public AbstractHttpMapSource(String name, int minZoom, int maxZoom, TileImageType tileType, + HttpMapSource.TileUpdate tileUpdate) { + log = Logger.getLogger(this.getClass()); + this.name = name; + this.minZoom = minZoom; + this.maxZoom = Math.min(maxZoom, JMapViewer.MAX_ZOOM); + this.tileType = tileType; + this.tileUpdate = tileUpdate; + } + + public boolean ignoreContentMismatch() + { + return false; + } + + public HttpURLConnection getTileUrlConnection(int zoom, int tilex, int tiley) throws IOException { + String url = getTileUrl(zoom, tilex, tiley); + if (url == null) + return null; + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + prepareTileUrlConnection(conn); + return conn; + } + + protected void prepareTileUrlConnection(HttpURLConnection conn) { + // Derived classes may override this method + } + + public abstract String getTileUrl(int zoom, int tilex, int tiley); + + /** + * Can be used to e.g. retrieve the url pattern before the first call + */ + protected final void initializeHttpMapSource() { + if (initialized) + return; + // Prevent multiple initializations in case of multi-threaded access + try { + synchronized (this) { + if (initialized) + // Another thread has already completed initialization while this one was blocked + return; + initernalInitialize(); + initialized = true; + log.debug("Map source has been initialized"); + } + } catch (Exception e) { + log.error("Map source initialization failed: " + e.getMessage(), e); + // TODO: inform user + } + initialized = true; + } + + protected void initernalInitialize() throws MapSourceInitializationException { + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (loadMethod == LoadMethod.CACHE) { + TileStoreEntry entry = TileStore.getInstance().getTile(x, y, zoom, this); + if (entry == null) + return null; + byte[] data = entry.getData(); + if (Thread.currentThread() instanceof MapSourceListener) { + ((MapSourceListener) Thread.currentThread()).tileDownloaded(data.length); + } + return data; + } else if (loadMethod == LoadMethod.SOURCE) { + initializeHttpMapSource(); + return TileDownLoader.downloadTileAndUpdateStore(x, y, zoom, this); + } else { + initializeHttpMapSource(); + return TileDownLoader.getImage(x, y, zoom, this); + } + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + byte[] data = getTileData(zoom, x, y, loadMethod); + if (data == null) + return null; + return ImageIO.read(new ByteArrayInputStream(data)); + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + public String getStoreName() { + return name; + } + + @Override + public String toString() { + return name; + } + + public TileImageType getTileImageType() { + return tileType; + } + + public HttpMapSource.TileUpdate getTileUpdate() { + return tileUpdate; + } + + public boolean allowFileStore() { + return true; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public Color getBackgroundColor() { + return Color.BLACK; + } + + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + if (this.loaderInfo != null) + throw new RuntimeException("LoaderInfo already set"); + this.loaderInfo = loaderInfo; + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (!(obj instanceof MapSource)) + return false; + MapSource other = (MapSource) obj; + return other.getName().equals(getName()); + } + +} diff --git a/src/main/java/mobac/mapsources/AbstractMultiLayerMapSource.java b/src/main/java/mobac/mapsources/AbstractMultiLayerMapSource.java new file mode 100644 index 0000000..d5f19d6 --- /dev/null +++ b/src/main/java/mobac/mapsources/AbstractMultiLayerMapSource.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.xml.bind.annotation.XmlTransient; + +import mobac.exceptions.TileException; +import mobac.gui.mapview.PreviewMap; +import mobac.program.interfaces.InitializableMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; + +import org.apache.log4j.Logger; + +public abstract class AbstractMultiLayerMapSource implements InitializableMapSource, Iterable { + + protected Logger log; + + protected String name = ""; + protected TileImageType tileType = TileImageType.PNG; + protected MapSource[] mapSources; + + private int maxZoom; + private int minZoom; + private MapSpace mapSpace; + protected MapSourceLoaderInfo loaderInfo = null; + + public AbstractMultiLayerMapSource(String name, TileImageType tileImageType) { + this(); + this.name = name; + this.tileType = tileImageType; + } + + protected AbstractMultiLayerMapSource() { + log = Logger.getLogger(this.getClass()); + } + + protected void initializeValues() { + MapSource refMapSource = mapSources[0]; + mapSpace = refMapSource.getMapSpace(); + maxZoom = PreviewMap.MAX_ZOOM; + minZoom = 0; + for (MapSource ms : mapSources) { + maxZoom = Math.min(maxZoom, ms.getMaxZoom()); + minZoom = Math.max(minZoom, ms.getMinZoom()); + if (!ms.getMapSpace().equals(mapSpace)) + throw new RuntimeException("Different map spaces used in multi-layer map source"); + } + } + + @Override + public void initialize() { + MapSource refMapSource = mapSources[0]; + mapSpace = refMapSource.getMapSpace(); + maxZoom = PreviewMap.MAX_ZOOM; + minZoom = 0; + for (MapSource ms : mapSources) { + if (ms instanceof InitializableMapSource) + ((InitializableMapSource) ms).initialize(); + maxZoom = Math.min(maxZoom, ms.getMaxZoom()); + minZoom = Math.max(minZoom, ms.getMinZoom()); + } + } + + public MapSource[] getLayerMapSources() { + return mapSources; + } + + public Color getBackgroundColor() { + return Color.BLACK; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + public String getStoreName() { + return null; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, InterruptedException, + TileException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(16000); + BufferedImage image = getTileImage(zoom, x, y, loadMethod); + if (image == null) + return null; + ImageIO.write(image, tileType.getFileExt(), buf); + return buf.toByteArray(); + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + InterruptedException, TileException { + BufferedImage image = null; + Graphics2D g2 = null; + try { + ArrayList layerImages = new ArrayList(mapSources.length); + int maxSize = mapSpace.getTileSize(); + for (int i = 0; i < mapSources.length; i++) { + MapSource layerMapSource = mapSources[i]; + BufferedImage layerImage = layerMapSource.getTileImage(zoom, x, y, loadMethod); + if (layerImage != null) { + log.debug("Multi layer loading: " + layerMapSource + " " + x + " " + y + " " + zoom); + layerImages.add(layerImage); + int size = layerImage.getWidth(); + if (size > maxSize) { + maxSize = size; + } + } + } + + image = new BufferedImage(maxSize, maxSize, BufferedImage.TYPE_3BYTE_BGR); + g2 = image.createGraphics(); + g2.setColor(getBackgroundColor()); + g2.fillRect(0, 0, maxSize, maxSize); + + for (int i = 0; i < layerImages.size(); i++) { + BufferedImage layerImage = layerImages.get(i); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, getLayerAlpha(i))); + g2.drawImage(layerImage, 0, 0, maxSize, maxSize, null); + } + return image; + } finally { + if (g2 != null) { + g2.dispose(); + } + + } + } + + protected float getLayerAlpha(int layerIndex) { + return 1.0f; + } + + public TileImageType getTileImageType() { + return tileType; + } + + @Override + public String toString() { + return getName(); + } + + public Iterator iterator() { + return Arrays.asList(mapSources).iterator(); + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + if (this.loaderInfo != null) + throw new RuntimeException("LoaderInfo already set"); + this.loaderInfo = loaderInfo; + } + +} diff --git a/src/main/java/mobac/mapsources/DefaultMapSourcesManager.java b/src/main/java/mobac/mapsources/DefaultMapSourcesManager.java new file mode 100644 index 0000000..bc64a2b --- /dev/null +++ b/src/main/java/mobac/mapsources/DefaultMapSourcesManager.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.TreeSet; +import java.util.Vector; + +import javax.swing.JOptionPane; + +import mobac.mapsources.custom.StandardMapSourceLayer; +import mobac.mapsources.impl.DebugLocalMapSource; +import mobac.mapsources.impl.DebugMapSource; +import mobac.mapsources.impl.DebugRandomLocalMapSource; +import mobac.mapsources.impl.DebugTransparentLocalMapSource; +import mobac.mapsources.impl.SimpleMapSource; +import mobac.mapsources.loader.BeanShellMapSourceLoader; +import mobac.mapsources.loader.CustomMapSourceLoader; +import mobac.mapsources.loader.EclipseMapPackLoader; +import mobac.mapsources.loader.MapPackManager; +import mobac.program.interfaces.MapSource; +import mobac.program.model.Settings; +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +public class DefaultMapSourcesManager extends MapSourcesManager { + + private Logger log = Logger.getLogger(DefaultMapSourcesManager.class); + + /** + * All map sources visible to the user independent of it is enabled or disabled + */ + private LinkedHashMap allMapSources = new LinkedHashMap(50); + + /** + * All means all visible map sources to the user plus all layers of multi-layer map sources + */ + private HashMap allAvailableMapSources = new HashMap(50); + + public DefaultMapSourcesManager() { + // Check for user specific configuration of mapsources directory + } + + protected void loadMapSources() { + try { + boolean devMode = Settings.getInstance().devMode; + if (devMode) { + addMapSource(new DebugMapSource()); + addMapSource(new DebugLocalMapSource()); + addMapSource(new DebugTransparentLocalMapSource()); + addMapSource(new DebugRandomLocalMapSource()); + } + File mapSourcesDir = Settings.getInstance().getMapSourcesDirectory(); + if (mapSourcesDir == null) + throw new RuntimeException("Map sources directory is unset"); + if (!mapSourcesDir.isDirectory()) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_environment_mapsrc_dir_not_exist"), + mapSourcesDir.getAbsolutePath()), + I18nUtils.localizedStringForKey("Error"), + JOptionPane.ERROR_MESSAGE); + return; + } + try { + MapPackManager mpm = new MapPackManager(mapSourcesDir); + mpm.installUpdates(); + if (!devMode || !loadMapPacksEclipseMode()) { + mpm.loadMapPacks(this); + } + } catch (Exception e) { + throw new RuntimeException("Failed to load map packs: " + e.getMessage(), e); + } + BeanShellMapSourceLoader bsmsl = new BeanShellMapSourceLoader(this, mapSourcesDir); + bsmsl.loadBeanShellMapSources(); + + CustomMapSourceLoader cmsl = new CustomMapSourceLoader(this, mapSourcesDir); + cmsl.loadCustomMapSources(); + + } finally { + // If no map sources are available load the simple map source which shows the informative message + if (allMapSources.size() == 0) + addMapSource(new SimpleMapSource()); + } + } + + private boolean loadMapPacksEclipseMode() { + EclipseMapPackLoader empl; + try { + empl = new EclipseMapPackLoader(this); + if (!empl.loadMapPacks()) + return false; + return true; + } catch (IOException e) { + log.error("Failed to load map packs directly from classpath"); + } + return false; + } + + public void addMapSource(MapSource mapSource) { + if (mapSource instanceof StandardMapSourceLayer) + mapSource = ((StandardMapSourceLayer) mapSource).getMapSource(); + allAvailableMapSources.put(mapSource.getName(), mapSource); + if (mapSource instanceof AbstractMultiLayerMapSource) { + for (MapSource lms : ((AbstractMultiLayerMapSource) mapSource)) { + if (lms instanceof StandardMapSourceLayer) + lms = ((StandardMapSourceLayer) lms).getMapSource(); + MapSource old = allAvailableMapSources.put(lms.getName(), lms); + if (old != null) { + allAvailableMapSources.put(old.getName(), old); + if (mapSource.equals(old)) + JOptionPane.showMessageDialog(null, + "Error: Duplicate map source name found: " + mapSource.getName(), "Duplicate name", + JOptionPane.ERROR_MESSAGE); + } + } + } + allMapSources.put(mapSource.getName(), mapSource); + } + + public static void initialize() { + INSTANCE = new DefaultMapSourcesManager(); + ((DefaultMapSourcesManager) INSTANCE).loadMapSources(); + } + + public static void initializeEclipseMapPacksOnly() { + INSTANCE = new DefaultMapSourcesManager(); + ((DefaultMapSourcesManager) INSTANCE).loadMapPacksEclipseMode(); + } + + @Override + public Vector getAllAvailableMapSources() { + return new Vector(allMapSources.values()); + } + + @Override + public Vector getAllMapSources() { + return new Vector(allMapSources.values()); + } + + @Override + public Vector getAllLayerMapSources() { + Vector all = getAllMapSources(); + TreeSet uniqueSources = new TreeSet(new Comparator() { + + public int compare(MapSource o1, MapSource o2) { + return o1.getName().compareTo(o2.getName()); + } + + }); + for (MapSource ms : all) { + if (ms instanceof AbstractMultiLayerMapSource) { + for (MapSource lms : ((AbstractMultiLayerMapSource) ms)) { + uniqueSources.add(lms); + } + } else + uniqueSources.add(ms); + } + Vector result = new Vector(uniqueSources); + return result; + } + + @Override + public Vector getEnabledOrderedMapSources() { + Vector mapSources = new Vector(allMapSources.size()); + + Vector enabledMapSources = Settings.getInstance().mapSourcesEnabled; + TreeSet notEnabledMapSources = new TreeSet(allMapSources.keySet()); + notEnabledMapSources.removeAll(enabledMapSources); + for (String mapSourceName : enabledMapSources) { + MapSource ms = getSourceByName(mapSourceName); + if (ms != null) { + mapSources.add(ms); + } + } + // remove all disabled map sources so we get those that are neither enabled nor disabled + notEnabledMapSources.removeAll(Settings.getInstance().mapSourcesDisabled); + for (String mapSourceName : notEnabledMapSources) { + MapSource ms = getSourceByName(mapSourceName); + if (ms != null) { + mapSources.add(ms); + } + } + if (mapSources.size() == 0) + mapSources.add(new SimpleMapSource()); + return mapSources; + } + + @Override + public Vector getDisabledMapSources() { + Vector disabledMapSources = Settings.getInstance().mapSourcesDisabled; + Vector mapSources = new Vector(disabledMapSources.size()); + for (String mapSourceName : disabledMapSources) { + MapSource ms = getSourceByName(mapSourceName); + if (ms != null) { + mapSources.add(ms); + } + } + return mapSources; + } + + @Override + public MapSource getDefaultMapSource() { + MapSource ms = getSourceByName("MapQuest");// DEFAULT; + if (ms != null) + return ms; + // Fallback: return first + return allMapSources.values().iterator().next(); + } + + @Override + public MapSource getSourceByName(String name) { + return allAvailableMapSources.get(name); + } + +} diff --git a/src/main/java/mobac/mapsources/MapSourceTools.java b/src/main/java/mobac/mapsources/MapSourceTools.java new file mode 100644 index 0000000..63d13ab --- /dev/null +++ b/src/main/java/mobac/mapsources/MapSourceTools.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources; + +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; + +/** + * Utility methods used by several map sources. + */ +public class MapSourceTools { + + protected static final char[] NUM_CHAR = { '0', '1', '2', '3' }; + + /** + * See: http://msdn.microsoft.com/en-us/library/bb259689.aspx + * + * @param zoom + * @param tilex + * @param tiley + * @return quadtree encoded tile number + * + */ + public static String encodeQuadTree(int zoom, int tilex, int tiley) { + char[] tileNum = new char[zoom]; + for (int i = zoom - 1; i >= 0; i--) { + // Binary encoding using ones for tilex and twos for tiley. if a bit + // is set in tilex and tiley we get a three. + int num = (tilex % 2) | ((tiley % 2) << 1); + tileNum[i] = NUM_CHAR[num]; + tilex >>= 1; + tiley >>= 1; + } + return new String(tileNum); + } + + /** + * Calculates latitude and longitude of the upper left corner of the specified tile of mapsource + * regarding the zoom level specified by zoom. + * + * @param mapSource + * @param zoom + * @param tilex + * horizontal tile number + * @param tiley + * vertical tile number + * @return double[] {lon_min , lat_min , lon_max , lat_max} + */ + public static double[] calculateLatLon(MapSource mapSource, int zoom, int tilex, int tiley) { + MapSpace mapSpace = mapSource.getMapSpace(); + int tileSize = mapSpace.getTileSize(); + double[] result = new double[4]; + tilex *= tileSize; + tiley *= tileSize; + result[0] = mapSpace.cXToLon(tilex, zoom); // lon_min + result[1] = mapSpace.cYToLat(tiley + tileSize, zoom); // lat_max + result[2] = mapSpace.cXToLon(tilex + tileSize, zoom); // lon_min + result[3] = mapSpace.cYToLat(tiley, zoom); // lat_max + return result; + } + + public static String formatMapUrl(String mapUrl, int zoom, int tilex, int tiley) { + String tmp = mapUrl; + tmp = tmp.replace("{$x}", Integer.toString(tilex)); + tmp = tmp.replace("{$y}", Integer.toString(tiley)); + tmp = tmp.replace("{$z}", Integer.toString(zoom)); + tmp = tmp.replace("{$q}", MapSourceTools.encodeQuadTree(zoom, tilex, tiley)); + return tmp; + } + + public static String formatMapUrl(String mapUrl, int serverNum, int zoom, int tilex, int tiley) { + String tmp = mapUrl; + tmp = tmp.replace("{$servernum}", Integer.toString(serverNum)); + return formatMapUrl(tmp, zoom, tilex, tiley); + } + + public static String formatMapUrl(String mapUrl, String serverPart, int zoom, int tilex, int tiley) { + String tmp = mapUrl; + tmp = tmp.replace("{$serverpart}", serverPart); + return formatMapUrl(tmp, zoom, tilex, tiley); + } +} diff --git a/src/main/java/mobac/mapsources/MapSourceUrlUpdater.java b/src/main/java/mobac/mapsources/MapSourceUrlUpdater.java new file mode 100644 index 0000000..3629f49 --- /dev/null +++ b/src/main/java/mobac/mapsources/MapSourceUrlUpdater.java @@ -0,0 +1,138 @@ +package mobac.mapsources; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import mobac.exceptions.MapSourceInitializationException; +import mobac.utilities.Utilities; +import mobac.utilities.writer.NullPrintWriter; + +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.w3c.tidy.Tidy; + +public class MapSourceUrlUpdater { + + public static final String ACCEPT = " text/*, text/html, text/html;level=1"; + + /** + * Loads the web page specified by url, parses it into DOM and extracts the src attribute + * of all <img> entities. + * + * @param url + * http or https url + * @param regex + * @return + * @throws IOException + */ + public static List extractImgSrcList(String url, String regex) throws IOException { + LinkedList list = new LinkedList(); + URL u = new URL(url); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.addRequestProperty("Accept", ACCEPT); + + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + Utilities.getInputBytes(conn.getInputStream()); + throw new IOException("Invalid HTTP response code: " + conn.getResponseCode()); + } + + Tidy tidy = new Tidy(); + tidy.setErrout(new NullPrintWriter()); // Suppress error messages + Document doc = tidy.parseDOM(conn.getInputStream(), null); + + XPathFactory factory = XPathFactory.newInstance(); + XPath xpath = factory.newXPath(); + XPathExpression expr; + NodeList nodes; + try { + expr = xpath.compile("//img[@src]"); + nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + Pattern p = null; + if (regex != null) + p = Pattern.compile(regex); + for (int i = 0; i < nodes.getLength(); i++) { + String imgUrl = nodes.item(i).getAttributes().getNamedItem("src").getNodeValue(); + if (imgUrl != null && imgUrl.length() > 0) { + if (p != null) { + if (!p.matcher(imgUrl).matches()) + continue; + } + list.add(imgUrl); + } + } + return list; + } + + /** + * Retrieves the text or HTML document on the specified url, interprets the retrieved data as + * {@link String} of {@link Charset} charset and returns this {@link String}. + * + * @param url + * @param charset + * @return + * @throws IOException + */ + public static String loadDocument(String url, Charset charset) throws IOException { + URL u = new URL(url); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.addRequestProperty("Accept", ACCEPT); + + byte[] data = Utilities.getInputBytes(conn.getInputStream()); + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("Invalid HTTP response code: " + conn.getResponseCode()); + } + + return new String(data, charset); + } + + /** + * + * @param url + * @param charset + * @param regex + * regex defining one group with will be returned + * @return + * @throws MapSourceInitializationException + */ + public static String loadDocumentAndExtractGroup(String url, Charset charset, String regex) + throws MapSourceInitializationException { + String document; + try { + document = loadDocument(url, charset); + } catch (IOException e) { + throw new MapSourceInitializationException("Faile dto retrieve initialization document from url: " + url + + "\nError: " + e.getMessage(), e); + } + Matcher m = Pattern.compile(regex).matcher(document); + if (!m.find()) + throw new MapSourceInitializationException("pattern not found: " + regex); + return m.group(1); + } + + public static void main(String[] args) { + try { + List imgUrls = extractImgSrcList("http://maps.google.com/?ie=UTF8&ll=0,0&spn=0,0&z=2", + "^http://mt\\d\\.google\\.com/.*"); + for (String s : imgUrls) + System.out.println(s); + } catch (Exception e) { + e.printStackTrace(); + } + + } +} diff --git a/src/main/java/mobac/mapsources/MapSourcesManager.java b/src/main/java/mobac/mapsources/MapSourcesManager.java new file mode 100644 index 0000000..3acf86c --- /dev/null +++ b/src/main/java/mobac/mapsources/MapSourcesManager.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources; + +import java.util.Vector; + +import mobac.program.interfaces.MapSource; + +public abstract class MapSourcesManager { + + protected static MapSourcesManager INSTANCE = null; + + public static MapSourcesManager getInstance() { + return INSTANCE; + } + + public abstract void addMapSource(MapSource mapSource); + + public abstract Vector getAllMapSources(); + + /** + * Returns all {@link MapSource} used implementations that represent a map layer (have a visible result). + * Meta-map-sources like multi-layer map sources are ignored. The result does contain each {@link MapSource} only + * once (no duplicates). + * + * @return + */ + public abstract Vector getAllLayerMapSources(); + + public abstract Vector getEnabledOrderedMapSources(); + + public abstract MapSource getDefaultMapSource(); + + public abstract MapSource getSourceByName(String name); + + public abstract Vector getDisabledMapSources(); + + /** + * All means all visible map sources to the user plus all layers of multi-layer map sources + * + * @return + */ + public abstract Vector getAllAvailableMapSources(); + +} diff --git a/src/main/java/mobac/mapsources/MapSourcesPropertiesManager.java b/src/main/java/mobac/mapsources/MapSourcesPropertiesManager.java new file mode 100644 index 0000000..6f0197b --- /dev/null +++ b/src/main/java/mobac/mapsources/MapSourcesPropertiesManager.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +import mobac.program.model.Settings; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +public class MapSourcesPropertiesManager { + + private static final Logger log = Logger.getLogger(MapSourcesPropertiesManager.class); + + private static final String FILENAME = "mapsources.properties"; + + public static final Properties PROPERTIES = new Properties(); + + private static boolean SHUTDOWN_HOOK_REGISTERED = false; + + public static void load() { + File mapSourcesDir = Settings.getInstance().getMapSourcesDirectory(); + File mapSourcesProperties = new File(mapSourcesDir, FILENAME); + if (!mapSourcesProperties.isFile()) + return; + FileInputStream in = null; + try { + in = new FileInputStream(mapSourcesProperties); + PROPERTIES.load(in); + } catch (IOException e) { + log.error("Failed to load mapsources.properties", e); + } finally { + Utilities.closeStream(in); + } + if (!SHUTDOWN_HOOK_REGISTERED) { + Runtime.getRuntime().addShutdownHook(new Thread() { + + @Override + public void run() { + save(); + } + + }); + + } + } + + public static void save() { + if (PROPERTIES.size() == 0) + return; + File mapSourcesDir = Settings.getInstance().getMapSourcesDirectory(); + File mapSourcesProperties = new File(mapSourcesDir, FILENAME); + FileOutputStream out = null; + try { + out = new FileOutputStream(mapSourcesProperties); + PROPERTIES.store(out, ""); + } catch (IOException e) { + log.error("", e); + } finally { + Utilities.closeStream(out); + } + } +} diff --git a/src/main/java/mobac/mapsources/custom/BeanShellHttpMapSource.java b/src/main/java/mobac/mapsources/custom/BeanShellHttpMapSource.java new file mode 100644 index 0000000..1c7f25b --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/BeanShellHttpMapSource.java @@ -0,0 +1,196 @@ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +import mobac.exceptions.TileException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.AbstractHttpMapSource; +import mobac.mapsources.mapspace.MapSpaceFactory; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.MapSpace; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.TileImageType; +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; +import bsh.EvalError; +import bsh.Interpreter; + +public class BeanShellHttpMapSource extends AbstractHttpMapSource { + + private static final String AH_ERROR = "Sourced file: inline evaluation of: " + + "``addHeaders(conn);'' : Command not found: addHeaders( sun.net.www.protocol.http.HttpURLConnection )"; + + private static int NUM = 0; + + private final Interpreter i; + + private Color backgroundColor = Color.BLACK; + + private boolean ignoreError = false; + + public static BeanShellHttpMapSource load(File f) throws EvalError, IOException { + FileInputStream in = new FileInputStream(f); + try { + BufferedReader br = new BufferedReader(new InputStreamReader(in, Charsets.UTF_8)); + StringWriter sw = new StringWriter(); + String line = br.readLine(); + while (line != null) { + sw.write(line + "\n"); + line = br.readLine(); + } + br.close(); + return new BeanShellHttpMapSource(sw.toString()); + } finally { + Utilities.closeStream(in); + } + } + + public BeanShellHttpMapSource(String code) throws EvalError { + super("", 0, 0, TileImageType.PNG, TileUpdate.None); + name = "BeanShell map source " + NUM++; + i = new Interpreter(); + + i.eval("import mobac.program.interfaces.HttpMapSource.TileUpdate;"); + i.eval("import java.net.HttpURLConnection;"); + i.eval("import mobac.utilities.beanshell.*;"); + i.eval(code); + Object o = i.get("name"); + if (o != null) + name = (String) o; + + o = i.get("tileSize"); + if (o != null) { + int tileSize = ((Integer) o).intValue(); + mapSpace = MapSpaceFactory.getInstance(tileSize, true); + } else + mapSpace = MercatorPower2MapSpace.INSTANCE_256; + + o = i.get("minZoom"); + if (o != null) + minZoom = ((Integer) o).intValue(); + else + minZoom = 0; + + o = i.get("maxZoom"); + if (o != null) + maxZoom = ((Integer) o).intValue(); + else + maxZoom = PreviewMap.MAX_ZOOM; + + o = i.get("tileType"); + if (o != null) + tileType = TileImageType.getTileImageType((String) o); + else + throw new EvalError("tileType definition missing", null, null); + + o = i.get("tileUpdate"); + if (o != null) + tileUpdate = (TileUpdate) o; + + o = i.get("ignoreError"); + if (o != null) { + if (o instanceof String) { + ignoreError = Boolean.parseBoolean((String) o); + } else if (o instanceof Boolean) { + ignoreError = ((Boolean) o).booleanValue(); + } else + throw new EvalError("Invalid type for \"ignoreError\": " + o.getClass(), null, null); + } + + o = i.get("backgroundColor"); + if (o != null) + try { + backgroundColor = ColorAdapter.parseColor((String) o); + } catch (javax.xml.bind.UnmarshalException e) { + throw new EvalError(e.getMessage(), null, null); + } + } + + @Override + public synchronized HttpURLConnection getTileUrlConnection(int zoom, int tilex, int tiley) throws IOException { + HttpURLConnection conn = null; + try { + String url = getTileUrl(zoom, tilex, tiley); + conn = (HttpURLConnection) new URL(url).openConnection(); + } catch (IOException e) { + throw e; + } catch (Exception e) { + log.error("", e); + throw new IOException(e); + } + try { + i.set("conn", conn); + i.eval("addHeaders(conn);"); + } catch (EvalError e) { + String msg = e.getMessage(); + if (!AH_ERROR.equals(msg)) { + log.error(e.getClass() + ": " + e.getMessage(), e); + throw new IOException(e); + } + } + return conn; + } + + @Override + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (!ignoreError) + return super.getTileData(zoom, x, y, loadMethod); + try { + return super.getTileData(zoom, x, y, loadMethod); + } catch (Exception e) { + return null; + } + } + + public boolean testCode() throws IOException { + return (getTileUrlConnection(minZoom, 0, 0) != null); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + try { + return (String) i.eval(String.format("getTileUrl(%d,%d,%d);", zoom, tilex, tiley)); + } catch (EvalError e) { + log.error(e.getClass() + ": " + e.getMessage(), e); + throw new RuntimeException(e); + } + } + + @Override + public MapSpace getMapSpace() { + return mapSpace; + } + + @Override + public int getMaxZoom() { + return maxZoom; + } + + @Override + public int getMinZoom() { + return minZoom; + } + + @Override + public String getName() { + return name; + } + + @Override + public TileUpdate getTileUpdate() { + return tileUpdate; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + +} diff --git a/src/main/java/mobac/mapsources/custom/CustomCloudMade.java b/src/main/java/mobac/mapsources/custom/CustomCloudMade.java new file mode 100644 index 0000000..ea65488 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomCloudMade.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.lang.reflect.Constructor; + +import javax.swing.JOptionPane; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import mobac.mapsources.mappacks.openstreetmap.CloudMade; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.WrappedMapSource; +import mobac.utilities.I18nUtils; + +/** + * Requires the OpenStreetMap + * + * + */ +@XmlRootElement(name = "cloudMade") +public class CustomCloudMade implements WrappedMapSource { + + private static boolean ERROR = false; + + public static Class CLOUD_MADE_CLASS = null; + + @XmlElement + public String styleID; + + @XmlElement + public String displayName; + + public MapSource getMapSource() { + try { + Constructor c = CLOUD_MADE_CLASS.getConstructor(String.class, String.class); + return c.newInstance(styleID, displayName); + } catch (Exception e) { + String errorMsg = I18nUtils.localizedStringForKey("msg_environment_error_load_cloudmade"); + if (!ERROR) { + JOptionPane.showMessageDialog(null, errorMsg, + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + ERROR = true; + } + throw new RuntimeException(errorMsg, e); + } + } + +} diff --git a/src/main/java/mobac/mapsources/custom/CustomLocalImageFileMapSource.java b/src/main/java/mobac/mapsources/custom/CustomLocalImageFileMapSource.java new file mode 100644 index 0000000..96bea98 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomLocalImageFileMapSource.java @@ -0,0 +1,276 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.JOptionPane; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.exceptions.TileException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.mapspace.MapSpaceFactory; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +@XmlRootElement(name = "localImageFile") +public class CustomLocalImageFileMapSource implements FileBasedMapSource { + + private static final Logger log = Logger.getLogger(CustomLocalImageFileMapSource.class); + + private MapSourceLoaderInfo loaderInfo = null; + + @XmlElement(required = true, nillable = false) + private double boxNorth = 90.0; + + @XmlElement(required = true, nillable = false) + private double boxSouth = -90.0; + + @XmlElement(required = true, nillable = false) + private double boxEast = 180.0; + + @XmlElement(required = true, nillable = false) + private double boxWest = -180.0; + + private MapSpace mapSpace = MapSpaceFactory.getInstance(256, true); + + private boolean initialized = false; + + BufferedImage fullImage = null; + + private TileImageType tileImageType = null; + + @XmlElement(nillable = false, defaultValue = "CustomImage") + private String name = "Custom"; + + @XmlElement(nillable = false, defaultValue = "0") + private int minZoom = PreviewMap.MIN_ZOOM; + + @XmlElement(nillable = false, defaultValue = "20") + private int maxZoom = PreviewMap.MAX_ZOOM; + + @XmlElement(required = true) + private File imageFile = null; + + @XmlElement(nillable = false, defaultValue = "false") + private boolean retinaDisplay = false; + + // @XmlElement() + // private CustomMapSourceType sourceType = CustomMapSourceType.DIR_ZOOM_X_Y; + + @XmlElement(defaultValue = "false") + private boolean invertYCoordinate = false; + + @XmlElement(defaultValue = "#00000000") + @XmlJavaTypeAdapter(ColorAdapter.class) + private Color backgroundColor = Color.BLACK; + + public CustomLocalImageFileMapSource() { + super(); + } + + public synchronized void initialize() { + if (initialized) + return; + reinitialize(); + } + + public void reinitialize() { + try { + if (!imageFile.isFile()) { + JOptionPane.showMessageDialog(null, String.format( + I18nUtils.localizedStringForKey("msg_environment_invalid_source_folder"), name, + imageFile.toString()), I18nUtils + .localizedStringForKey("msg_environment_invalid_source_folder_title"), + JOptionPane.ERROR_MESSAGE); + initialized = true; + return; + } + String[] parts = imageFile.getName().split("\\."); + if (parts.length >= 2) { + tileImageType = TileImageType.getTileImageType(parts[parts.length - 1]); + } else { + tileImageType = TileImageType.PNG; + } + + boxWest = Math.min(boxEast, boxWest); + boxEast = Math.max(boxEast, boxWest); + boxSouth = Math.min(boxNorth, boxSouth); + boxNorth = Math.max(boxNorth, boxSouth); + + } finally { + initialized = true; + } + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(16000); + BufferedImage image = getTileImage(zoom, x, y, loadMethod); + if (image == null) + return null; + ImageIO.write(image, tileImageType.getFileExt(), buf); + return buf.toByteArray(); + } + + // integer nearest to zero + private int absFloor(double value) { + return value > 0 ? (int) Math.floor(value) : (int) Math.ceil(value); + } + + // integer farest from zero + private int absCeil(double value) { + return value > 0 ? (int) Math.ceil(value) : (int) Math.floor(value); + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (!initialized) + initialize(); + + if (log.isTraceEnabled()) + log.trace(String.format("Loading tile z=%d x=%d y=%d", zoom, x, y)); + + BufferedImage image = null; + Graphics2D g2 = null; + + try { + if (fullImage == null) { + fullImage = ImageIO.read(imageFile); + } + int imageWidth = fullImage.getWidth(); + int imageHeight = fullImage.getHeight(); + int tileSize = mapSpace.getTileSize(); + double tileWest = mapSpace.cXToLon(x * tileSize, zoom); + double tileNorth = mapSpace.cYToLat(y * tileSize, zoom); + double tileEast = mapSpace.cXToLon((x + 1) * tileSize, zoom); + double tileSouth = mapSpace.cYToLat((y + 1) * tileSize, zoom); + double tileWidth = tileEast - tileWest; + double tileHeight = tileNorth - tileSouth; + + // intersects coordinate region + double intersectWest = Math.max(tileWest, boxWest); + double intersectEast = Math.min(tileEast, boxEast); + double intersectNorth = Math.min(tileNorth, boxNorth); + double intersectSouth = Math.max(tileSouth, boxSouth); + double intersectWidth = intersectEast - intersectWest; + double intersectHeight = intersectNorth - intersectSouth; + + // intersects + if (intersectWidth > 0 && intersectHeight > 0) { + int graphContextSize = tileSize * (retinaDisplay ? 2 : 1); + image = new BufferedImage(graphContextSize, graphContextSize, BufferedImage.TYPE_4BYTE_ABGR); + g2 = image.createGraphics(); + g2.setColor(getBackgroundColor()); + g2.fillRect(0, 0, graphContextSize, graphContextSize); + + double boxWidth = (boxEast - boxWest); + double boxHeight = (boxNorth - boxSouth); + + // crop parameters + double cropWScale = (boxWidth <= 0) ? 0 : (imageWidth / boxWidth); + double cropHScale = (boxHeight <= 0) ? 0 : (imageHeight / boxHeight); + int cropW = absCeil(intersectWidth * cropWScale); + int cropH = absCeil(intersectHeight * cropHScale); + int cropX = absFloor((intersectWest - boxWest) * cropWScale); + // int cropY = imageHeight - absCeil((boxNorth - intersectNorth) * cropHScale) - cropH; + int cropY = absFloor((boxNorth - intersectNorth) * cropHScale); + // skip when no valid crop + if (cropX < imageWidth && cropY < imageHeight && (cropX + cropW) > 0 && (cropY + cropH) > 0) { + // draw rect + double drawrectWScale = (tileWidth <= 0) ? 0 : (graphContextSize / tileWidth); + double drawrectHScale = (tileHeight <= 0) ? 0 : (graphContextSize / tileHeight); + int drawrectW = absCeil(intersectWidth * drawrectWScale); + int drawrectH = absCeil(intersectHeight * drawrectHScale); + int drawrectX = absFloor((intersectWest - tileWest) * drawrectWScale); + // int drawrectY = tileSize - absCeil((tileNorth - intersectNorth) * drawrectHScale) - drawrectH; + int drawrectY = absFloor((tileNorth - intersectNorth) * drawrectHScale); + + // skip when draw rectangle totally draw out of image + if (drawrectX < graphContextSize && drawrectY < graphContextSize && (drawrectX + drawrectW) > 1 + && (drawrectY + drawrectH) > 1) { + g2.drawImage(fullImage, drawrectX, drawrectY, drawrectX + drawrectW, drawrectY + drawrectH, + cropX, cropY, cropX + cropW, cropY + cropH, null); + } + } + + } + } catch (FileNotFoundException e) { + log.debug("Map image file not found: " + imageFile.getAbsolutePath()); + } finally { + if (g2 != null) { + g2.dispose(); + } + } + return image; + } + + public TileImageType getTileImageType() { + return tileImageType; + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + this.loaderInfo = loaderInfo; + } + +} diff --git a/src/main/java/mobac/mapsources/custom/CustomLocalTileFilesMapSource.java b/src/main/java/mobac/mapsources/custom/CustomLocalTileFilesMapSource.java new file mode 100644 index 0000000..69c1285 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomLocalTileFilesMapSource.java @@ -0,0 +1,300 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; +import javax.swing.JOptionPane; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.exceptions.TileException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.MapSourceTools; +import mobac.mapsources.mapspace.MapSpaceFactory; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +@XmlRootElement(name = "localTileFiles") +public class CustomLocalTileFilesMapSource implements FileBasedMapSource { + + private static final Logger log = Logger.getLogger(CustomLocalTileFilesMapSource.class); + + private MapSourceLoaderInfo loaderInfo = null; + + private MapSpace mapSpace = MapSpaceFactory.getInstance(256, true); + + private boolean initialized = false; + + private String fileSyntax = null; + + private TileImageType tileImageType = null; + + @XmlElement(nillable = false, defaultValue = "CustomLocal") + private String name = "Custom"; + + private int minZoom = PreviewMap.MIN_ZOOM; + + private int maxZoom = PreviewMap.MAX_ZOOM; + + @XmlElement(required = true) + private File sourceFolder = null; + + @XmlElement() + private CustomMapSourceType sourceType = CustomMapSourceType.DIR_ZOOM_X_Y; + + @XmlElement(defaultValue = "false") + private boolean invertYCoordinate = false; + + @XmlElement(defaultValue = "#000000") + @XmlJavaTypeAdapter(ColorAdapter.class) + private Color backgroundColor = Color.BLACK; + + public CustomLocalTileFilesMapSource() { + super(); + } + + public synchronized void initialize() { + if (initialized) + return; + reinitialize(); + } + + public void reinitialize() { + try { + if (!sourceFolder.isDirectory()) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_environment_invalid_source_folder"), + name, sourceFolder.toString()), + I18nUtils.localizedStringForKey("msg_environment_invalid_source_folder_title"), + JOptionPane.ERROR_MESSAGE); + initialized = true; + return; + } + switch (sourceType) { + case DIR_ZOOM_X_Y: + case DIR_ZOOM_Y_X: + initializeDirType(); + break; + case QUADKEY: + initializeQuadKeyType(); + break; + default: + throw new RuntimeException("Invalid source type"); + } + } finally { + initialized = true; + } + } + + private void initializeDirType() { + /* Update zoom levels */ + FileFilter ff = new NumericDirFileFilter(); + File[] zoomDirs = sourceFolder.listFiles(ff); + if (zoomDirs.length < 1) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_environment_invalid_source_folder_zoom"), + name ,sourceFolder), + I18nUtils.localizedStringForKey("msg_environment_invalid_source_folder_title"), JOptionPane.ERROR_MESSAGE); + initialized = true; + return; + } + int min = PreviewMap.MAX_ZOOM; + int max = PreviewMap.MIN_ZOOM; + for (File file : zoomDirs) { + int z = Integer.parseInt(file.getName()); + min = Math.min(min, z); + max = Math.max(max, z); + } + minZoom = min; + maxZoom = max; + + for (File zDir : zoomDirs) { + for (File xDir : zDir.listFiles(ff)) { + try { + xDir.listFiles(new FilenameFilter() { + + String syntax = "%d/%d/%d"; + + public boolean accept(File dir, String name) { + String[] parts = name.split("\\."); + if (parts.length < 2 || parts.length > 3) + return false; + syntax += "." + parts[1]; + if (parts.length == 3) + syntax += "." + parts[2]; + tileImageType = TileImageType.getTileImageType(parts[1]); + fileSyntax = syntax; + log.debug("Detected file syntax: " + fileSyntax + " tileImageType=" + tileImageType); + throw new RuntimeException("break"); + } + }); + } catch (RuntimeException e) { + } catch (Exception e) { + log.error(e.getMessage()); + } + return; + } + } + } + + private void initializeQuadKeyType() { + String[] files = sourceFolder.list(); + Pattern p = Pattern.compile("([0123]+)\\.(png|gif|jpg)", Pattern.CASE_INSENSITIVE); + String fileExt = null; + for (String file : files) { + Matcher m = p.matcher(file); + if (!m.matches()) + continue; + fileExt = m.group(2); + break; + } + if (fileExt == null) + return; // Error no suitable file found + fileSyntax = "%s." + fileExt; + + tileImageType = TileImageType.getTileImageType(fileExt); + p = Pattern.compile("([0123]+)\\.(" + fileExt + ")", Pattern.CASE_INSENSITIVE); + + int min = PreviewMap.MAX_ZOOM; + int max = 1; + + for (String file : files) { + Matcher m = p.matcher(file); + if (!m.matches()) + continue; + if (fileSyntax == null) + fileSyntax = "%s." + m.group(2); + int z = m.group(1).length(); + min = Math.min(min, z); + max = Math.max(max, z); + } + minZoom = min; + maxZoom = max; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (!initialized) + initialize(); + if (fileSyntax == null) + return null; + if (log.isTraceEnabled()) + log.trace(String.format("Loading tile z=%d x=%d y=%d", zoom, x, y)); + + if (invertYCoordinate) + y = ((1 << zoom) - y - 1); + String fileName; + switch (sourceType) { + case DIR_ZOOM_X_Y: + fileName = String.format(fileSyntax, zoom, x, y); + break; + case DIR_ZOOM_Y_X: + fileName = String.format(fileSyntax, zoom, y, x); + break; + case QUADKEY: + fileName = String.format(fileSyntax, MapSourceTools.encodeQuadTree(zoom, x, y)); + break; + default: + throw new RuntimeException("Invalid source type"); + } + File file = new File(sourceFolder, fileName); + try { + return Utilities.getFileBytes(file); + } catch (FileNotFoundException e) { + log.debug("Map tile file not found: " + file.getAbsolutePath()); + return null; + } + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + byte[] data = getTileData(zoom, x, y, loadMethod); + if (data == null) + return null; + return ImageIO.read(new ByteArrayInputStream(data)); + } + + public TileImageType getTileImageType() { + return tileImageType; + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + this.loaderInfo = loaderInfo; + } + + private static class NumericDirFileFilter implements FileFilter { + + private Pattern p = Pattern.compile("^\\d+$"); + + public boolean accept(File f) { + if (!f.isDirectory()) + return false; + return p.matcher(f.getName()).matches(); + } + + } +} diff --git a/src/main/java/mobac/mapsources/custom/CustomLocalTileSQliteMapSource.java b/src/main/java/mobac/mapsources/custom/CustomLocalTileSQliteMapSource.java new file mode 100644 index 0000000..9516285 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomLocalTileSQliteMapSource.java @@ -0,0 +1,320 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.imageio.ImageIO; +import javax.swing.JOptionPane; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.exceptions.TileException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.mapspace.MapSpaceFactory; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.jdbc.SQLiteLoader; + +import org.apache.log4j.Logger; + +/** + * + * MBTiles input http://mbtiles.org/ + * + */ +@XmlRootElement(name = "localTileSQLite") +public class CustomLocalTileSQliteMapSource implements FileBasedMapSource { + + private static Logger log = Logger.getLogger(CustomLocalTileSQliteMapSource.class); + + private static enum SQLiteAtlasType { + RMaps, MBTiles, BigPlanetTracks, Galileo, NaviComputer, OSMAND + }; + + private MapSourceLoaderInfo loaderInfo = null; + + private boolean initialized = false; + + @XmlElement(required = false) + private TileImageType tileImageType = null; + + @XmlElement(nillable = false, defaultValue = "CustomLocalSQLite") + private String name = "CustomLocalSQLite"; + + private int minZoom = PreviewMap.MIN_ZOOM; + + private int maxZoom = PreviewMap.MAX_ZOOM; + + @XmlElement(required = true) + private File sourceFile = null; + + @XmlElement(required = true) + private SQLiteAtlasType atlasType = null; + + @XmlElement(defaultValue = "#000000") + @XmlJavaTypeAdapter(ColorAdapter.class) + private Color backgroundColor = Color.BLACK; + + private String sqlMaxZoomStatement; + private String sqlMinZoomStatement; + private String sqlTileStatement; + private String sqlTileImageTypeStatement; + + /** + * SQLite connection with database file + */ + private Connection conn = null; + + private final MapSpace mapSpace = MapSpaceFactory.getInstance(256, true); + + public CustomLocalTileSQliteMapSource() { + super(); + } + + protected void updateZoomLevelInfo() { + Statement statement = null; + try { + statement = conn.createStatement(); + if (statement.execute(sqlMaxZoomStatement)) { + ResultSet rs = statement.getResultSet(); + if (rs.next()) { + maxZoom = rs.getInt(1); + } + rs.close(); + } + if (statement.execute(sqlMinZoomStatement)) { + ResultSet rs = statement.getResultSet(); + if (rs.next()) { + minZoom = rs.getInt(1); + } + rs.close(); + } + statement.close(); + } catch (SQLException e) { + log.error("", e); + } finally { + Utilities.closeStatement(statement); + } + } + + public synchronized void initialize() { + if (initialized) + return; + reinitialize(); + } + + public void reinitialize() { + if (atlasType == null) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_custom_map_invalid_source_file"), + name, sourceFile), + I18nUtils.localizedStringForKey("msg_custom_map_invalid_source_file_title"), + JOptionPane.ERROR_MESSAGE); + initialized = true; + return; + } + if (!sourceFile.isFile()) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_custom_map_invalid_source_sqlitedb"), + name, sourceFile), + I18nUtils.localizedStringForKey("msg_custom_map_invalid_source_file_title"), + JOptionPane.ERROR_MESSAGE); + initialized = true; + return; + } + if (!SQLiteLoader.loadSQLiteOrShowError()) { + initialized = true; + return; + } + log.debug("Loading SQLite database " + sourceFile); + String url = "jdbc:sqlite:" + this.sourceFile; + try { + conn = DriverManager.getConnection(url); + } catch (SQLException e) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_custom_map_source_failed_load_sqlitedb"), + name,sourceFile,e.getMessage()), + I18nUtils.localizedStringForKey("msg_custom_map_source_failed_load_sqlitedb_title"), + JOptionPane.ERROR_MESSAGE); + initialized = true; + return; + } + switch (atlasType) { + case MBTiles: + // DISTINCT works much faster than min(zoom_level) or max(zoom_level) - uses index? + sqlMaxZoomStatement = "SELECT DISTINCT zoom_level FROM tiles ORDER BY zoom_level DESC LIMIT 1;"; + sqlMinZoomStatement = "SELECT DISTINCT zoom_level FROM tiles ORDER BY zoom_level ASC LIMIT 1;"; + sqlTileStatement = "SELECT tile_data from tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?;"; + sqlTileImageTypeStatement = "SELECT tile_data from tiles LIMIT 1;"; + break; + case RMaps: + case BigPlanetTracks: + case Galileo: + case OSMAND: + sqlMaxZoomStatement = "SELECT DISTINCT (17 - z) as zoom FROM tiles ORDER BY zoom DESC LIMIT 1;"; + sqlMinZoomStatement = "SELECT DISTINCT (17 - z) as zoom FROM tiles ORDER BY zoom ASC LIMIT 1;"; + sqlTileStatement = "SELECT image from tiles WHERE z=(17 - ?) AND x=? AND y=?;"; + sqlTileImageTypeStatement = "SELECT image from tiles LIMIT 1;"; + break; + case NaviComputer: + sqlMaxZoomStatement = "SELECT DISTINCT zoom FROM Tiles ORDER BY zoom DESC LIMIT 1;"; + sqlMinZoomStatement = "SELECT DISTINCT zoom FROM Tiles ORDER BY zoom ASC LIMIT 1;"; + sqlTileStatement = "SELECT Tile FROM Tiles LEFT JOIN Tilesdata ON Tiles.id=Tilesdata.id WHERE Zoom=? AND X=? AND Y=?;"; + sqlTileImageTypeStatement = "SELECT Tile from Tilesdata LIMIT 1;"; + break; + } + updateZoomLevelInfo(); + detectTileImageType(); + initialized = true; + } + + protected void detectTileImageType() { + if (tileImageType != null) + return; // Already specified manually by user + Statement statement = null; + try { + statement = conn.createStatement(); + if (statement.execute(sqlTileImageTypeStatement)) { + ResultSet rs = statement.getResultSet(); + if (rs.next()) + tileImageType = Utilities.getImageType(rs.getBytes(1)); + rs.close(); + } + statement.close(); + } catch (SQLException e) { + log.error("", e); + } finally { + Utilities.closeStatement(statement); + } + if (tileImageType == null) + throw new RuntimeException("Unable to detect image type of " + sourceFile + ".\n" + + "Please specify it manually using entry in map source definition."); + + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (!initialized) + initialize(); + PreparedStatement statement = null; + try { + switch (atlasType) { + case MBTiles: + y = (1 << zoom) - y - 1; + break; + default: + break; + } + + statement = conn.prepareStatement(sqlTileStatement); + statement.setInt(1, zoom); + statement.setInt(2, x); + statement.setInt(3, y); + if (log.isTraceEnabled()) + log.trace(String.format("Loading tile z=%d x=%d y=%d", zoom, x, y)); + if (statement.execute()) { + ResultSet rs = statement.getResultSet(); + if (!rs.next()) { + if (log.isDebugEnabled()) + log.debug(String.format("Tile in database not found: z=%d x=%d y=%d", zoom, x, y)); + return null; + } + byte[] data = rs.getBytes(1); + rs.close(); + return data; + } + } catch (SQLException e) { + log.error("", e); + } finally { + Utilities.closeStatement(statement); + } + return null; + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + byte[] data = getTileData(zoom, x, y, loadMethod); + if (data == null) + return null; + return ImageIO.read(new ByteArrayInputStream(data)); + } + + public TileImageType getTileImageType() { + return tileImageType; + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + this.loaderInfo = loaderInfo; + } + + protected void closeConnection() { + try { + if (conn != null) + conn.close(); + } catch (SQLException e) { + } + conn = null; + } +} diff --git a/src/main/java/mobac/mapsources/custom/CustomLocalTileZipMapSource.java b/src/main/java/mobac/mapsources/custom/CustomLocalTileZipMapSource.java new file mode 100644 index 0000000..ce47db2 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomLocalTileZipMapSource.java @@ -0,0 +1,316 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.imageio.ImageIO; +import javax.swing.JOptionPane; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.exceptions.TileException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.MapSourceTools; +import mobac.mapsources.mapspace.MapSpaceFactory; +import mobac.program.Logging; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +@XmlRootElement(name = "localTileZip") +public class CustomLocalTileZipMapSource implements FileBasedMapSource { + + private static final Logger log = Logger.getLogger(CustomLocalTileZipMapSource.class); + + private MapSourceLoaderInfo loaderInfo = null; + + private MapSpace mapSpace = MapSpaceFactory.getInstance(256, true); + + private boolean initialized = false; + + private String fileSyntax = null; + + private TileImageType tileImageType = null; + + @XmlElement(nillable = false, defaultValue = "CustomLocal") + private String name = "Custom"; + + private int minZoom = PreviewMap.MIN_ZOOM; + + private int maxZoom = PreviewMap.MAX_ZOOM; + + @XmlElement(name = "zipFile", required = true) + private File[] zipFiles = new File[] {}; + + @XmlElement() + private CustomMapSourceType sourceType = CustomMapSourceType.DIR_ZOOM_X_Y; + + @XmlElement(defaultValue = "false") + private boolean invertYCoordinate = false; + + private LinkedList zips = new LinkedList(); + + @XmlElement(defaultValue = "#000000") + @XmlJavaTypeAdapter(ColorAdapter.class) + private Color backgroundColor = Color.BLACK; + + public CustomLocalTileZipMapSource() { + super(); + } + + protected synchronized void openZipFile() { + for (File zipFile : zipFiles) { + if (!zipFile.isFile()) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_custom_map_invalid_source_zip_title"), + name, zipFile.toString()), + I18nUtils.localizedStringForKey("msg_custom_map_invalid_source_zip_title"), + JOptionPane.ERROR_MESSAGE); + } else { + try { + Logging.LOG.debug("Opening zip file " + zipFile.getAbsolutePath()); + zips.add(new ZipFile(zipFile)); + Logging.LOG.debug("Zip file open completed"); + } catch (Exception e) { + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_custom_map_failed_open_source_zip"), + name, zipFile.toString()), + I18nUtils.localizedStringForKey("msg_custom_map_failed_open_source_zip_title"), + JOptionPane.ERROR_MESSAGE); + } + } + } + } + + public synchronized void initialize() { + if (initialized) + return; + reinitialize(); + } + + public void reinitialize() { + try { + openZipFile(); + if (zips.size() == 0) + return; + switch (sourceType) { + case DIR_ZOOM_X_Y: + case DIR_ZOOM_Y_X: + initializeDirType(); + break; + case QUADKEY: + initializeQuadKeyType(); + break; + default: + throw new RuntimeException("Invalid source type"); + } + } finally { + initialized = true; + } + } + + public synchronized void initializeDirType() { + int min = PreviewMap.MAX_ZOOM; + int max = PreviewMap.MIN_ZOOM; + for (ZipFile zip : zips) { + for (int z = PreviewMap.MAX_ZOOM; z > PreviewMap.MIN_ZOOM; z--) { + ZipEntry entry = zip.getEntry(Integer.toString(z) + "/"); + if (entry != null) { + max = Math.max(max, z); + break; + } + } + for (int z = PreviewMap.MIN_ZOOM; z < PreviewMap.MAX_ZOOM; z++) { + ZipEntry entry = zip.getEntry(Integer.toString(z) + "/"); + if (entry != null) { + min = Math.min(min, z); + break; + } + } + } + minZoom = min; + maxZoom = max; + + Enumeration entries = zips.get(0).entries(); + String syntax = "%d/%d/%d"; + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) + continue; + String name = entry.getName(); + int i = name.lastIndexOf("/"); + name = name.substring(i + 1); + + String[] parts = name.split("\\."); + if (parts.length < 2 || parts.length > 3) + break; + syntax += "." + parts[1]; + tileImageType = TileImageType.getTileImageType(parts[1]); + if (parts.length == 3) + syntax += "." + parts[2]; + fileSyntax = syntax; + log.debug("Detected file syntax: " + fileSyntax + " tileImageType=" + tileImageType); + break; + } + } + + public synchronized void initializeQuadKeyType() { + Pattern p = Pattern.compile("([0123]+)\\.(png|gif|jpg)", Pattern.CASE_INSENSITIVE); + Enumeration entries = zips.get(0).entries(); + String fileExt = null; + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + Matcher m = p.matcher(entry.getName()); + if (!m.matches()) + continue; + fileExt = m.group(2); + break; + } + if (fileExt == null) + return; // Error no suitable file found + fileSyntax = "%s." + fileExt; + + tileImageType = TileImageType.getTileImageType(fileExt); + p = Pattern.compile("([0123]+)\\.(" + fileExt + ")", Pattern.CASE_INSENSITIVE); + + int min = PreviewMap.MAX_ZOOM; + int max = 1; + + for (ZipFile zipFile : zips) { + entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + Matcher m = p.matcher(entry.getName()); + if (!m.matches()) + continue; + if (fileSyntax == null) + fileSyntax = "%s." + m.group(2); + int z = m.group(1).length(); + min = Math.min(min, z); + max = Math.max(max, z); + } + } + minZoom = min; + maxZoom = max; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (!initialized) + initialize(); + if (fileSyntax == null) + return null; + if (log.isTraceEnabled()) + log.trace(String.format("Loading tile z=%d x=%d y=%d", zoom, x, y)); + + if (invertYCoordinate) + y = ((1 << zoom) - y - 1); + ZipEntry entry = null; + String fileName; + switch (sourceType) { + case DIR_ZOOM_X_Y: + fileName = String.format(fileSyntax, zoom, x, y); + break; + case DIR_ZOOM_Y_X: + fileName = String.format(fileSyntax, zoom, y, x); + break; + case QUADKEY: + fileName = String.format(fileSyntax, MapSourceTools.encodeQuadTree(zoom, x, y)); + break; + default: + throw new RuntimeException("Invalid source type"); + } + for (ZipFile zip : zips) { + entry = zip.getEntry(fileName); + if (entry != null) { + InputStream in = zip.getInputStream(entry); + byte[] data = Utilities.getInputBytes(in); + in.close(); + return data; + } + } + log.debug("Map tile file not found in zip files: " + fileName); + return null; + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + byte[] data = getTileData(zoom, x, y, loadMethod); + if (data == null) + return null; + return ImageIO.read(new ByteArrayInputStream(data)); + } + + public TileImageType getTileImageType() { + return tileImageType; + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + this.loaderInfo = loaderInfo; + } + +} diff --git a/src/main/java/mobac/mapsources/custom/CustomMapSource.java b/src/main/java/mobac/mapsources/custom/CustomMapSource.java new file mode 100644 index 0000000..19f1b1a --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomMapSource.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import javax.imageio.ImageIO; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlList; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.mapsources.MapSourceTools; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.download.TileDownLoader; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSourceListener; +import mobac.program.interfaces.MapSpace; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreEntry; + +/** + * Custom tile store provider, configurable via settings.xml. + */ +@XmlRootElement +public class CustomMapSource implements HttpMapSource { + + @XmlElement(nillable = false, defaultValue = "Custom") + private String name = "Custom"; + + @XmlElement(defaultValue = "0") + private int minZoom = 0; + + @XmlElement(required = true) + private int maxZoom = 0; + + @XmlElement(defaultValue = "PNG") + protected TileImageType tileType = TileImageType.PNG; + + @XmlElement(defaultValue = "NONE") + private HttpMapSource.TileUpdate tileUpdate; + + @XmlElement(required = true, nillable = false) + protected String url = "http://127.0.0.1/{$x}_{$y}_{$z}"; + + @XmlElement(defaultValue = "false") + private boolean invertYCoordinate = false; + + @XmlElement(defaultValue = "#000000") + @XmlJavaTypeAdapter(ColorAdapter.class) + private Color backgroundColor = Color.BLACK; + + @XmlElement(required = false, defaultValue = "false") + private boolean ignoreErrors = false; + + @XmlElement(required = false, defaultValue = "") + @XmlList + private String[] serverParts = null; + private int currentServerPart = 0; + +// @XmlElement(required = false, defaultValue = "false") +// protected boolean ignoreContentMismatch = false; + + private MapSourceLoaderInfo loaderInfo = null; + + /** + * Constructor without parameters - required by JAXB + */ + protected CustomMapSource() { + } + + public CustomMapSource(String name, String url) { + this.name = name; + this.url = url; + } + +// public boolean ignoreContentMismatch() +// { +// return ignoreContentMismatch; +// } + + public TileUpdate getTileUpdate() { + return tileUpdate; + } + + public int getMaxZoom() { + return maxZoom; + } + + public int getMinZoom() { + return minZoom; + } + + public String getName() { + return name; + } + + public String getStoreName() { + return name; + } + + public TileImageType getTileImageType() { + return tileType; + } + + public HttpURLConnection getTileUrlConnection(int zoom, int tilex, int tiley) throws IOException { + String url = getTileUrl(zoom, tilex, tiley); + if (url == null) + return null; + return (HttpURLConnection) new URL(url).openConnection(); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + if (serverParts == null || serverParts.length == 0) { + return MapSourceTools.formatMapUrl(url, zoom, tilex, tiley); + } else { + currentServerPart = (currentServerPart + 1) % serverParts.length; + String serverPart = serverParts[currentServerPart]; + return MapSourceTools.formatMapUrl(url, serverPart, zoom, tilex, tiley); + } + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + if (invertYCoordinate) + y = ((1 << zoom) - y - 1); + + if (loadMethod == LoadMethod.CACHE) { + TileStoreEntry entry = TileStore.getInstance().getTile(x, y, zoom, this); + if (entry == null) + return null; + byte[] data = entry.getData(); + if (Thread.currentThread() instanceof MapSourceListener) { + ((MapSourceListener) Thread.currentThread()).tileDownloaded(data.length); + } + return data; + } + if (ignoreErrors) { + try { + return TileDownLoader.getImage(x, y, zoom, this); + } catch (Exception e) { + return null; + } + } else { + return TileDownLoader.getImage(x, y, zoom, this); + } + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + + byte[] data = getTileData(zoom, x, y, loadMethod); + + if (data == null) { + if (!ignoreErrors) + return null; + else { + int tileSize = this.getMapSpace().getTileSize(); + BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_4BYTE_ABGR); + Graphics g = (Graphics) image.getGraphics(); + try { + g.setColor(backgroundColor); + g.fillRect(0, 0, tileSize, tileSize); + } finally { + g.dispose(); + } + return image; + } + } else { + return ImageIO.read(new ByteArrayInputStream(data)); + } + } + + @Override + public String toString() { + return name; + } + + public MapSpace getMapSpace() { + return MercatorPower2MapSpace.INSTANCE_256; + } + + public Color getBackgroundColor() { + return backgroundColor; + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return loaderInfo; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + if (this.loaderInfo != null) + throw new RuntimeException("LoaderInfo already set"); + this.loaderInfo = loaderInfo; + } + +} diff --git a/src/main/java/mobac/mapsources/custom/CustomMapSourceType.java b/src/main/java/mobac/mapsources/custom/CustomMapSourceType.java new file mode 100644 index 0000000..42fb53a --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomMapSourceType.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +public enum CustomMapSourceType { + DIR_ZOOM_X_Y, DIR_ZOOM_Y_X, QUADKEY; +} diff --git a/src/main/java/mobac/mapsources/custom/CustomMultiLayerMapSource.java b/src/main/java/mobac/mapsources/custom/CustomMultiLayerMapSource.java new file mode 100644 index 0000000..6aa5509 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomMultiLayerMapSource.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlElements; +import javax.xml.bind.annotation.XmlList; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSeeAlso; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.model.TileImageType; + +/** + * + */ +@XmlRootElement +@XmlAccessorType(XmlAccessType.PROPERTY) +@XmlSeeAlso({ CustomMapSource.class }) +public class CustomMultiLayerMapSource extends AbstractMultiLayerMapSource { + + @XmlElementWrapper(name = "layers") + @XmlElements({ @XmlElement(name = "customMapSource", type = CustomMapSource.class), + @XmlElement(name = "customWmsMapSource", type = CustomWmsMapSource.class), + @XmlElement(name = "mapSource", type = StandardMapSourceLayer.class), + @XmlElement(name = "cloudMade", type = CustomCloudMade.class), + @XmlElement(name = "localTileSQLite", type = CustomLocalTileSQliteMapSource.class), + @XmlElement(name = "localTileFiles", type = CustomLocalTileFilesMapSource.class), + @XmlElement(name = "localTileZip", type = CustomLocalTileZipMapSource.class), + @XmlElement(name = "localImageFile", type = CustomLocalImageFileMapSource.class)}) + protected List layers = new ArrayList(); + + @XmlList() + protected List layersAlpha = new ArrayList(); + + @XmlElement(defaultValue = "#000000") + @XmlJavaTypeAdapter(ColorAdapter.class) + protected Color backgroundColor = Color.BLACK; + + public CustomMultiLayerMapSource() { + super(); + mapSources = new MapSource[0]; + } + + public TileImageType getTileType() { + return tileType; + } + + public void setTileType(TileImageType tileType) { + this.tileType = tileType; + } + + protected void afterUnmarshal(Unmarshaller u, Object parent) { + mapSources = new MapSource[layers.size()]; + layers.toArray(mapSources); + initializeValues(); + } + + @XmlElement(name = "name") + public String getMLName() { + return name; + } + + public void setMLName(String name) { + this.name = name; + } + + @Override + public Color getBackgroundColor() { + return backgroundColor; + } + + @Override + protected float getLayerAlpha(int layerIndex) { + if (layersAlpha.size() <= layerIndex) + return 1.0f; + + return layersAlpha.get(layerIndex); + } + + // public static void main(String[] args) { + // OutputStream os = null; + // try { + // JAXBContext context = JAXBContext.newInstance(new Class[] { CustomMultiLayerMapSource.class, + // CustomMapSource.class }); + // + // Marshaller marshaller = context.createMarshaller(); + // marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + // CustomMultiLayerMapSource ms = new CustomMultiLayerMapSource(); + // os = new FileOutputStream(new File("mapsources/Custom multi-layer map source.xml")); + // marshaller.marshal(ms, os); + // } catch (Exception e) { + // e.printStackTrace(); + // } finally { + // Utilities.closeStream(os); + // } + // } + // public static void main(String[] args) { + // try { + // JAXBContext context = JAXBContext.newInstance(new Class[] { CustomMultiLayerMapSource.class, + // CustomMapSource.class }); + // + // Unmarshaller unmarshaller = context.createUnmarshaller(); + // CustomMultiLayerMapSource ms = (CustomMultiLayerMapSource) unmarshaller.unmarshal(new + // File("mapsources/Example custom multi-layer map source.xml")); + // System.out.println(ms); + // System.out.println(ms.layers); + // } catch (Exception e) { + // e.printStackTrace(); + // } + // } +} diff --git a/src/main/java/mobac/mapsources/custom/CustomWmsMapSource.java b/src/main/java/mobac/mapsources/custom/CustomWmsMapSource.java new file mode 100644 index 0000000..464b136 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/CustomWmsMapSource.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import mobac.mapsources.MapSourceTools; + +/** + * Custom tile store provider for wms map sources, configurable via xml file + * + * @author oruxman + */ +@XmlRootElement +public class CustomWmsMapSource extends CustomMapSource { + + /** + * tested with 1.1.1, but should work with other versions + */ + @XmlElement(required = true, name = "version") + private String version = "1.1.1"; + + /** + * no spaces allowed, must be replaced with %20 in the url + */ + @XmlElement(required = true, name = "layers") + private String layers = ""; + + /** + * currently only the coordinate system epsg:4326 is supported + */ + @XmlElement(required = true, name = "coordinatesystem", defaultValue = "EPSG:4326") + private String coordinatesystem = "EPSG:4326"; + + /** + * some wms needs more parameters: &EXCEPTIONS=BLANK&Styles= ..... + */ + @XmlElement(required = false, name = "aditionalparameters") + private String additionalparameters = ""; + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + double[] coords = MapSourceTools.calculateLatLon(this, zoom, tilex, tiley); + String url = this.url + "REQUEST=GetMap" + "&LAYERS=" + layers + "&SRS=" + coordinatesystem + "&VERSION=" + + version + "&FORMAT=image/" + tileType.getMimeType() + "&BBOX=" + coords[0] + "," + coords[1] + "," + + coords[2] + "," + coords[3] + "&WIDTH=256&HEIGHT=256" + additionalparameters; + return url; + } + + public String getVersion() { + return version; + } + + public String getLayers() { + return layers; + } + + public String getCoordinatesystem() { + return coordinatesystem; + } + +} diff --git a/src/main/java/mobac/mapsources/custom/StandardMapSourceLayer.java b/src/main/java/mobac/mapsources/custom/StandardMapSourceLayer.java new file mode 100644 index 0000000..47d45a2 --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/StandardMapSourceLayer.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.custom; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +import mobac.exceptions.TileException; +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; + +@XmlRootElement +/** + * Wraps an already existing map source so that it can be loaded by name in a custom multi-layer map + */ +public class StandardMapSourceLayer implements MapSource { + + protected MapSource mapSource = null; + + @XmlElement(name = "name") + protected String mapSourceName; + + public MapSource getMapSource() { + return mapSource; + } + + protected void afterUnmarshal(Unmarshaller u, Object parent) { + mapSource = MapSourcesManager.getInstance().getSourceByName(mapSourceName); + if (mapSource == null) + throw new RuntimeException("Invalid map source name used: " + mapSourceName); + } + + public int getMaxZoom() { + return mapSource.getMaxZoom(); + } + + public int getMinZoom() { + return mapSource.getMinZoom(); + } + + public String getName() { + return mapSource.getName(); + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + return mapSource.getTileData(zoom, x, y, loadMethod); + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + return mapSource.getTileImage(zoom, x, y, loadMethod); + } + + public TileImageType getTileImageType() { + return mapSource.getTileImageType(); + } + + public MapSpace getMapSpace() { + return mapSource.getMapSpace(); + } + + public Color getBackgroundColor() { + return mapSource.getBackgroundColor(); + } + + @Override + public String toString() { + return mapSource.toString(); + } + + @Override + public int hashCode() { + return mapSource.hashCode(); + } + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo() { + return mapSource.getLoaderInfo(); + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + mapSource.setLoaderInfo(loaderInfo); + } + + @Override + public boolean equals(Object obj) { + return mapSource.equals(obj); + } + +} diff --git a/src/main/java/mobac/mapsources/custom/package-info.java b/src/main/java/mobac/mapsources/custom/package-info.java new file mode 100644 index 0000000..66f4aaa --- /dev/null +++ b/src/main/java/mobac/mapsources/custom/package-info.java @@ -0,0 +1,12 @@ +@javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters({ + @javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter( + type = mobac.program.model.TileImageType.class, + value = mobac.program.jaxb.TileImageTypeAdapter.class + ), + @javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter( + type = java.awt.Color.class, + value = mobac.program.jaxb.ColorAdapter.class + ) + +}) +package mobac.mapsources.custom; \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/impl/DebugLocalMapSource.java b/src/main/java/mobac/mapsources/impl/DebugLocalMapSource.java new file mode 100644 index 0000000..5d36f5b --- /dev/null +++ b/src/main/java/mobac/mapsources/impl/DebugLocalMapSource.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.impl; + +import mobac.program.interfaces.FileBasedMapSource; + +/** + * A {@link FileBasedMapSource} for debugging and testing purposes + */ +public class DebugLocalMapSource extends DebugMapSource implements FileBasedMapSource { + + public void initialize() { + } + + public void reinitialize() { + } + + @Override + public String getName() { + return "DebugLocal"; + } + + @Override + public String toString() { + return "Debug (local)"; + } + +} diff --git a/src/main/java/mobac/mapsources/impl/DebugMapSource.java b/src/main/java/mobac/mapsources/impl/DebugMapSource.java new file mode 100644 index 0000000..d65cd2f --- /dev/null +++ b/src/main/java/mobac/mapsources/impl/DebugMapSource.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.impl; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Deflater; + +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; +import mobac.utilities.imageio.Png4BitWriter; + +public class DebugMapSource implements MapSource { + + private int pngCompressionLevel = Deflater.BEST_SPEED; + + private static final byte[] COLORS = { 0,// + (byte) 0xff, (byte) 0xff, (byte) 0xff, // white + (byte) 0xcc, (byte) 0xcc, (byte) 0xcc // light gray + }; + + private static final IndexColorModel COLORMODEL = new IndexColorModel(8, 2, COLORS, 1, false); + + private Color COLOR_BG = new Color(COLORS[1] & 0xFF, COLORS[2] & 0xFF, COLORS[3] & 0xFF); + private Color COLOR_VG = new Color(COLORS[4] & 0xFF, COLORS[5] & 0xFF, COLORS[6] & 0xFF); + + private static final Font FONT_LARGE = new Font("Sans Serif", Font.BOLD, 30); + + public DebugMapSource() { + } + + public Color getBackgroundColor() { + return Color.BLACK; + } + + public MapSpace getMapSpace() { + return MercatorPower2MapSpace.INSTANCE_256; + } + + public int getMaxZoom() { + return PreviewMap.MAX_ZOOM; + } + + public int getMinZoom() { + return 0; + } + + public String getName() { + return "Debug"; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(16000); + // ImageIO.write(getTileImage(zoom, x, y, LoadMethod.DEFAULT), "png", buf); + String pngMetaText = String.format("zoom=%d x=%d y=%d", zoom, x, y); + Png4BitWriter.writeImage(buf, getTileImage(zoom, x, y, loadMethod), pngCompressionLevel, pngMetaText); + return buf.toByteArray(); + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_BYTE_INDEXED, COLORMODEL); + Graphics2D g2 = image.createGraphics(); + try { + g2.setColor(COLOR_BG); + g2.fillRect(0, 0, 255, 255); + g2.setColor(COLOR_VG); + g2.drawRect(0, 0, 255, 255); + g2.drawRect(1, 1, 254, 254); + g2.drawLine(0, 0, 255, 255); + g2.drawLine(255, 0, 0, 255); + g2.setFont(FONT_LARGE); + g2.drawString("x: " + x, 8, 40); + g2.drawString("y: " + y, 8, 75); + g2.drawString("z: " + zoom, 8, 110); + return image; + } finally { + g2.dispose(); + } + } + + public TileImageType getTileImageType() { + return TileImageType.PNG; + } + + public MapSourceLoaderInfo getLoaderInfo() { + return null; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + throw new RuntimeException("LoaderInfo can not be set"); + } + + @Override + public String toString() { + return getName(); + } + +} diff --git a/src/main/java/mobac/mapsources/impl/DebugRandomLocalMapSource.java b/src/main/java/mobac/mapsources/impl/DebugRandomLocalMapSource.java new file mode 100644 index 0000000..169e50a --- /dev/null +++ b/src/main/java/mobac/mapsources/impl/DebugRandomLocalMapSource.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.impl; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; + +import javax.imageio.ImageIO; + +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; + +/** + * A {@link FileBasedMapSource} for debugging and testing purposes + */ +public class DebugRandomLocalMapSource implements MapSource, FileBasedMapSource { + + BufferedImage image = null; + byte[] imageData = null; + + public Color getBackgroundColor() { + return Color.BLACK; + } + + public MapSpace getMapSpace() { + return MercatorPower2MapSpace.INSTANCE_256; + } + + public int getMaxZoom() { + return PreviewMap.MAX_ZOOM; + } + + public int getMinZoom() { + return 0; + } + + @Override + public TileImageType getTileImageType() { + return TileImageType.PNG; + } + + public void initialize() { + } + + public void reinitialize() { + } + + @Override + public String getName() { + return "DebugRandomLocal"; + } + + @Override + public String toString() { + return "Debug Random (local)"; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + if (imageData != null) + return imageData; + synchronized (this) { + if (imageData != null) + return imageData; + ByteArrayOutputStream buf = new ByteArrayOutputStream(16000); + BufferedImage image = getTileImage(zoom, x, y, loadMethod); + if (image == null) + return null; + ImageIO.write(image, "png", buf); + imageData = buf.toByteArray(); + return imageData; + } + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + if (image != null) + return image; + synchronized (this) { + if (image != null) + return image; + BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = image.createGraphics(); + SecureRandom rnd = new SecureRandom(); + try { + g2.setColor(Color.WHITE); + g2.fillRect(0, 0, 255, 255); + for (int i = 0; i < 100; i++) { + g2.setColor(new Color(rnd.nextInt())); + int x1 = rnd.nextInt(256); + int y1 = rnd.nextInt(256); + int x2 = rnd.nextInt(256); + int y2 = rnd.nextInt(256); + g2.drawLine(x1, y1, x2, y2); + } + g2.setColor(Color.RED); + this.image = image; + return image; + } finally { + g2.dispose(); + } + } + } + + @Override + public MapSourceLoaderInfo getLoaderInfo() { + return null; + } + + @Override + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + } +} diff --git a/src/main/java/mobac/mapsources/impl/DebugTransparentLocalMapSource.java b/src/main/java/mobac/mapsources/impl/DebugTransparentLocalMapSource.java new file mode 100644 index 0000000..e7b65cf --- /dev/null +++ b/src/main/java/mobac/mapsources/impl/DebugTransparentLocalMapSource.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.impl; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; + +import javax.imageio.ImageIO; + +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.gui.mapview.PreviewMap; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; + +/** + * A {@link FileBasedMapSource} for debugging and testing purposes + */ +public class DebugTransparentLocalMapSource implements MapSource, FileBasedMapSource { + + BufferedImage image = null; + byte[] imageData = null; + + Color backgroundColor = new Color(0, 0, 0, 0); + + public Color getBackgroundColor() { + return backgroundColor; + } + + public MapSpace getMapSpace() { + return MercatorPower2MapSpace.INSTANCE_256; + } + + public int getMaxZoom() { + return PreviewMap.MAX_ZOOM; + } + + public int getMinZoom() { + return 0; + } + + @Override + public TileImageType getTileImageType() { + return TileImageType.PNG; + } + + public void initialize() { + } + + public void reinitialize() { + } + + @Override + public String getName() { + return "DebugTransparentLocal"; + } + + @Override + public String toString() { + return "Debug Transparent (local)"; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + if (imageData != null) + return imageData; + synchronized (this) { + if (imageData != null) + return imageData; + ByteArrayOutputStream buf = new ByteArrayOutputStream(16000); + BufferedImage image = getTileImage(zoom, x, y, loadMethod); + if (image == null) + return null; + ImageIO.write(image, "png", buf); + imageData = buf.toByteArray(); + return imageData; + } + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + if (image != null) + return image; + synchronized (this) { + if (image != null) + return image; + BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = image.createGraphics(); + SecureRandom rnd = new SecureRandom(); + try { + for (int i = 0; i < 100; i++) { + g2.setColor(new Color(rnd.nextInt())); + int x1 = rnd.nextInt(256); + int y1 = rnd.nextInt(256); + int x2 = rnd.nextInt(256); + int y2 = rnd.nextInt(256); + g2.drawLine(x1, y1, x2, y2); + } + g2.setColor(Color.RED); + this.image = image; + return image; + } finally { + g2.dispose(); + } + } + } + + @Override + public MapSourceLoaderInfo getLoaderInfo() { + return null; + } + + @Override + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + } +} diff --git a/src/main/java/mobac/mapsources/impl/LocalhostTestSource.java b/src/main/java/mobac/mapsources/impl/LocalhostTestSource.java new file mode 100644 index 0000000..e29d324 --- /dev/null +++ b/src/main/java/mobac/mapsources/impl/LocalhostTestSource.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.impl; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +public class LocalhostTestSource extends AbstractHttpMapSource { + + private String baseUrl; + + public LocalhostTestSource(String name, TileImageType tileType) { + this(name, 80, tileType); + } + + public LocalhostTestSource(String name, int port, TileImageType tileType) { + super(name, 0, 22, tileType); + baseUrl = "http://127.0.0.1:" + port + "/tile." + tileType + "?"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return baseUrl + "x=" + tilex + "&y=" + tiley + "&z=" + zoom; + } + +} diff --git a/src/main/java/mobac/mapsources/impl/SimpleMapSource.java b/src/main/java/mobac/mapsources/impl/SimpleMapSource.java new file mode 100644 index 0000000..9f21bce --- /dev/null +++ b/src/main/java/mobac/mapsources/impl/SimpleMapSource.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.impl; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; + +/** + * A simple {@link MapSource} implementation serving as fall-back if no other map source is available/can be loaded. + */ +public class SimpleMapSource implements MapSource { + + public SimpleMapSource() { + } + + public Color getBackgroundColor() { + return Color.WHITE; + } + + public MapSpace getMapSpace() { + return MercatorPower2MapSpace.INSTANCE_256; + } + + public int getMaxZoom() { + return 2; + } + + public int getMinZoom() { + return 0; + } + + public String getName() { + return "Simple"; + } + + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(16000); + ImageIO.write(getTileImage(zoom, x, y, LoadMethod.DEFAULT), "png", buf); + return buf.toByteArray(); + } + + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g2 = image.createGraphics(); + try { + g2.setColor(Color.WHITE); + g2.fillRect(0, 0, 255, 255); + g2.setColor(Color.BLACK); + g2.drawString("No map sources available", 8, 40); + return image; + } finally { + g2.dispose(); + } + } + + public TileImageType getTileImageType() { + return TileImageType.PNG; + } + + public MapSourceLoaderInfo getLoaderInfo() { + return null; + } + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo) { + throw new RuntimeException("LoaderInfo can not be set"); + } + + @Override + public String toString() { + return ""; + } + +} diff --git a/src/main/java/mobac/mapsources/loader/BeanShellMapSourceLoader.java b/src/main/java/mobac/mapsources/loader/BeanShellMapSourceLoader.java new file mode 100644 index 0000000..e32a743 --- /dev/null +++ b/src/main/java/mobac/mapsources/loader/BeanShellMapSourceLoader.java @@ -0,0 +1,44 @@ +package mobac.mapsources.loader; + +import java.io.File; + +import javax.swing.JOptionPane; + +import mobac.mapsources.MapSourcesManager; +import mobac.mapsources.custom.BeanShellHttpMapSource; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.MapSourceLoaderInfo.LoaderType; +import mobac.utilities.file.FileExtFilter; + +import org.apache.log4j.Logger; + +public class BeanShellMapSourceLoader { + + private final Logger log = Logger.getLogger(BeanShellMapSourceLoader.class); + private final MapSourcesManager mapSourcesManager; + private final File mapSourcesDir; + + public BeanShellMapSourceLoader(MapSourcesManager mapSourceManager, File mapSourcesDir) { + this.mapSourcesManager = mapSourceManager; + this.mapSourcesDir = mapSourcesDir; + } + + public void loadBeanShellMapSources() { + File[] customMapSourceFiles = mapSourcesDir.listFiles(new FileExtFilter(".bsh")); + for (File f : customMapSourceFiles) { + try { + BeanShellHttpMapSource mapSource = BeanShellHttpMapSource.load(f); + log.trace("BeanShell map source loaded: " + mapSource + " from file \"" + f.getName() + "\""); + mapSource.setLoaderInfo(new MapSourceLoaderInfo(LoaderType.BSH, f)); + mapSourcesManager.addMapSource(mapSource); + } catch (Exception e) { + String errorMsg = "Failed to load custom BeanShell map source \"" + f.getName() + "\": " + + e.getMessage(); + log.error(errorMsg, e); + JOptionPane.showMessageDialog(null, errorMsg, "Failed to load custom BeanShell map source", + JOptionPane.ERROR_MESSAGE); + } + } + } + +} diff --git a/src/main/java/mobac/mapsources/loader/CustomMapSourceLoader.java b/src/main/java/mobac/mapsources/loader/CustomMapSourceLoader.java new file mode 100644 index 0000000..8bfa47c --- /dev/null +++ b/src/main/java/mobac/mapsources/loader/CustomMapSourceLoader.java @@ -0,0 +1,126 @@ +package mobac.mapsources.loader; + +import java.io.File; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Arrays; + +import javax.swing.JOptionPane; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.ValidationEvent; +import javax.xml.bind.ValidationEventHandler; +import javax.xml.bind.ValidationEventLocator; + +import mobac.exceptions.MapSourceCreateException; +import mobac.mapsources.MapSourcesManager; +import mobac.mapsources.custom.CustomCloudMade; +import mobac.mapsources.custom.CustomLocalTileFilesMapSource; +import mobac.mapsources.custom.CustomLocalTileSQliteMapSource; +import mobac.mapsources.custom.CustomLocalTileZipMapSource; +import mobac.mapsources.custom.CustomMapSource; +import mobac.mapsources.custom.CustomMultiLayerMapSource; +import mobac.mapsources.custom.CustomWmsMapSource; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.WrappedMapSource; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.MapSourceLoaderInfo.LoaderType; +import mobac.utilities.file.FileExtFilter; + +import org.apache.log4j.Logger; + +public class CustomMapSourceLoader implements ValidationEventHandler { + + private final Logger log = Logger.getLogger(MapPackManager.class); + private final MapSourcesManager mapSourcesManager; + private final File mapSourcesDir; + + private final Unmarshaller unmarshaller; + + public CustomMapSourceLoader(MapSourcesManager mapSourceManager, File mapSourcesDir) { + this.mapSourcesManager = mapSourceManager; + this.mapSourcesDir = mapSourcesDir; + try { + Class[] customMapClasses = new Class[] { CustomMapSource.class, CustomWmsMapSource.class, + CustomMultiLayerMapSource.class, CustomCloudMade.class, CustomLocalTileFilesMapSource.class, + CustomLocalTileZipMapSource.class, CustomLocalTileSQliteMapSource.class }; + JAXBContext context = JAXBContext.newInstance(customMapClasses); + unmarshaller = context.createUnmarshaller(); + unmarshaller.setEventHandler(this); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXB context for custom map sources", e); + } + } + + public void loadCustomMapSources() { + File[] customMapSourceFiles = mapSourcesDir.listFiles(new FileExtFilter(".xml")); + Arrays.sort(customMapSourceFiles); + for (File f : customMapSourceFiles) { + try { + MapSource customMapSource; + Object o = unmarshaller.unmarshal(f); + if (o instanceof WrappedMapSource) + customMapSource = ((WrappedMapSource) o).getMapSource(); + else + customMapSource = (MapSource) o; + customMapSource.setLoaderInfo(new MapSourceLoaderInfo(LoaderType.XML, f)); + if (!(customMapSource instanceof FileBasedMapSource) && customMapSource.getTileImageType() == null) + log.warn("A problem occured while loading \"" + f.getName() + + "\": tileType is null - some atlas formats will produce an error!"); + log.trace("Custom map source loaded: " + customMapSource + " from file \"" + f.getName() + "\""); + mapSourcesManager.addMapSource(customMapSource); + } catch (Exception e) { + log.error("failed to load custom map source \"" + f.getName() + "\": " + e.getMessage(), e); + } + } + } + + public MapSource loadCustomMapSource(InputStream in) throws MapSourceCreateException, JAXBException { + MapSource customMapSource; + Object o = unmarshaller.unmarshal(in); + if (o instanceof WrappedMapSource) + customMapSource = ((WrappedMapSource) o).getMapSource(); + else + customMapSource = (MapSource) o; + customMapSource.setLoaderInfo(new MapSourceLoaderInfo(LoaderType.XML, null)); + log.trace("Custom map source loaded: " + customMapSource); + return customMapSource; + } + + public boolean handleEvent(ValidationEvent event) { + ValidationEventLocator loc = event.getLocator(); + String file = loc.getURL().getFile(); + try { + file = URLDecoder.decode(file, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + int lastSlash = file.lastIndexOf('/'); + if (lastSlash > 0) + file = file.substring(lastSlash + 1); + + String errorMsg = event.getMessage(); + if (errorMsg == null) { + Throwable t = event.getLinkedException(); + while (t != null && errorMsg == null) { + errorMsg = t.getMessage(); + t = t.getCause(); + } + } + + JOptionPane + .showMessageDialog(null, "

Failed to load a custom map

" + errorMsg + + "


file: \"" + file + "\"
line/column: " + loc.getLineNumber() + + "/" + loc.getColumnNumber() + "

", "Error: custom map loading failed", + JOptionPane.ERROR_MESSAGE); + log.error(event.toString()); + return false; + } + + public static class WrappedMap { + + } +} diff --git a/src/main/java/mobac/mapsources/loader/EclipseMapPackLoader.java b/src/main/java/mobac/mapsources/loader/EclipseMapPackLoader.java new file mode 100644 index 0000000..1a662e5 --- /dev/null +++ b/src/main/java/mobac/mapsources/loader/EclipseMapPackLoader.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.loader; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; +import mobac.utilities.file.DirectoryFileFilter; + +import org.apache.log4j.Logger; + +/** + * For map sources debugging inside eclipse. Allows to load the map sources directly from program class path instead of + * the map packs. + * + */ +public class EclipseMapPackLoader { + + private final Logger log = Logger.getLogger(EclipseMapPackLoader.class); + + private final MapSourcesManager mapSourcesManager; + + public EclipseMapPackLoader(MapSourcesManager mapSourcesManager) throws IOException { + this.mapSourcesManager = mapSourcesManager; + } + + public boolean loadMapPacks() throws IOException { + ClassLoader cl = EclipseMapPackLoader.class.getClassLoader(); + boolean success = false; + File binDir; + try { + binDir = new File(cl.getResource(".").toURI()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + File mapPackDir = new File(binDir, "mobac/mapsources/mappacks"); + if (!mapPackDir.isDirectory()) + return false; + File[] mapPacks = mapPackDir.listFiles(new DirectoryFileFilter()); + for (File d : mapPacks) { + File list = new File(d, "mapsources.list"); + if (!list.isFile()) + continue; + String listContent = new String(Utilities.getFileBytes(list), Charsets.UTF_8); + String[] classNames = listContent.split("\\s+"); + for (String className : classNames) { + try { + Class clazz = Class.forName(className); + MapSource ms = (MapSource) clazz.newInstance(); + mapSourcesManager.addMapSource(ms); + success = true; + } catch (Exception e) { + log.error("className: \"" + className + "\"", e); + } + } + } + return success; + } + +} diff --git a/src/main/java/mobac/mapsources/loader/MapPackClassLoader.java b/src/main/java/mobac/mapsources/loader/MapPackClassLoader.java new file mode 100644 index 0000000..3c86a43 --- /dev/null +++ b/src/main/java/mobac/mapsources/loader/MapPackClassLoader.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.loader; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Loads all available classes from the map source packages and everything else from the fallback + * {@link ClassLoader}. Therefore in difference to the standard parent {@link ClassLoader} concept this implementation + * first tries to load the and then asks the fallback whereas usually it is the opposite (first try to load via parent + * and only if that fails try to do it self). + */ +public class MapPackClassLoader extends URLClassLoader { + + private final ClassLoader fallback; + + public MapPackClassLoader(URL url, ClassLoader fallback) { + this(new URL[] { url }, fallback); + } + + protected MapPackClassLoader(URL[] urls, ClassLoader fallback) { + super(urls, null); + this.fallback = fallback; + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + try { + return super.loadClass(name, resolve); + } catch (ClassNotFoundException e) { + return fallback.loadClass(name); + } + } + +} diff --git a/src/main/java/mobac/mapsources/loader/MapPackManager.java b/src/main/java/mobac/mapsources/loader/MapPackManager.java new file mode 100644 index 0000000..72eaf17 --- /dev/null +++ b/src/main/java/mobac/mapsources/loader/MapPackManager.java @@ -0,0 +1,481 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSigner; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import javax.swing.JOptionPane; + +import mobac.exceptions.MapSourceCreateException; +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.exceptions.UpdateFailedException; +import mobac.mapsources.MapSourcesManager; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.interfaces.MapSource; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.MapSourceLoaderInfo.LoaderType; +import mobac.program.model.Settings; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.file.FileExtFilter; + +import org.apache.commons.codec.binary.Hex; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +public class MapPackManager { + + private final Logger log = Logger.getLogger(MapPackManager.class); + + private final int requiredMapPackVersion; + + private final File mapPackDir; + + private final X509Certificate mapPackCert; + + public MapPackManager(File mapPackDir) throws CertificateException, IOException { + this.mapPackDir = mapPackDir; + requiredMapPackVersion = Integer.parseInt(System.getProperty("mobac.mappackversion")); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection certs = cf.generateCertificates(Utilities + .loadResourceAsStream("cert/MapPack.cer")); + mapPackCert = (X509Certificate) certs.iterator().next(); + } + + /** + * Searches for updated map packs, verifies the signature + * + * @throws IOException + */ + public void installUpdates() throws IOException { + File[] newMapPacks = mapPackDir.listFiles(new FileExtFilter(".jar.new")); + if (newMapPacks == null) + throw new IOException("Failed to enumerate installable mappacks"); + for (File newMapPack : newMapPacks) { + try { + testMapPack(newMapPack); + String name = newMapPack.getName(); + name = name.substring(0, name.length() - 4); // remove ".new" + File oldMapPack = new File(mapPackDir, name); + if (oldMapPack.isFile()) { + // TODO: Check if new map pack file is still compatible + // TODO: Check if the downloaded version is newer + File oldMapPack2 = new File(mapPackDir, name + ".old"); + Utilities.renameFile(oldMapPack, oldMapPack2); + } + if (!newMapPack.renameTo(oldMapPack)) + throw new IOException("Failed to rename file: " + newMapPack); + } catch (CertificateException e) { + Utilities.deleteFile(newMapPack); + log.error("Map pack certificate cerificateion failed (" + newMapPack.getName() + + ") installation aborted and file was deleted"); + } + } + } + + public File[] getAllMapPackFiles() { + return mapPackDir.listFiles(new FileExtFilter(".jar")); + } + + public void loadMapPacks(MapSourcesManager mapSourcesManager) throws IOException, CertificateException { + File[] mapPacks = getAllMapPackFiles(); + for (File mapPackFile : mapPacks) { + File oldMapPackFile = new File(mapPackFile.getAbsolutePath() + ".old"); + try { + loadMapPack(mapPackFile, mapSourcesManager); + if (oldMapPackFile.isFile()) + Utilities.deleteFile(oldMapPackFile); + } catch (MapSourceCreateException e) { + if (oldMapPackFile.isFile()) { + mapPackFile.deleteOnExit(); + File newMapPackFile = new File(mapPackFile.getAbsolutePath() + ".new"); + Utilities.renameFile(oldMapPackFile, newMapPackFile); + try { + JOptionPane.showMessageDialog(null, + I18nUtils.localizedStringForKey("msg_update_map_pack_error"), + I18nUtils.localizedStringForKey("msg_update_map_pack_error_title"), + JOptionPane.INFORMATION_MESSAGE); + System.exit(1); + } catch (Exception e1) { + log.error(e1.getMessage(), e1); + } + } + GUIExceptionHandler.processException(e); + } catch (CertificateException e) { + throw e; + } catch (Exception e) { + throw new IOException("Failed to load map pack: " + mapPackFile, e); + } + } + } + + public void loadMapPack(File mapPackFile, MapSourcesManager mapSourcesManager) throws CertificateException, + IOException, MapSourceCreateException { + // testMapPack(mapPackFile); + URLClassLoader urlCl; + URL url = mapPackFile.toURI().toURL(); + urlCl = new MapPackClassLoader(url, ClassLoader.getSystemClassLoader()); + InputStream manifestIn = urlCl.getResourceAsStream("META-INF/MANIFEST.MF"); + String rev = null; + if (manifestIn != null) { + Manifest mf = new Manifest(manifestIn); + rev = mf.getMainAttributes().getValue("MapPackRevision"); + manifestIn.close(); + if (rev != null) { + if ("exported".equals(rev)) { + rev = ProgramInfo.getRevisionStr(); + } else { + rev = Integer.toString(Utilities.parseSVNRevision(rev)); + } + } + mf = null; + } + MapSourceLoaderInfo loaderInfo = new MapSourceLoaderInfo(LoaderType.MAPPACK, mapPackFile, rev); + final Iterator iterator = ServiceLoader.load(MapSource.class, urlCl).iterator(); + + while (iterator.hasNext()) { + try { + MapSource ms = iterator.next(); + ms.setLoaderInfo(loaderInfo); + mapSourcesManager.addMapSource(ms); + log.trace("Loaded map source: " + ms.toString() + " (name: " + ms.getName() + ")"); + } catch (Error e) { + urlCl = null; + throw new MapSourceCreateException("Failed to load a map sources from map pack: " + + mapPackFile.getName() + " " + e.getMessage(), e); + } + } + } + + public String downloadMD5SumList() throws IOException, UpdateFailedException { + String md5eTag = Settings.getInstance().mapSourcesUpdate.etag; + log.debug("Last md5 eTag: " + md5eTag); + String updateUrl = System.getProperty("mobac.updateurl"); + if (updateUrl == null) + throw new RuntimeException("Update url not present"); + + byte[] data = null; + + // Proxy p = new Proxy(Type.HTTP, InetSocketAddress.createUnresolved("localhost", 8888)); + HttpURLConnection conn = (HttpURLConnection) new URL(updateUrl).openConnection(); + conn.setInstanceFollowRedirects(false); + if (md5eTag != null) + conn.addRequestProperty("If-None-Match", md5eTag); + int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { + log.debug("No newer md5 file available"); + return null; + } + if (responseCode != HttpURLConnection.HTTP_OK) + throw new UpdateFailedException("Invalid HTTP response: " + responseCode + " for update url " + + conn.getURL()); + // Case HTTP_OK + InputStream in = conn.getInputStream(); + data = Utilities.getInputBytes(in); + in.close(); + Settings.getInstance().mapSourcesUpdate.etag = conn.getHeaderField("ETag"); + log.debug("New md5 file retrieved"); + String md5sumList = new String(data); + return md5sumList; + } + + /** + * Clean up old files (.jar.new and jar.unverified)in mapsources directory + * + * @throws IOException + */ + public void cleanMapPackDir() throws IOException { + File[] newMapPacks = mapPackDir.listFiles(new FileExtFilter(".jar.new")); + for (File newMapPack : newMapPacks) + Utilities.deleteFile(newMapPack); + File[] unverifiedMapPacks = mapPackDir.listFiles(new FileExtFilter(".jar.unverified")); + for (File unverifiedMapPack : unverifiedMapPacks) + Utilities.deleteFile(unverifiedMapPack); + } + + /** + * Performs on map sources online update + * + * @return
    + *
  • 0: no change in online md5 sum file (based on ETag)
  • + *
  • -1: Online md5 file is empty indicationg that this MOBAc versiosn is no longer supported
  • + *
  • x>0: Number of updated map packs
  • + *
+ * @throws IOException + */ + public int updateMapPacks() throws UpdateFailedException, UnrecoverableDownloadException, IOException { + String updateBaseUrl = System.getProperty("mobac.updatebaseurl"); + if (updateBaseUrl == null) + throw new RuntimeException("Update base url not present"); + + cleanMapPackDir(); + String md5sumList = downloadMD5SumList(); + if (md5sumList == null) + return 0; // no new md5 file available + if (md5sumList.length() == 0) + return -1; // empty file means - outdated version + int updateCount = 0; + String[] outdatedMapPacks = searchForOutdatedMapPacks(md5sumList); + for (String mapPack : outdatedMapPacks) { + log.debug("Updaing map pack: " + mapPack); + try { + File newMapPackFile = downloadMapPack(updateBaseUrl, mapPack); + try { + testMapPack(newMapPackFile); + } catch (CertificateException e) { + // Certificate validation failed + log.error(e.getMessage(), e); + Utilities.deleteFile(newMapPackFile); + continue; + } + log.debug("Verification of map pack \"" + mapPack + "\" passed successfully"); + + // Check if the downloaded version is newer + int newRev = getMapPackRevision(newMapPackFile); + File oldMapPack = new File(mapPackDir, mapPack); + int oldRev = -1; + if (oldMapPack.isFile()) + oldRev = getMapPackRevision(oldMapPack); + if (newRev < oldRev) { + log.warn("Downloaded map pack was older than existing map pack - ignoring update"); + Utilities.deleteFile(newMapPackFile); + } else { + String name = newMapPackFile.getName(); + name = name.replace(".unverified", ".new"); + File f = new File(newMapPackFile.getParentFile(), name); + // Change file extension + Utilities.renameFile(newMapPackFile, f); + updateCount++; + } + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + return updateCount; + } + + public int getMapPackRevision(File mapPackFile) throws ZipException, IOException { + ZipFile zip = new ZipFile(mapPackFile); + try { + ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF"); + if (entry == null) + throw new ZipException("Unable to find MANIFEST.MF"); + Manifest mf = new Manifest(zip.getInputStream(entry)); + Attributes a = mf.getMainAttributes(); + String mpv = a.getValue("MapPackRevision").trim(); + return Utilities.parseSVNRevision(mpv); + } catch (NumberFormatException e) { + return -1; + } finally { + zip.close(); + } + } + + public File downloadMapPack(String baseURL, String mapPackFilename) throws IOException { + if (!mapPackFilename.endsWith(".jar")) + throw new IOException("Invalid map pack filename"); + byte[] mapPackData = Utilities.downloadHttpFile(baseURL + mapPackFilename); + File newMapPackFile = new File(mapPackDir, mapPackFilename + ".unverified"); + FileOutputStream out = new FileOutputStream(newMapPackFile); + try { + out.write(mapPackData); + out.flush(); + } finally { + Utilities.closeStream(out); + } + log.debug("New map pack \"" + mapPackFilename + "\" successfully downloaded"); + return newMapPackFile; + } + + /** + * + * @param md5sumList + * @return Array of filenames of map packs which are outdated + */ + public String[] searchForOutdatedMapPacks(String md5sumList) throws UpdateFailedException { + ArrayList outdatedMappacks = new ArrayList(); + String[] md5s = md5sumList.split("[\\n\\r]+"); + Pattern linePattern = Pattern.compile("([0-9a-f]{32}) (mp-[\\w]+\\.jar)"); + + for (String line : md5s) { + line = line.trim(); + if (line.length() == 0) + continue; + Matcher m = linePattern.matcher(line); + if (!m.matches()) { + throw new UpdateFailedException("Invalid content found in md5 list: \"" + line + "\""); + } + String md5 = m.group(1); + String filename = m.group(2); + // Check if there is already an update map pack + File mapPackFile = new File(mapPackDir, filename + ".new"); + if (!mapPackFile.isFile()) + mapPackFile = new File(mapPackDir, filename); + if (!mapPackFile.isFile()) { + outdatedMappacks.add(filename); + log.debug("local map pack file missing: " + filename); + continue; + } + try { + String localmd5 = generateMappackMD5(mapPackFile); + if (localmd5.equals(md5)) + continue; // No change in map pack + log.debug("Found outdated map pack: \"" + filename + "\" local md5: " + localmd5 + " remote md5: " + + md5); + outdatedMappacks.add(filename); + } catch (Exception e) { + log.error("Failed to generate md5sum of " + mapPackFile, e); + } + } + String[] result = new String[outdatedMappacks.size()]; + outdatedMappacks.toArray(result); + return result; + } + + /** + * Calculate the md5sum on all files in the map pack file (except those in META-INF) and their filenames inclusive + * path in the map pack file). + * + * @param mapPackFile + * @return + * @throws IOException + * @throws NoSuchAlgorithmException + */ + public String generateMappackMD5(File mapPackFile) throws IOException, NoSuchAlgorithmException { + ZipFile zip = new ZipFile(mapPackFile); + try { + Enumeration entries = zip.entries(); + MessageDigest md5Total = MessageDigest.getInstance("MD5"); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + + if (entry.isDirectory()) + continue; + // Do not hash files from META-INF + String name = entry.getName(); + if (name.toUpperCase().startsWith("META-INF")) + continue; + md5.reset(); + InputStream in = zip.getInputStream(entry); + byte[] data = Utilities.getInputBytes(in); + in.close(); + // name = name.replaceAll("\\\\", "/"); + byte[] digest = md5.digest(data); + log.trace("Hashsum " + Hex.encodeHexString(digest) + " includes \"" + name + "\""); + md5Total.update(digest); + md5Total.update(name.getBytes()); + } + String md5sum = Hex.encodeHexString(md5Total.digest()); + log.trace("md5sum of " + mapPackFile.getName() + ": " + md5sum); + return md5sum; + } finally { + zip.close(); + } + } + + /** + * Verifies the class file signatures of the specified map pack + * + * @param mapPackFile + * @throws IOException + * @throws CertificateException + */ + public void testMapPack(File mapPackFile) throws IOException, CertificateException { + String fileName = mapPackFile.getName(); + JarFile jf = new JarFile(mapPackFile, true); + try { + Enumeration it = jf.entries(); + while (it.hasMoreElements()) { + JarEntry entry = it.nextElement(); + // We verify only class files + if (!entry.getName().endsWith(".class")) + continue; // directory or other entry + // Get the input stream (triggers) the signature verification for the specific class + Utilities.readFully(jf.getInputStream(entry)); + if (entry.getCodeSigners() == null) + throw new CertificateException("Unsigned class file found: " + entry.getName()); + CodeSigner signer = entry.getCodeSigners()[0]; + List cp = signer.getSignerCertPath().getCertificates(); + if (cp.size() > 1) + throw new CertificateException("Signature certificate not accepted: " + + "certificate path contains more than one certificate"); + // Compare the used certificate with the mapPack certificate + if (!mapPackCert.equals(cp.get(0))) + throw new CertificateException("Signature certificate not accepted: " + + "not the MapPack signer certificate"); + } + Manifest mf = jf.getManifest(); + Attributes a = mf.getMainAttributes(); + String mpv = a.getValue("MapPackVersion"); + if (mpv == null) + throw new IOException("MapPackVersion info missing!"); + int mapPackVersion = Integer.parseInt(mpv); + if (requiredMapPackVersion != mapPackVersion) + throw new IOException("This pack \"" + fileName + "\" is not compatible with this MOBAC version."); + ZipEntry entry = jf.getEntry("META-INF/services/mobac.program.interfaces.MapSource"); + if (entry == null) + throw new IOException("MapSources services list is missing in file " + fileName); + } finally { + jf.close(); + } + + } + + public static void main(String[] args) { + try { + Logging.configureConsoleLogging(Level.DEBUG); + ProgramInfo.initialize(); + MapPackManager mpm = new MapPackManager(new File("mapsources")); + // System.out.println(mpm.generateMappackMD5(new File("mapsources/mp-bing.jar"))); + mpm.updateMapPacks(); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/AbstractOsmMapSource.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/AbstractOsmMapSource.java new file mode 100644 index 0000000..dceee8c --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/AbstractOsmMapSource.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.openstreetmap; + +import java.net.HttpURLConnection; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.ProgramInfo; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +public abstract class AbstractOsmMapSource extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public AbstractOsmMapSource(String name) { + super(name, 0, 18, TileImageType.PNG); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "/" + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + public TileImageType getTileImageType() { + return TileImageType.PNG; + } + + @Override + protected void prepareTileUrlConnection(HttpURLConnection conn) { + super.prepareTileUrlConnection(conn); + conn.setRequestProperty("User-agent", ProgramInfo.getUserAgent()); + } + + public String getAttributionText() { + return "© OpenStreetMap contributors, CC-BY-SA"; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org"; + } +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/CloudMade.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/CloudMade.java new file mode 100644 index 0000000..94d0afd --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/CloudMade.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.openstreetmap; + +import mobac.exceptions.MapSourceInitializationException; +import mobac.mapsources.MapSourceUrlUpdater; +import mobac.utilities.Charsets; + +public class CloudMade extends AbstractOsmMapSource { + + static { + // Initialize the custom CludMade loader + try { + Class c = Class.forName("mobac.mapsources.custom.CustomCloudMade"); + c.getField("CLOUD_MADE_CLASS").set(c, CloudMade.class); + } catch (Exception e) { + } + } + + private static final String INIT_REGEX = "\"api_key\"\\:\"([A-F0-9]+)\""; + + private String styleID; + + private String displayName; + + private String apiKey = ""; + + private static final String PATTERN = "http://%s.tile.cloudmade.com/%s/%s/256/%d/%d/%d.png"; + + private static final String[] SERVER = { "a", "b", "c" }; + + private int SERVER_NUM = 0; + + public CloudMade(String styleID, String displayName) { + super("OSM CloudMade " + styleID); + this.displayName = displayName; + this.styleID = styleID; + this.maxZoom = 18; + this.tileUpdate = TileUpdate.IfModifiedSince; + } + + public CloudMade() { + this("1", "OpenStreetMap CloudMade Default Style"); + } + + @Override + protected void initernalInitialize() throws MapSourceInitializationException { + apiKey = MapSourceUrlUpdater.loadDocumentAndExtractGroup("http://maps.cloudmade.com/", Charsets.UTF_8, + INIT_REGEX); + } + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + String url = String.format(PATTERN, new Object[] { SERVER[SERVER_NUM], apiKey, styleID, zoom, tilex, tiley }); + SERVER_NUM = (SERVER_NUM + 1) % SERVER.length; + return url; + } + + @Override + public String toString() { + return displayName; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/Hikebikemap.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/Hikebikemap.java new file mode 100644 index 0000000..35b6e4c --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/Hikebikemap.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * + */ +package mobac.mapsources.mappacks.openstreetmap; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.exceptions.TileException; +import mobac.mapsources.AbstractHttpMapSource; +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageType; + +public class Hikebikemap extends AbstractMultiLayerMapSource { + + public Hikebikemap() { + super("OpenStreetMap Hikebikemap.de", TileImageType.PNG); + mapSources = new MapSource[] { new HikebikemapBase(), new HikebikemapRelief() }; + initializeValues(); + } + + /** + * http://hikebikemap.de/ + */ + public static class HikebikemapBase extends AbstractHttpMapSource { + + public HikebikemapBase() { + super("HikebikemapTiles", 0, 17, TileImageType.PNG, HttpMapSource.TileUpdate.None); + } + + @Override + public String toString() { + return "OpenStreetMap Hikebikemap Map"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "https://a.tiles.wmflabs.org/hikebike/" + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + } + + /** + * Hill shades / relief + * + * http://hikebikemap.de/ + */ + public static class HikebikemapRelief extends AbstractHttpMapSource { + + public HikebikemapRelief() { + super("HikebikemapRelief", 0, 17, TileImageType.PNG, HttpMapSource.TileUpdate.None); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "https://a.tiles.wmflabs.org/hillshading/" + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + @Override + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + TileException, InterruptedException { + if (zoom > 16) + return null; + return super.getTileImage(zoom, x, y, loadMethod); + } + + } +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/MapQuest.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/MapQuest.java new file mode 100644 index 0000000..205df59 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/MapQuest.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.openstreetmap; + +import java.io.IOException; +import java.util.concurrent.Semaphore; + +import mobac.exceptions.TileException; +import mobac.program.model.TileImageType; + +public class MapQuest extends AbstractOsmMapSource { + + private static String[] SERVERS = { "otile1", "otile2", "otile3", "otile4" }; + private static int SERVER_NUM = 0; + + private static final Semaphore SEM = new Semaphore(2); + + public MapQuest() { + super("MapQuest"); + minZoom = 0; + maxZoom = 18; + tileUpdate = TileUpdate.IfModifiedSince; + } + + @Override + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + SEM.acquire(); + try { + return super.getTileData(zoom, x, y, loadMethod); + } finally { + SEM.release(); + } + } + + @Override + public TileImageType getTileImageType() { + return TileImageType.JPG; + } + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + String server = SERVERS[SERVER_NUM]; + SERVER_NUM = (SERVER_NUM + 1) % SERVERS.length; + String baseUrl = "http://" + server + ".mqcdn.com/tiles/1.0.0/osm"; + return baseUrl + super.getTileUrl(zoom, tilex, tiley); + } + + @Override + public String toString() { + return "OpenStreetMap MapQuest"; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/Mapnik.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/Mapnik.java new file mode 100644 index 0000000..72a0136 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/Mapnik.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.openstreetmap; + +import java.io.IOException; +import java.util.concurrent.Semaphore; + +import mobac.exceptions.TileException; + +public class Mapnik extends AbstractOsmMapSource { + + private static final String MAP_MAPNIK = "http://tile.openstreetmap.org"; + + /** + * Maximum of 2 download threads + * + * @see http://wiki.openstreetmap.org/wiki/Tile_usage_policy + */ + private static final Semaphore SEM = new Semaphore(2); + + public Mapnik() { + super("Mapnik"); + maxZoom = 16; + } + + @Override + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + SEM.acquire(); + try { + return super.getTileData(zoom, x, y, loadMethod); + } finally { + SEM.release(); + } + } + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + return MAP_MAPNIK + super.getTileUrl(zoom, tilex, tiley); + } + + public TileUpdate getTileUpdate() { + return TileUpdate.IfNoneMatch; + } + + @Override + public String toString() { + return "OpenStreetMap Mapnik"; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenPisteMap.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenPisteMap.java new file mode 100644 index 0000000..ecdbf7c --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenPisteMap.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * + */ +package mobac.mapsources.mappacks.openstreetmap; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import mobac.exceptions.DownloadFailedException; +import mobac.exceptions.StopAllDownloadsException; +import mobac.exceptions.TileException; +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +public class OpenPisteMap extends AbstractMultiLayerMapSource implements MapSourceTextAttribution { + + private static final String BASE = "http://tiles.openpistemap.org/nocontours"; + //private static final String CONTOURS = "http://tiles.openpistemap.org/contours-only"; + private static final String LAMDSHED = "http://tiles2.openpistemap.org/landshaded"; + + public OpenPisteMap() { + super("OpenPisteMapBCL", TileImageType.PNG); + mapSources = new MapSource[] { new Mapnik(), new OpenPisteMapBase(), new OpenPisteMapLandshed()/*, new OpenPisteMapContours()*/ }; + initializeValues(); + } + + @Override + public String toString() { + return "Open Piste Map"; + } + + public String getAttributionText() { + return "© OpenStreetMap contributors, CC-BY-SA"; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org"; + } + + public static abstract class AbstractOpenPisteMap extends AbstractOsmMapSource { + + public AbstractOpenPisteMap(String name) { + super(name); + } + + @Override + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + try { + return super.getTileData(zoom, x, y, loadMethod); + } catch (DownloadFailedException e) { + if (e.getHttpResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) { + throw new StopAllDownloadsException("Server blocks mass download - aborting map dowload", e); + } else + throw e; + } + } + + } + + public static class OpenPisteMapBase extends AbstractOpenPisteMap { + + public OpenPisteMapBase() { + super("OpenPisteMap"); + maxZoom = 17; + tileUpdate = HttpMapSource.TileUpdate.LastModified; + } + + @Override + public String toString() { + return "Open Piste Contours Layer"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return BASE + super.getTileUrl(zoom, tilex, tiley); + } + + } + + // public static class OpenPisteMapContours extends AbstractOpenPisteMap { + // + // public OpenPisteMapContours() { + // super("OpenPisteMapCont"); + // maxZoom = 17; + // tileUpdate = HttpMapSource.TileUpdate.LastModified; + // } + // + // @Override + // public String toString() { + // return "Open Piste Map Base Layer"; + // } + // + // public String getTileUrl(int zoom, int tilex, int tiley) { + // return CONTOURS + super.getTileUrl(zoom, tilex, tiley); + // } + // + // } + + public static class OpenPisteMapLandshed extends AbstractOpenPisteMap { + + public OpenPisteMapLandshed() { + super("OpenPisteMapLandshed"); + maxZoom = 17; + tileUpdate = HttpMapSource.TileUpdate.LastModified; + } + + @Override + public String toString() { + return "Open Piste Landshed Layer"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return LAMDSHED + super.getTileUrl(zoom, tilex, tiley); + } + } +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenSeaMap.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenSeaMap.java new file mode 100644 index 0000000..cb8a08b --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OpenSeaMap.java @@ -0,0 +1,121 @@ +package mobac.mapsources.mappacks.openstreetmap; + +import java.awt.Color; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.awt.image.FilteredImageSource; +import java.awt.image.ImageFilter; +import java.awt.image.ImageProducer; +import java.awt.image.RGBImageFilter; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import mobac.exceptions.TileException; +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.mapsources.AbstractHttpMapSource; +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.program.Logging; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; +import mobac.program.tilestore.TileStore; +import mobac.utilities.Utilities; + +/** + * http://openseamap.org/ + * @see OpenSeaMapLayer + */ +public class OpenSeaMap extends AbstractMultiLayerMapSource implements MapSourceTextAttribution { + + public static final String LAYER_OPENSEA = "http://tiles.openseamap.org/seamark/"; + + public OpenSeaMap() { + super("OpenSeaMap", TileImageType.PNG); + mapSources = new MapSource[] { new Mapnik(), new OpenSeaMapLayer() }; + initializeValues(); + } + + public String getAttributionText() { + return "© OpenStreetMap contributors, CC-BY-SA"; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org"; + } + + /** + * Not working correctly: + * + * 1. The map is a "sparse map" (only tiles are present that have content - the other are missing)
+ * 2. The map layer's background is not transparent! + */ + public static class OpenSeaMapLayer extends AbstractHttpMapSource { + + public OpenSeaMapLayer() { + super("OpenSeaMapLayer", 11, 17, TileImageType.PNG, TileUpdate.LastModified); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return LAYER_OPENSEA + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + @Override + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + InterruptedException, TileException { + byte[] data = super.getTileData(zoom, x, y, loadMethod); + if (data != null && data.length == 0) { + log.info("loaded non-existing tile"); + return null; + } + return data; + } + + @Override + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, + UnrecoverableDownloadException, InterruptedException { + try { + byte[] data = getTileData(zoom, x, y, loadMethod); + if (data == null) { + return null; + } + com.sixlegs.png.PngImage png = new com.sixlegs.png.PngImage(); + BufferedImage image = png.read(new ByteArrayInputStream(data), true); + return image; + } catch (FileNotFoundException e) { + TileStore ts = TileStore.getInstance(); + ts.putTile(ts.createNewEmptyEntry(x, y, zoom), this); + } catch (Exception e) { + Logging.LOG.error("Unknown error in OpenSeaMap", e); + } + return null; + } + + @Override + public Color getBackgroundColor() { + return Utilities.COLOR_TRANSPARENT; + } + + } + + public static Image makeColorTransparent(Image im, final Color color) { + ImageFilter filter = new RGBImageFilter() { + // the color we are looking for... Alpha bits are set to opaque + public int markerRGB = color.getRGB() | 0xFF000000; + + public final int filterRGB(int x, int y, int rgb) { + if ((rgb | 0xFF000000) == markerRGB) { + // Mark the alpha bits as zero - transparent + return 0x00FFFFFF & rgb; + } else { + // nothing to do + return rgb; + } + } + }; + + ImageProducer ip = new FilteredImageSource(im.getSource(), filter); + return Toolkit.getDefaultToolkit().createImage(ip); + } +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmPublicTransport.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmPublicTransport.java new file mode 100644 index 0000000..51ddd87 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmPublicTransport.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * + */ +package mobac.mapsources.mappacks.openstreetmap; + +import mobac.program.interfaces.HttpMapSource; + +public class OsmPublicTransport extends AbstractOsmMapSource { + + private static final String PATTERN = "http://tile.memomaps.de/tilegen/%d/%d/%d.png"; + + public OsmPublicTransport() { + super("OSMPublicTransport"); + this.maxZoom = 16; + this.minZoom = 2; + this.tileUpdate = HttpMapSource.TileUpdate.IfNoneMatch; + } + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + String url = String.format(PATTERN, new Object[] { zoom, tilex, tiley }); + return url; + } + + @Override + public String toString() { + return "OpenStreetMap Public Transport"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmStandard.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmStandard.java new file mode 100644 index 0000000..c651ce0 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/OsmStandard.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.openstreetmap; + +import java.net.HttpURLConnection; + +public class OsmStandard extends AbstractOsmMapSource { + + public static final String NAME = "OpenStreetMap Standard Tile Layer"; + private static String SERVER = "https://a.tile.openstreetmap.org"; + + public OsmStandard() { + super(NAME); + maxZoom = 16; + tileUpdate = TileUpdate.IfNoneMatch; + } + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + return SERVER + super.getTileUrl(zoom, tilex, tiley); + } + + @Override + public String toString() { + return "OpenStreetMap Standard Tile Layer"; + } + + @Override + protected void prepareTileUrlConnection(HttpURLConnection conn) { + super.prepareTileUrlConnection(conn); + conn.setInstanceFollowRedirects(true); + } + + +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/WanderreitkarteAbo.java b/src/main/java/mobac/mapsources/mappacks/openstreetmap/WanderreitkarteAbo.java new file mode 100644 index 0000000..f40c48b --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/WanderreitkarteAbo.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.openstreetmap; + +import java.io.IOException; + +import mobac.exceptions.TileException; +import mobac.program.model.Settings; + +public class WanderreitkarteAbo extends AbstractOsmMapSource { + + public static final String ABO = "http://abo.wanderreitkarte.de"; + + public WanderreitkarteAbo() { + super("WanderreitkarteAbo"); + minZoom = 2; + maxZoom = 16; + } + + @Override + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException { + if (loadMethod == LoadMethod.CACHE) + return super.getTileData(zoom, x, y, loadMethod); + + // No multi threaded download possible/allowed + // if we don't synchronize here we get a high percentage of errors + synchronized (this) { + return super.getTileData(zoom, x, y, loadMethod); + } + } + + @Override + public String getTileUrl(int zoom, int tilex, int tiley) { + String ticket = Settings.getInstance().osmHikingTicket; + if (ticket != null && ticket.length() > 0) { + return ABO + super.getTileUrl(zoom, tilex, tiley) + "/ticket/" + ticket; + } else + return null; + } + + @Override + public String toString() { + return "Reit- und Wanderkarte ($Abo)"; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/openstreetmap/mapsources.list b/src/main/java/mobac/mapsources/mappacks/openstreetmap/mapsources.list new file mode 100644 index 0000000..fe81c92 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/openstreetmap/mapsources.list @@ -0,0 +1,7 @@ +mobac.mapsources.mappacks.openstreetmap.Hikebikemap +mobac.mapsources.mappacks.openstreetmap.OpenPisteMap +mobac.mapsources.mappacks.openstreetmap.OpenSeaMap +mobac.mapsources.mappacks.openstreetmap.MapQuest +mobac.mapsources.mappacks.openstreetmap.OsmPublicTransport +mobac.mapsources.mappacks.openstreetmap.WanderreitkarteAbo +mobac.mapsources.mappacks.openstreetmap.OsmStandard diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRA.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRA.java new file mode 100644 index 0000000..c4662c4 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRA.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3376462&group_id=238075 + * + * http://www.chartbundle.com/charts/ + */ +public class ChartbundleENRA extends AbstractHttpMapSource { + + public ChartbundleENRA() { + super("cb-enra", 4, 13, TileImageType.PNG, TileUpdate.None); + } + + @Override + public String toString() { + return "Chartbundle US Area Charts"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://wms.chartbundle.com/tms/v1.0/enra/" + zoom + "/" + tilex + "/" + tiley + ".png?type=google"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRH.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRH.java new file mode 100644 index 0000000..ecf005a --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRH.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3376462&group_id=238075 + * + * http://www.chartbundle.com/charts/ + */ +public class ChartbundleENRH extends AbstractHttpMapSource { + + public ChartbundleENRH() { + super("cb-enrh", 4, 12, TileImageType.PNG, TileUpdate.None); + } + + @Override + public String toString() { + return "Chartbundle US Enroute High Charts"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://wms.chartbundle.com/tms/v1.0/enrh/" + zoom + "/" + tilex + "/" + tiley + ".png?type=google"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRL.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRL.java new file mode 100644 index 0000000..5c8e049 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleENRL.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3376462&group_id=238075 + * + * http://www.chartbundle.com/charts/ + */ +public class ChartbundleENRL extends AbstractHttpMapSource { + + public ChartbundleENRL() { + super("cb-enrl", 4, 13, TileImageType.PNG, TileUpdate.None); + } + + @Override + public String toString() { + return "Chartbundle US Enroute Low Charts"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://wms.chartbundle.com/tms/v1.0/enrl/" + zoom + "/" + tilex + "/" + tiley + ".png?type=google"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleSEC.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleSEC.java new file mode 100644 index 0000000..3395fba --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleSEC.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3376462&group_id=238075 + * + * http://www.chartbundle.com/charts/ + */ +public class ChartbundleSEC extends AbstractHttpMapSource { + + public ChartbundleSEC() { + super("cb-sec", 4, 13, TileImageType.PNG, TileUpdate.None); + } + + @Override + public String toString() { + return "Chartbundle US Sectional Charts"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://wms.chartbundle.com/tms/v1.0/sec/" + zoom + "/" + tilex + "/" + tiley + ".png?type=google"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleTAC.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleTAC.java new file mode 100644 index 0000000..6549e31 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleTAC.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3376462&group_id=238075 + * + * http://www.chartbundle.com/charts/ + */ +public class ChartbundleTAC extends AbstractHttpMapSource { + + public ChartbundleTAC() { + super("cb-tac", 4, 14, TileImageType.PNG, TileUpdate.None); + } + + @Override + public String toString() { + return "Chartbundle US Terminal Area Charts"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://wms.chartbundle.com/tms/v1.0/tac/" + zoom + "/" + tilex + "/" + tiley + ".png?type=google"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleWAC.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleWAC.java new file mode 100644 index 0000000..1f43905 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/ChartbundleWAC.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3376462&group_id=238075 + * + * http://www.chartbundle.com/charts/ + */ +public class ChartbundleWAC extends AbstractHttpMapSource { + + public ChartbundleWAC() { + super("cb-wac", 4, 12, TileImageType.PNG, TileUpdate.None); + } + + @Override + public String toString() { + return "Chartbundle US World Aeronautical Charts"; + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://wms.chartbundle.com/tms/v1.0/wac/" + zoom + "/" + tilex + "/" + tiley + ".png?type=google"; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapBase.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapBase.java new file mode 100644 index 0000000..4f7501b --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapBase.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * http://viewer.nationalmap.gov/example/services.html + */ +public class USNationalMapBase extends AbstractHttpMapSource { + + public USNationalMapBase() { + super("USGS National Map Base", 0, 15, TileImageType.JPG, TileUpdate.ETag); + } + + public String getTileUrl(int zoom, int x, int y) { + return "https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/" + zoom + "/" + y + "/" + + x; + } +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImagery.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImagery.java new file mode 100644 index 0000000..74bdaa0 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImagery.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * http://viewer.nationalmap.gov/example/services.html + */ +public class USNationalMapImagery extends AbstractHttpMapSource { + + public USNationalMapImagery() { + super("USGS National Map Imagery", 0, 15, TileImageType.JPG, TileUpdate.IfNoneMatch); + } + + private static final String BASE_URL = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/"; + + public String getTileUrl(int zoom, int x, int y) { + return BASE_URL + zoom + "/" + y + "/" + x; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImageryTopo.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImageryTopo.java new file mode 100644 index 0000000..09dbbc6 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapImageryTopo.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * http://viewer.nationalmap.gov/example/services.html + */ +public class USNationalMapImageryTopo extends AbstractHttpMapSource { + + public USNationalMapImageryTopo() { + super("USGS National Map Imagery Topo", 0, 15, TileImageType.JPG, TileUpdate.IfNoneMatch); + } + + private static final String BASE_URL = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer/tile/"; + + public String getTileUrl(int zoom, int x, int y) { + return BASE_URL + zoom + "/" + y + "/" + x; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapRelief.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapRelief.java new file mode 100644 index 0000000..6e6dfd3 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapRelief.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * Unused + */ +public class USNationalMapRelief extends AbstractHttpMapSource { + + public USNationalMapRelief() { + super("USGS National Map Relief", 0, 6, TileImageType.JPG, TileUpdate.IfNoneMatch); + } + + private static final String BASE_URL = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSShadedReliefOnly/MapServer/tile/"; + + public String getTileUrl(int zoom, int x, int y) { + return BASE_URL + zoom + "/" + y + "/" + x; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapTopo.java b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapTopo.java new file mode 100644 index 0000000..d2cae8a --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/USNationalMapTopo.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_america_north; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +public class USNationalMapTopo extends AbstractHttpMapSource { + + public USNationalMapTopo() { + super("USGS National Map Topo", 0, 15, TileImageType.JPG, TileUpdate.IfNoneMatch); + } + + private static final String BASE_URL = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/"; + + public String getTileUrl(int zoom, int x, int y) { + return BASE_URL + zoom + "/" + y + "/" + x; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_america_north/mapsources.list b/src/main/java/mobac/mapsources/mappacks/region_america_north/mapsources.list new file mode 100644 index 0000000..304b040 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_america_north/mapsources.list @@ -0,0 +1,9 @@ +mobac.mapsources.mappacks.region_america_north.ChartbundleENRA +mobac.mapsources.mappacks.region_america_north.ChartbundleENRH +mobac.mapsources.mappacks.region_america_north.ChartbundleENRL +mobac.mapsources.mappacks.region_america_north.ChartbundleSEC +mobac.mapsources.mappacks.region_america_north.ChartbundleTAC +mobac.mapsources.mappacks.region_america_north.ChartbundleWAC +mobac.mapsources.mappacks.region_america_north.USNationalMapImagery +mobac.mapsources.mappacks.region_america_north.USNationalMapImageryTopo +mobac.mapsources.mappacks.region_america_north.USNationalMapTopo diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakia.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakia.java new file mode 100644 index 0000000..66a2a3f --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakia.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +/** + * http://www.freemap.sk + * + * @author SourceForge.net user didoa, nickn17 + */ +public class FreemapSlovakia extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public FreemapSlovakia() { + super("FreemapSlovakia", 5, 16, TileImageType.PNG, TileUpdate.IfModifiedSince); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://a.freemap.sk/data/layers/presets/A/" + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + @Override + public String toString() { + return "Freemap Slovakia Car Atlas"; + } + + public String getAttributionText() { + return "© OpenStreetMap contributors, CC-BY-SA"; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org"; + } +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaCycling.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaCycling.java new file mode 100644 index 0000000..78cc44e --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaCycling.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +/** + * http://www.freemap.sk + */ +public class FreemapSlovakiaCycling extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public FreemapSlovakiaCycling() { + super("FreemapSlovakiaCyclo", 6, 16, TileImageType.PNG, TileUpdate.IfModifiedSince); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://a.freemap.sk/data/layers/presets/C/" + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + @Override + public String toString() { + return "Freemap Slovakia Cycle Map"; + } + + public String getAttributionText() { + return "© OpenStreetMap contributors, CC-BY-SA"; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org"; + } +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaHiking.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaHiking.java new file mode 100644 index 0000000..11eb952 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/FreemapSlovakiaHiking.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +/** + * http://www.freemap.sk + * + * @author SourceForge.net user didoa, nickn17 + */ +public class FreemapSlovakiaHiking extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public FreemapSlovakiaHiking() { + super("FreemapSlovakiaHiking", 6, 16, TileImageType.PNG, TileUpdate.IfModifiedSince); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + return "http://a.freemap.sk/data/layers/presets/T/" + zoom + "/" + tilex + "/" + tiley + ".png"; + } + + @Override + public String toString() { + return "Freemap Slovakia Hiking"; + } + + public String getAttributionText() { + return "© OpenStreetMap contributors, CC-BY-SA"; + } + + public String getAttributionLinkURL() { + return "http://openstreetmap.org"; + } +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/MoldovaPointMd.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/MoldovaPointMd.java new file mode 100644 index 0000000..73a4fa7 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/MoldovaPointMd.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * Institute of Geodesy Technical Research and Cadastre "INGEOCAD" under Sate Agency of Land + * Relations and Cadastre Tiles created by and sourced from point.md + * + * http://point.md/Map/#x=28.870983&y=47.017756&z=10 + * + * https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3321793&group_id=238075 + */ +public class MoldovaPointMd extends AbstractHttpMapSource { + + public MoldovaPointMd() { + super("MoldovaPointMd", 8, 18, TileImageType.PNG, TileUpdate.None); + } + + public String getTileUrl(int zoom, int x, int y) { + return "http://point.md/map/Map/GetTile?path=1/" + zoom + "/" + x + "/" + y + ".png"; + } + + @Override + public String toString() { + return "Moldova (point.md)"; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopo.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopo.java new file mode 100644 index 0000000..def1a74 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopo.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +/** + * http://osm.trail.pl/ol.xhtml + * http://sourceforge.net/tracker/?func=detail&aid=3379692&group_id=238075&atid=1105497 + */ +public class OSMapaTopo extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public OSMapaTopo() { + super("OSMapaTopo", 7, 18, TileImageType.PNG, TileUpdate.IfNoneMatch); + } + + public String getTileUrl(int zoom, int x, int y) { + return "http://osm.trail.pl/bezpoziomic/" + zoom + "/" + x + "/" + y + ".png"; + } + + @Override + public String toString() { + return "OSMapa-Topo (Poland)"; + } + + public String getAttributionText() { + // http://wiki.openstreetmap.pl/Serwer_kafelk%C3%B3w_TRAIL + return "© Data OpenStreetMap, Hosting TRAIL.PL and centuria.pl"; + } + + public String getAttributionLinkURL() { + return null; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopoContours.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopoContours.java new file mode 100644 index 0000000..27fbfd1 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/OSMapaTopoContours.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +/** + * http://osm.trail.pl/ol.xhtml + * http://sourceforge.net/tracker/?func=detail&aid=3379692&group_id=238075&atid=1105497 + */ +public class OSMapaTopoContours extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public OSMapaTopoContours() { + super("OSMapaTopoContours", 0, 18, TileImageType.PNG, TileUpdate.IfNoneMatch); + } + + public String getTileUrl(int zoom, int x, int y) { + return "http://osm.trail.pl/" + zoom + "/" + x + "/" + y + ".png"; + } + + @Override + public String toString() { + return "OSMapa-Topo with contours (PL)"; + } + + public String getAttributionText() { + // http://wiki.openstreetmap.pl/Serwer_kafelk%C3%B3w_TRAIL + return "© Data OpenStreetMap, Hosting TRAIL.PL and centuria.pl"; + } + + public String getAttributionLinkURL() { + return null; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/Turaterkep.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/Turaterkep.java new file mode 100644 index 0000000..9cd481a --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/Turaterkep.java @@ -0,0 +1,32 @@ +package mobac.mapsources.mappacks.region_europe_east; + +import java.awt.Color; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * + */ +public class Turaterkep extends AbstractHttpMapSource { + + public Turaterkep() { + super("Turaterkep256", 7, 15, TileImageType.PNG, TileUpdate.IfModifiedSince); + } + + public String getTileUrl(int zoom, int x, int y) { + return "http://a.map.turistautak.hu/tiles/turistautak-domborzattal/" + zoom + "/" + x + "/" + y + ".png"; + + } + + @Override + public String toString() { + return "Turaterkep (Hungary)"; + } + + @Override + public Color getBackgroundColor() { + return Color.WHITE; + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/UmpWawPl.java b/src/main/java/mobac/mapsources/mappacks/region_europe_east/UmpWawPl.java new file mode 100644 index 0000000..53772ba --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/UmpWawPl.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_europe_east; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.model.TileImageType; + +/** + * Darmowa Mapa Polski dla GPS Garmin - UMP-pcPL (added by "maniek-ols") + *

+ * ump.waw.pl + *

+ */ +public class UmpWawPl extends AbstractHttpMapSource { + + private static int SERVER_NUM = 0; + private static final int MAX_SERVER_NUM = 4; + + public UmpWawPl() { + super("UMP-pcPL", 0, 18, TileImageType.PNG, TileUpdate.LastModified); + } + + public String getTileUrl(int zoom, int tilex, int tiley) { + String s = "http://" + SERVER_NUM + ".tiles.ump.waw.pl/ump_tiles/" + zoom + "/" + tilex + "/" + tiley + ".png"; + SERVER_NUM = (SERVER_NUM + 1) % MAX_SERVER_NUM; + return s; + } + + @Override + public String toString() { + return getName() + " (Poland only)"; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_europe_east/mapsources.list b/src/main/java/mobac/mapsources/mappacks/region_europe_east/mapsources.list new file mode 100644 index 0000000..c16a020 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_europe_east/mapsources.list @@ -0,0 +1,8 @@ +mobac.mapsources.mappacks.region_europe_east.UmpWawPl +mobac.mapsources.mappacks.region_europe_east.FreemapSlovakia +mobac.mapsources.mappacks.region_europe_east.FreemapSlovakiaCycling +mobac.mapsources.mappacks.region_europe_east.FreemapSlovakiaHiking +mobac.mapsources.mappacks.region_europe_east.MoldovaPointMd +mobac.mapsources.mappacks.region_europe_east.Turaterkep +mobac.mapsources.mappacks.region_europe_east.OSMapaTopo +mobac.mapsources.mappacks.region_europe_east.OSMapaTopoContours diff --git a/src/main/java/mobac/mapsources/mappacks/region_oceania/NzTopoMaps.java b/src/main/java/mobac/mapsources/mappacks/region_oceania/NzTopoMaps.java new file mode 100644 index 0000000..dae8d98 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_oceania/NzTopoMaps.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mappacks.region_oceania; + +import java.net.HttpURLConnection; + +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.interfaces.MapSourceTextAttribution; +import mobac.program.model.TileImageType; + +/** + *
+ * New Zealand Topographic Maps produced by Land Information New Zealand (Government Department).
+ * http://www.linz.govt.nz/topography/topo-maps/index.aspx 
+ * 
+ * Licence: Creative Commons Attribution 3.0 New Zealand
+ * (http://creativecommons.org/licenses/by/3.0/nz/deed.en) 
+ * 
+ * Tiles created by and sourced from nztopomaps.com
+ * 
+ * http://www.nztopomaps.com/ + */ +public class NzTopoMaps extends AbstractHttpMapSource implements MapSourceTextAttribution { + + public NzTopoMaps() { + super("New Zealand Topographic Maps", 6, 15, TileImageType.PNG, TileUpdate.IfNoneMatch); + } + + public String getTileUrl(int zoom, int x, int y) { + // nzy = 2^zoom - 1 - y + int nzy = (1 << zoom) - 1 - y; + return "http://nz1.nztopomaps.com/" + zoom + "/" + x + "/" + nzy + ".png"; + } + + @Override + protected void prepareTileUrlConnection(HttpURLConnection conn) { + super.prepareTileUrlConnection(conn); + conn.addRequestProperty("Referer", "http://m.nztopomaps.com"); + } + + @Override + public String toString() { + return "nztopomaps.com (New Zealand only)"; + } + + public String getAttributionText() { + return "Images sourced from NZTopo database"; + } + + public String getAttributionLinkURL() { + return "http://nztopomaps.com"; + } + +} diff --git a/src/main/java/mobac/mapsources/mappacks/region_oceania/mapsources.list b/src/main/java/mobac/mapsources/mappacks/region_oceania/mapsources.list new file mode 100644 index 0000000..a544335 --- /dev/null +++ b/src/main/java/mobac/mapsources/mappacks/region_oceania/mapsources.list @@ -0,0 +1 @@ +mobac.mapsources.mappacks.region_oceania.NzTopoMaps \ No newline at end of file diff --git a/src/main/java/mobac/mapsources/mapspace/MapSpaceFactory.java b/src/main/java/mobac/mapsources/mapspace/MapSpaceFactory.java new file mode 100644 index 0000000..44f2bd5 --- /dev/null +++ b/src/main/java/mobac/mapsources/mapspace/MapSpaceFactory.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mapspace; + +import mobac.program.interfaces.MapSpace; + +public class MapSpaceFactory { + + /** + * @param tileSize + * @param isSpherical + * @return + */ + public static MapSpace getInstance(int tileSize, boolean isSpherical) { + if (isSpherical) + return new MercatorPower2MapSpace(tileSize); + else + return new MercatorPower2MapSpaceEllipsoidal(tileSize); + } + +} diff --git a/src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpace.java b/src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpace.java new file mode 100644 index 0000000..ed88ab1 --- /dev/null +++ b/src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpace.java @@ -0,0 +1,220 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mapspace; + +import java.awt.Point; + +import mobac.gui.mapview.PreviewMap; +import mobac.program.interfaces.MapSpace; + +/** + * Mercator projection with a world width and height of 256 * 2zoom pixel. This is the common projection used + * by OpenStreetMap and Google. It provides methods to translate coordinates from 'map space' into latitude and + * longitude (on the WGS84 ellipsoid) and vice versa. Map space is measured in pixels. The origin of the map space is + * the top left corner. The map space origin (0,0) has latitude ~85 and longitude -180 + * + *

+ * This is the only implementation that is currently supported by Mobile Atlas Creator. + *

+ *

+ * DO NOT TRY TO IMPLEMENT YOUR OWN. IT WILL MOST LIKELY NOT WORK! + *

+ * + * @see MapSpace + */ +public class MercatorPower2MapSpace implements MapSpace { + + public static final double MAX_LAT = 85.05112877980659; + public static final double MIN_LAT = -85.05112877980659; + + protected final int tileSize; + + /** + * Pre-computed values for the world size (height respectively width) in the different zoom levels. + */ + protected final int[] worldSize; + public static final MapSpace INSTANCE_256 = new MercatorPower2MapSpace(256); + + protected MercatorPower2MapSpace(int tileSize) { + this.tileSize = tileSize; + worldSize = new int[PreviewMap.MAX_ZOOM + 1]; + for (int zoom = 0; zoom < worldSize.length; zoom++) + worldSize[zoom] = tileSize * (1 << zoom); + } + + protected double radius(int zoom) { + return getMaxPixels(zoom) / (2.0 * Math.PI); + } + + public ProjectionCategory getProjectionCategory() { + return ProjectionCategory.SPHERE; + } + + /** + * Returns the absolute number of pixels in y or x, defined as: 2zoom * tileSize + * + * @param zoom + * [0..22] (for tileSize = 256) + * @return + */ + public int getMaxPixels(int zoom) { + return worldSize[zoom]; + } + + protected int falseNorthing(int aZoomlevel) { + return (-1 * getMaxPixels(aZoomlevel) / 2); + } + + /** + * Transforms latitude to pixelspace + * + * @param lat + * [-90...90] + * @param zoom + * [0..22] (for tileSize = 256) + * @return [0..2^zoom*tileSize[ + * @author Jan Peter Stotz + */ + public int cLatToY(double lat, int zoom) { + lat = Math.max(MIN_LAT, Math.min(MAX_LAT, lat)); + double sinLat = Math.sin(Math.toRadians(lat)); + double log = Math.log((1.0 + sinLat) / (1.0 - sinLat)); + int mp = getMaxPixels(zoom); + int y = (int) (mp * (0.5 - (log / (4.0 * Math.PI)))); + y = Math.min(y, mp - 1); + return y; + } + + /** + * Transform longitude to pixelspace + * + * @param lon + * [-180..180] + * @param zoom + * [0..22] (for tileSize = 256) + * @return [0..2^zoom*TILE_SIZE[ + * @author Jan Peter Stotz + */ + public int cLonToX(double lon, int zoom) { + int mp = getMaxPixels(zoom); + int x = (int) ((mp * (lon + 180l)) / 360l); + x = Math.min(x, mp - 1); + return x; + } + + /** + * Transforms pixel coordinate X to longitude + * + * @param x + * [0..2^zoom * tileSize[ + * @param zoom + * [0..22] + * @return ]-180..180[ + * @author Jan Peter Stotz + */ + public double cXToLon(int x, int zoom) { + return ((360d * x) / getMaxPixels(zoom)) - 180.0; + } + + /** + * Transforms pixel coordinate Y to latitude + * + * @param y + * [0..2^zoom * tileSize[ + * @param zoom + * [0..22] + * @return [MIN_LAT..MAX_LAT] is about [-85..85] + */ + public double cYToLat(int y, int zoom) { + y += falseNorthing(zoom); + double latitude = (Math.PI / 2) - (2 * Math.atan(Math.exp(-1.0 * y / radius(zoom)))); + return -1 * Math.toDegrees(latitude); + } + + public int getTileSize() { + return tileSize; + } + + public int moveOnLatitude(int startX, int y, int zoom, double angularDist) { + y += falseNorthing(zoom); + double lat = -1 * ((Math.PI / 2) - (2 * Math.atan(Math.exp(-1.0 * y / radius(zoom))))); + + double lon = cXToLon(startX, zoom); + double sinLat = Math.sin(lat); + + lon += Math + .toDegrees(Math.atan2(Math.sin(angularDist) * Math.cos(lat), Math.cos(angularDist) - sinLat * sinLat)); + int newX = cLonToX(lon, zoom); + int w = newX - startX; + return w; + } + + public double horizontalDistance(int zoom, int y, int xDist) { + y = Math.max(y, 0); + y = Math.min(y, getMaxPixels(zoom)); + double lat = cYToLat(y, zoom); + double lon1 = -180.0; + double lon2 = cXToLon(xDist, zoom); + + double dLon = Math.toRadians(lon2 - lon1); + + double cos_lat = Math.cos(Math.toRadians(lat)); + double sin_dLon_2 = Math.sin(dLon) / 2; + + double a = cos_lat * cos_lat * sin_dLon_2 * sin_dLon_2; + return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + } + + public int xChangeZoom(int x, int oldZoom, int newZoom) { + int zoomDiff = oldZoom - newZoom; + return (zoomDiff > 0) ? x >> zoomDiff : x << -zoomDiff; + } + + public int yChangeZoom(int y, int oldZoom, int newZoom) { + int zoomDiff = oldZoom - newZoom; + return (zoomDiff > 0) ? y >> zoomDiff : y << -zoomDiff; + } + + public Point changeZoom(Point pixelCoordinate, int oldZoom, int newZoom) { + int x = xChangeZoom(pixelCoordinate.x, oldZoom, newZoom); + int y = yChangeZoom(pixelCoordinate.y, oldZoom, newZoom); + return new Point(x, y); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + tileSize; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MercatorPower2MapSpace other = (MercatorPower2MapSpace) obj; + if (tileSize != other.tileSize) + return false; + return true; + } + +} diff --git a/src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpaceEllipsoidal.java b/src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpaceEllipsoidal.java new file mode 100644 index 0000000..ea3b49a --- /dev/null +++ b/src/main/java/mobac/mapsources/mapspace/MercatorPower2MapSpaceEllipsoidal.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.mapsources.mapspace; + +import mobac.program.interfaces.MapSpace; + +/** + * + * Provides support for true Ellipsoidal Mercator projections; + * + * Based on: + * + * GeoTools - The Open Source Java GIS Toolkit http://geotools.org + * + * (C) 1999-2008, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library 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 Lesser General Public License for more + * details. + * + * This package contains formulas from the PROJ package of USGS. USGS's work is fully acknowledged here. This derived + * work has been relicensed under LGPL with Frank Warmerdam's permission. + * + */ +public class MercatorPower2MapSpaceEllipsoidal extends MercatorPower2MapSpace { + + /** + * Difference allowed in iterative computations. + */ + private static final double ITERATION_TOLERANCE = 1E-10; + + /** + * Maximum number of iterations for iterative computations. + */ + private static final int MAXIMUM_ITERATIONS = 15; + + /** + * The square of excentricity: e² = (a²-b²)/a² where e is the excentricity, a is the semi + * major axis length and b is the semi minor axis length. + * + * For WGS84 ellipsoid a = 6378137 b = 6356752.3142 + */ + protected final double excentricitySquared = 0.00669438; + + /** + * Ellipsoid excentricity, equals to sqrt({@link + * #excentricitySquared}). Value 0 means that the ellipsoid is spherical. + */ + protected final double excentricity = Math.sqrt(excentricitySquared); + + public static final MapSpace INSTANCE_256 = new MercatorPower2MapSpaceEllipsoidal(256); + + protected MercatorPower2MapSpaceEllipsoidal(int tileSize) { + super(tileSize); + } + + @Override + public ProjectionCategory getProjectionCategory() { + return ProjectionCategory.ELLIPSOID; + } + + /* + * (non-Javadoc) + * + * @see mobac.mapsources.mapspace.MercatorPower2MapSpace#cLatToY(double, int) + */ + @Override + public int cLatToY(double lat, int zoom) { + lat = Math.max(MIN_LAT, Math.min(MAX_LAT, lat)); + lat = Math.toRadians(lat); + lat = -Math.log(tsfn(lat, Math.sin(lat))); + int mp = getMaxPixels(zoom); + int y = (-1) * (int) (mp * lat / (2 * Math.PI)); + y = y - falseNorthing(zoom) - (y > 0 ? -1 : 1); + y = Math.min(y, mp - 1); + return y; + } + + /* + * (non-Javadoc) + * + * @see mobac.mapsources.mapspace.MercatorPower2MapSpace#cYToLat(int, int) + */ + @Override + public double cYToLat(int y, int zoom) { + + int y2 = y + falseNorthing(zoom); + double latitude = Math.exp(-y2 / radius(zoom)); + try { + latitude = cphi2(latitude); + } catch (Exception e) { + // No convergence; try spheric aproximation. + return super.cYToLat(y, zoom); + } + return -1 * Math.toDegrees(latitude); + } + + /** + * Iteratively solve equation (7-9) from Snyder. + */ + private double cphi2(final double ts) throws Exception { + final double eccnth = 0.5 * excentricity; + double phi = (Math.PI / 2) - 2.0 * Math.atan(ts); + for (int i = 0; i < MAXIMUM_ITERATIONS; i++) { + final double con = excentricity * Math.sin(phi); + final double dphi = (Math.PI / 2) - 2.0 * Math.atan(ts * Math.pow((1 - con) / (1 + con), eccnth)) - phi; + phi += dphi; + if (Math.abs(dphi) <= ITERATION_TOLERANCE) { + return phi; + } + } + // No convergence, wrong parameters. + throw new Exception(); + } + + /** + * Computes function (15-9) and (9-13) from Snyder. Equivalent to negative of function (7-7). + */ + private double tsfn(final double phi, double sinphi) { + sinphi *= excentricity; + /* + * NOTE: change sign to get the equivalent of Snyder (7-7). + */ + return Math.tan(0.5 * (Math.PI / 2 - phi)) / Math.pow((1 - sinphi) / (1 + sinphi), 0.5 * excentricity); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(excentricity); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(excentricitySquared); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + MercatorPower2MapSpaceEllipsoidal other = (MercatorPower2MapSpaceEllipsoidal) obj; + if (Double.doubleToLongBits(excentricity) != Double.doubleToLongBits(other.excentricity)) + return false; + if (Double.doubleToLongBits(excentricitySquared) != Double.doubleToLongBits(other.excentricitySquared)) + return false; + return true; + } + +} diff --git a/src/main/java/mobac/optional/JavaAdvancedImaging.java b/src/main/java/mobac/optional/JavaAdvancedImaging.java new file mode 100644 index 0000000..50d28d9 --- /dev/null +++ b/src/main/java/mobac/optional/JavaAdvancedImaging.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.optional; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.image.BufferedImage; + +import javax.media.jai.RenderedOp; +import javax.media.jai.operator.ColorQuantizerDescriptor; + +/** + * Centralizes all methods that require the optional Java Advanced Imaging + * library. + * + */ +public class JavaAdvancedImaging { + + // private static final Logger log = + // Logger.getLogger(JavaAdvancedImaging.class); + + public static BufferedImage colorReduceMedianCut(BufferedImage image, int colorCount) { + int pixelBits = image.getColorModel().getPixelSize(); + if (pixelBits != 24) { + /* + * For preventing the javax.media.jai.util.ImagingException: All + * factories fail for the operation "ColorQuantizer" we have to + * create a "compatible" (e.g. TYPE_3BYTE_BGR) BufferedImage + */ + BufferedImage trueColorImage = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); + Graphics g = trueColorImage.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + image = trueColorImage; + } + RenderedOp ro = ColorQuantizerDescriptor.create(image, ColorQuantizerDescriptor.MEDIANCUT, // + new Integer(colorCount), // Max number of colors + null, null, new Integer(1), Integer.valueOf(1), null); + return ro.getAsBufferedImage(); + } +} diff --git a/src/main/java/mobac/program/AtlasThread.java b/src/main/java/mobac/program/AtlasThread.java new file mode 100644 index 0000000..9d545ec --- /dev/null +++ b/src/main/java/mobac/program/AtlasThread.java @@ -0,0 +1,456 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +import java.awt.Toolkit; +import java.io.File; +import java.io.IOException; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapDownloadSkippedException; +import mobac.gui.AtlasProgress; +import mobac.gui.AtlasProgress.AtlasCreationController; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.atlascreators.tileprovider.DownloadedTileProvider; +import mobac.program.atlascreators.tileprovider.FilteredMapSourceProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.download.DownloadJobProducerThread; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.DownloadJobListener; +import mobac.program.interfaces.DownloadableElement; +import mobac.program.interfaces.FileBasedMapSource; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSource.LoadMethod; +import mobac.program.model.AtlasOutputFormat; +import mobac.program.model.Settings; +import mobac.program.tilestore.TileStore; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.tar.TarIndex; +import mobac.utilities.tar.TarIndexedArchive; + +import org.apache.log4j.Logger; + +public class AtlasThread extends Thread implements DownloadJobListener, AtlasCreationController { + + private static final Logger log = Logger.getLogger(AtlasThread.class); + private static int threadNum = 0; + + private File customAtlasDir = null; + private boolean quitMobacAfterAtlasCreation = false; + + private DownloadJobProducerThread djp = null; + private JobDispatcher downloadJobDispatcher; + private AtlasProgress ap; // The GUI showing the progress + + private AtlasInterface atlas; + private AtlasCreator atlasCreator = null; + private PauseResumeHandler pauseResumeHandler; + + private int activeDownloads = 0; + private int jobsCompleted = 0; + private int jobsRetryError = 0; + private int jobsPermanentError = 0; + private int maxDownloadRetries = 1; + + public AtlasThread(AtlasInterface atlas) throws AtlasTestException { + this(atlas, atlas.getOutputFormat().createAtlasCreatorInstance()); + } + + public AtlasThread(AtlasInterface atlas, AtlasCreator atlasCreator) throws AtlasTestException { + super("AtlasThread " + getNextThreadNum()); + ap = new AtlasProgress(this); + this.atlas = atlas; + this.atlasCreator = atlasCreator; + testAtlas(); + TileStore.getInstance().closeAll(); + maxDownloadRetries = Settings.getInstance().downloadRetryCount; + pauseResumeHandler = new PauseResumeHandler(); + } + + private void testAtlas() throws AtlasTestException { + try { + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + MapSource mapSource = map.getMapSource(); + if (!atlasCreator.testMapSource(mapSource)) + throw new AtlasTestException("The selected atlas output format \"" + atlas.getOutputFormat() + + "\" does not support the map source \"" + map.getMapSource() + "\""); + } + } + } catch (AtlasTestException e) { + throw e; + } catch (Exception e) { + throw new AtlasTestException(e); + } + } + + private static synchronized int getNextThreadNum() { + threadNum++; + return threadNum; + } + + public void run() { + GUIExceptionHandler.registerForCurrentThread(); + log.info("Starting creation of " + atlas.getOutputFormat() + " atlas \"" + atlas.getName() + "\""); + if (customAtlasDir != null) + log.debug("Target directory: " + customAtlasDir); + ap.setDownloadControlerListener(this); + try { + createAtlas(); + log.info("Altas creation finished"); + if (quitMobacAfterAtlasCreation) + System.exit(0); + } catch (OutOfMemoryError e) { + System.gc(); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + String message = I18nUtils.localizedStringForKey("msg_out_of_memory_head"); + int maxMem = Utilities.getJavaMaxHeapMB(); + if (maxMem > 0) + message += String.format(I18nUtils.localizedStringForKey("msg_out_of_memory_detail"), maxMem); + JOptionPane.showMessageDialog(null, message, + I18nUtils.localizedStringForKey("msg_out_of_memory_title"), JOptionPane.ERROR_MESSAGE); + ap.closeWindow(); + } + }); + log.error("Out of memory: ", e); + } catch (InterruptedException e) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + JOptionPane.showMessageDialog(null, I18nUtils.localizedStringForKey("msg_atlas_download_abort"), + I18nUtils.localizedStringForKey("Information"), JOptionPane.INFORMATION_MESSAGE); + ap.closeWindow(); + } + }); + log.info("Altas creation was interrupted by user"); + } catch (Exception e) { + log.error("Altas creation aborted because of an error: ", e); + GUIExceptionHandler.showExceptionDialog(e); + } + System.gc(); + if (quitMobacAfterAtlasCreation) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + } + System.exit(1); + } + } + + /** + * Create atlas: For each map download the tiles and perform atlas/map creation + */ + protected void createAtlas() throws InterruptedException, IOException { + + long totalNrOfOnlineTiles = atlas.calculateTilesToDownload(); + + for (LayerInterface l : atlas) { + for (MapInterface m : l) { + // Offline map sources are not relevant for the maximum tile limit. + if (m.getMapSource() instanceof FileBasedMapSource) + totalNrOfOnlineTiles -= m.calculateTilesToDownload(); + } + } + + if (totalNrOfOnlineTiles > 1000000) { + // NumberFormat f = DecimalFormat.getInstance(); + JOptionPane.showMessageDialog(null, String.format( + I18nUtils.localizedStringForKey("msg_too_many_tiles_msg"), 1000000, totalNrOfOnlineTiles), I18nUtils + .localizedStringForKey("msg_too_many_tiles_title"), JOptionPane.ERROR_MESSAGE); + return; + } + try { + atlasCreator.startAtlasCreation(atlas, customAtlasDir); + } catch (AtlasTestException e) { + JOptionPane.showMessageDialog(null, e.getMessage(), "Atlas format restriction violated", + JOptionPane.ERROR_MESSAGE); + return; + } + + ap.initAtlas(atlas); + ap.setVisible(true); + + Settings s = Settings.getInstance(); + + downloadJobDispatcher = new JobDispatcher(s.downloadThreadCount, pauseResumeHandler, ap); + try { + for (LayerInterface layer : atlas) { + atlasCreator.initLayerCreation(layer); + for (MapInterface map : layer) { + try { + while (!createMap(map)) + ; + } catch (InterruptedException e) { + throw e; // User has aborted + } catch (MapDownloadSkippedException e) { + // Do nothing and continue with next map + } catch (Exception e) { + log.error("", e); + String[] options = { I18nUtils.localizedStringForKey("Continue"), + I18nUtils.localizedStringForKey("Abort"), + I18nUtils.localizedStringForKey("dlg_download_show_error_report") }; + int a = JOptionPane.showOptionDialog(null, + I18nUtils.localizedStringForKey("dlg_download_erro_head") + e.getMessage() + "\n[" + + e.getClass().getSimpleName() + "]\n\n", + I18nUtils.localizedStringForKey("Error"), 0, JOptionPane.ERROR_MESSAGE, null, options, + options[0]); + switch (a) { + case 2: + GUIExceptionHandler.processException(e); + case 1: + throw new InterruptedException(); + } + } + } + atlasCreator.finishLayerCreation(); + } + } catch (InterruptedException e) { + atlasCreator.abortAtlasCreation(); + throw e; + } catch (Error e) { + atlasCreator.abortAtlasCreation(); + throw e; + } finally { + // In case of an abort: Stop create new download jobs + if (djp != null) + djp.cancel(); + downloadJobDispatcher.terminateAllWorkerThreads(); + if (!atlasCreator.isAborted()) + atlasCreator.finishAtlasCreation(); + ap.atlasCreationFinished(); + } + + } + + /** + * + * @param map + * @return true if map creation process was finished and false if something went wrong and the user decided to retry + * map download + * @throws Exception + */ + public boolean createMap(MapInterface map) throws Exception { + TarIndex tileIndex = null; + TarIndexedArchive tileArchive = null; + + jobsCompleted = 0; + jobsRetryError = 0; + jobsPermanentError = 0; + + ap.initMapDownload(map); + if (currentThread().isInterrupted()) + throw new InterruptedException(); + + // Prepare the tile store directory + // ts.prepareTileStore(map.getMapSource()); + + /*** + * In this section of code below, tiles for Atlas is being downloaded and saved in the temporary layer tar file + * in the system temp directory. + **/ + int zoom = map.getZoom(); + + final int tileCount = (int) map.calculateTilesToDownload(); + + ap.setZoomLevel(zoom); + try { + tileArchive = null; + TileProvider mapTileProvider; + if (!(map.getMapSource() instanceof FileBasedMapSource)) { + // For online maps we download the tiles first and then start creating the map if + // we are sure we got all tiles + if (!AtlasOutputFormat.TILESTORE.equals(atlas.getOutputFormat())) { + String tempSuffix = "MOBAC_" + atlas.getName() + "_" + zoom + "_"; + File tileArchiveFile = File.createTempFile(tempSuffix, ".tar", DirectoryManager.tempDir); + // If something goes wrong the temp file only persists until the VM exits + tileArchiveFile.deleteOnExit(); + log.debug("Writing downloaded tiles to " + tileArchiveFile.getPath()); + tileArchive = new TarIndexedArchive(tileArchiveFile, tileCount); + } else + log.debug("Downloading to tile store only"); + + djp = new DownloadJobProducerThread(this, downloadJobDispatcher, tileArchive, (DownloadableElement) map); + + boolean failedMessageAnswered = false; + + while (djp.isAlive() || (downloadJobDispatcher.getWaitingJobCount() > 0) + || downloadJobDispatcher.isAtLeastOneWorkerActive()) { + Thread.sleep(500); + if (!failedMessageAnswered && (jobsRetryError > 50) && !ap.ignoreDownloadErrors()) { + pauseResumeHandler.pause(); + String[] answers = new String[] { I18nUtils.localizedStringForKey("Continue"), + I18nUtils.localizedStringForKey("Retry"), I18nUtils.localizedStringForKey("Skip"), + I18nUtils.localizedStringForKey("Abort") }; + int answer = JOptionPane.showOptionDialog(ap, + I18nUtils.localizedStringForKey("dlg_download_errors_todo_msg"), + I18nUtils.localizedStringForKey("dlg_download_errors_todo"), 0, + JOptionPane.QUESTION_MESSAGE, null, answers, answers[0]); + failedMessageAnswered = true; + switch (answer) { + case 0: // Continue + pauseResumeHandler.resume(); + break; + case 1: // Retry + djp.cancel(); + djp = null; + downloadJobDispatcher.cancelOutstandingJobs(); + return false; + case 2: // Skip + downloadJobDispatcher.cancelOutstandingJobs(); + throw new MapDownloadSkippedException(); + default: // Abort or close dialog + downloadJobDispatcher.cancelOutstandingJobs(); + downloadJobDispatcher.terminateAllWorkerThreads(); + throw new InterruptedException(); + } + } + } + djp = null; + log.debug("All download jobs has been completed!"); + if (tileArchive != null) { + tileArchive.writeEndofArchive(); + tileArchive.close(); + tileIndex = tileArchive.getTarIndex(); + if (tileIndex.size() < tileCount && !ap.ignoreDownloadErrors()) { + int missing = tileCount - tileIndex.size(); + log.debug("Expected tile count: " + tileCount + " downloaded tile count: " + tileIndex.size() + + " missing: " + missing); + int answer = JOptionPane.showConfirmDialog(ap, String.format( + I18nUtils.localizedStringForKey("dlg_download_errors_missing_tile_msg"), missing), + I18nUtils.localizedStringForKey("dlg_download_errors_missing_tile"), + JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE); + if (answer != JOptionPane.YES_OPTION) + throw new InterruptedException(); + } + } + downloadJobDispatcher.cancelOutstandingJobs(); + log.debug("Starting to create atlas from downloaded tiles"); + mapTileProvider = new DownloadedTileProvider(tileIndex, map); + } else { + // We don't need to download anything. Everything is already stored locally therefore we can just use it + mapTileProvider = new FilteredMapSourceProvider(map, LoadMethod.DEFAULT); + } + atlasCreator.initializeMap(map, mapTileProvider); + atlasCreator.createMap(); + } catch (Error e) { + log.error("Error in createMap: " + e.getMessage(), e); + throw e; + } finally { + if (tileIndex != null) + tileIndex.closeAndDelete(); + else if (tileArchive != null) + tileArchive.delete(); + } + return true; + } + + public void pauseResumeAtlasCreation() { + if (pauseResumeHandler.isPaused()) { + log.debug("Atlas creation resumed"); + pauseResumeHandler.resume(); + } else { + log.debug("Atlas creation paused"); + pauseResumeHandler.pause(); + } + } + + public boolean isPaused() { + return pauseResumeHandler.isPaused(); + } + + public PauseResumeHandler getPauseResumeHandler() { + return pauseResumeHandler; + } + + /** + * Stop listener from {@link AtlasProgress} + */ + public void abortAtlasCreation() { + try { + DownloadJobProducerThread djp_ = djp; + if (djp_ != null) + djp_.cancel(); + if (downloadJobDispatcher != null) + downloadJobDispatcher.terminateAllWorkerThreads(); + pauseResumeHandler.resume(); + this.interrupt(); + } catch (Exception e) { + log.error("Exception thrown in stopDownload()" + e.getMessage()); + } + } + + public int getActiveDownloads() { + return activeDownloads; + } + + public synchronized void jobStarted() { + activeDownloads++; + } + + public void jobFinishedSuccessfully(int bytesDownloaded) { + synchronized (this) { + ap.incMapDownloadProgress(); + activeDownloads--; + jobsCompleted++; + } + ap.updateGUI(); + } + + public void jobFinishedWithError(boolean retry) { + synchronized (this) { + activeDownloads--; + if (retry) + jobsRetryError++; + else { + jobsPermanentError++; + ap.incMapDownloadProgress(); + } + } + if (!ap.ignoreDownloadErrors()) + Toolkit.getDefaultToolkit().beep(); + ap.setErrorCounter(jobsRetryError, jobsPermanentError); + ap.updateGUI(); + } + + public int getMaxDownloadRetries() { + return maxDownloadRetries; + } + + public AtlasProgress getAtlasProgress() { + return ap; + } + + public File getCustomAtlasDir() { + return customAtlasDir; + } + + public void setCustomAtlasDir(File customAtlasDir) { + this.customAtlasDir = customAtlasDir; + } + + public void setQuitMobacAfterAtlasCreation(boolean quitMobacAfterAtlasCreation) { + this.quitMobacAfterAtlasCreation = quitMobacAfterAtlasCreation; + } + +} diff --git a/src/main/java/mobac/program/DirectoryManager.java b/src/main/java/mobac/program/DirectoryManager.java new file mode 100644 index 0000000..f5a1bfa --- /dev/null +++ b/src/main/java/mobac/program/DirectoryManager.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.JOptionPane; + +import mobac.utilities.Utilities; + +/** + * Provides the common directories used within Mobile Atlas Creator: + *
    + *
  • current directory
  • + *
  • program directory
  • + *
  • user home directory
  • + *
  • user settings directory
  • + *
  • temporary directory
  • + *
+ * + */ +public class DirectoryManager { + + public static final File currentDir; + public static final File programDir; + public static final File userHomeDir; + public static final File tempDir; + public static final File userAppDataDir; + + public static final File userSettingsDir; + public static final File mapSourcesDir; + public static final File toolsDir; + public static final File atlasProfilesDir; + public static final File tileStoreDir; + + private static Properties dirConfig = null; + + static { + currentDir = new File(System.getProperty("user.dir")); + userHomeDir = new File(System.getProperty("user.home")); + programDir = getProgramDir(); + loadDirectoriesIni(); + + userAppDataDir = getUserAppDataDir(); + tempDir = applyDirConfig("mobac.tmpdir", new File(System.getProperty("java.io.tmpdir"))); + + mapSourcesDir = applyDirConfig("mobac.mapsourcesdir", new File(programDir, "mapsources")); + toolsDir = applyDirConfig("mobac.toolsdir", new File(programDir, "tools")); + userSettingsDir = applyDirConfig("mobac.usersettingsdir", programDir); + atlasProfilesDir = applyDirConfig("mobac.atlasprofilesdir", currentDir); + tileStoreDir = applyDirConfig("mobac.tilestoredir", new File(programDir, "tilestore")); + } + + private static File applyDirConfig(String propertyName, File defaultDir) { + if (dirConfig == null) + return defaultDir; + try { + final String dirCfg = dirConfig.getProperty(propertyName); + if (dirCfg == null) { + return defaultDir; + } else { + return expandCommandLine(dirCfg); + } + } catch (Exception e) { + Logging.LOG.error("Error reading directory configuration: " + e.getMessage(), e); + JOptionPane.showMessageDialog(null, "

Failed to load directory.ini - entry \"" + propertyName + + "\":

" + e.getMessage() + "

", "Faile do load directory.ini", + JOptionPane.ERROR_MESSAGE); + return defaultDir; + } + } + + /** + * Modified version of + * http://stackoverflow.com/questions/2090647/evaluation-of-environment-variables-in-command-run- + * by-javas-runtime-exec + * + * @param cmd + * @return + */ + private static File expandCommandLine(final String cmd) { + final Pattern vars = Pattern.compile("[$]\\{(\\S+)\\}"); + final Matcher m = vars.matcher(cmd.trim()); + + final StringBuffer sb = new StringBuffer(cmd.length()); + int lastMatchEnd = 0; + while (m.find()) { + sb.append(cmd.substring(lastMatchEnd, m.start())); + final String envVar = m.group(1); + String envVal = System.getenv(envVar); + if (envVal == null) { + File defPath = null; + + if ("mobac-prog".equalsIgnoreCase(envVar)) + defPath = programDir; + else if ("home".equalsIgnoreCase(envVar)) + defPath = userHomeDir; + else if ("XDG_CONFIG_HOME".equalsIgnoreCase(envVar)) + defPath = new File(userHomeDir, ".config"); + else if ("XDG_CACHE_HOME".equalsIgnoreCase(envVar)) + defPath = new File(userHomeDir, ".cache"); + else if ("XDG_DATA_HOME".equalsIgnoreCase(envVar)) { + File localDataDir = new File(userHomeDir, ".local"); + defPath = new File(localDataDir, "share"); + } + + if (defPath != null) + envVal = defPath.getAbsolutePath(); + } + if (envVal == null) + sb.append(cmd.substring(m.start(), m.end())); + else + sb.append(envVal); + lastMatchEnd = m.end(); + } + sb.append(cmd.substring(lastMatchEnd)); + + return new File(sb.toString()); + } + + public static void initialize() { + if (currentDir == null || userAppDataDir == null || tempDir == null || programDir == null) + throw new RuntimeException("DirectoryManager failed"); + } + + private static void loadDirectoriesIni() { + File dirIniFile = new File(programDir, "directories.ini"); + if (!dirIniFile.isFile()) + return; + dirConfig = new Properties(); + FileInputStream in = null; + try { + in = new FileInputStream(dirIniFile); + dirConfig.load(in); + } catch (IOException e) { + System.err.println("Failed to load " + dirIniFile.getName()); + e.printStackTrace(); + } finally { + Utilities.closeStream(in); + } + } + + /** + * Returns the directory from which this java program is executed + * + * @return + */ + private static File getProgramDir() { + File f = null; + try { + f = Utilities.getClassLocation(DirectoryManager.class); + } catch (Exception e) { + System.err.println(e.getMessage()); + return currentDir; + } + if ("bin".equals(f.getName())) // remove the bin dir -> this usually + // happens only in a development environment + return f.getParentFile(); + else + return f; + } + + /** + * Returns the directory where Mobile Atlas Creator saves it's application settings. + * + * Examples: + *
    + *
  • English Windows XP:
    + * C:\Document and Settings\%username%\Application Data\Mobile Atlas Creator + *
  • Vista:
    + * C:\Users\%username%\Application Data\Mobile Atlas Creator + *
  • Linux:
    + * /home/$username$/.mobac
  • + *
+ * + * @return + */ + private static File getUserAppDataDir() { + String appData = System.getenv("APPDATA"); + if (appData != null) { + File appDataDir = new File(appData); + if (appDataDir.isDirectory()) { + File mobacDataDir = new File(appData, "Mobile Atlas Creator"); + if (mobacDataDir.isDirectory() || mobacDataDir.mkdir()) + return mobacDataDir; + else + throw new RuntimeException("Unable to create directory \"" + mobacDataDir.getAbsolutePath() + "\""); + } + } + File userDir = new File(System.getProperty("user.home")); + File mobacUserDir = new File(userDir, ".mobac"); + if (!mobacUserDir.exists() && !mobacUserDir.mkdir()) + throw new RuntimeException("Unable to create directory \"" + mobacUserDir.getAbsolutePath() + "\""); + return mobacUserDir; + } +} diff --git a/src/main/java/mobac/program/EnvironmentSetup.java b/src/main/java/mobac/program/EnvironmentSetup.java new file mode 100644 index 0000000..f57658c --- /dev/null +++ b/src/main/java/mobac/program/EnvironmentSetup.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Locale; + +import javax.swing.JOptionPane; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; +import mobac.program.model.Atlas; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.Layer; +import mobac.program.model.Profile; +import mobac.program.model.Settings; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.file.FileExtFilter; +import mobac.utilities.file.NamePatternFileFilter; + +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; + +/** + * Creates the necessary files on first time Mobile Atlas Creator is started or tries to update the environment if the + * version has changed. + */ +public class EnvironmentSetup { + + private static boolean FIRST_START = false; + + public static Logger log = Logger.getLogger(EnvironmentSetup.class); + + public static void checkMemory() { + Runtime r = Runtime.getRuntime(); + long maxHeap = r.maxMemory(); + String heapMBFormatted = String.format(Locale.ENGLISH, "%3.2f MiB", maxHeap / 1048576d); + log.info("Total available memory to MOBAC: " + heapMBFormatted); + if (maxHeap < 200000000) { + String msg = String.format(I18nUtils.localizedStringForKey("msg_environment_lack_memory"), heapMBFormatted); + JOptionPane.showMessageDialog(null, msg, + I18nUtils.localizedStringForKey("msg_environment_lack_memory_title"), JOptionPane.WARNING_MESSAGE); + } + } + + public static void upgrade() { + FileFilter ff = new NamePatternFileFilter("tac-profile-.*.xml"); + File profilesDir = DirectoryManager.currentDir; + File[] files = profilesDir.listFiles(ff); + for (File f : files) { + File dest = new File(profilesDir, f.getName().replaceFirst("tac-", "mobac-")); + f.renameTo(dest); + } + } + + /** + * In case the mapsources directory has been moved by configuration (directories.ini or settings.xml) we + * need to copy the existing map packs into the configured directory + */ + public static void copyMapPacks() { + File userMapSourcesDir = Settings.getInstance().getMapSourcesDirectory(); + File progMapSourcesDir = new File(DirectoryManager.programDir, "mapsources"); + if (userMapSourcesDir.equals(progMapSourcesDir)) + return; // no user specific directory configured + if (userMapSourcesDir.isDirectory()) + return; // directory already exists - map packs should have been already copied + try { + Utilities.mkDirs(userMapSourcesDir); + FileUtils.copyDirectory(progMapSourcesDir, userMapSourcesDir, new FileExtFilter(".jar")); + } catch (IOException e) { + log.error(e); + JOptionPane.showMessageDialog(null, + I18nUtils.localizedStringForKey("msg_environment_error_init_mapsrc_dir") + e.getMessage(), + I18nUtils.localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + System.exit(1); + } + } + + /** + * Note: This method has be be called before {@link Settings#loadOrQuit()}. Therefore no localization is available at + * this point. + */ + public static void checkFileSetup() { + checkDirectory(DirectoryManager.userSettingsDir, "user settings", true); + checkDirectory(DirectoryManager.atlasProfilesDir, "atlas profile", true); + checkDirectory(DirectoryManager.tileStoreDir, "tile store", true); + checkDirectory(DirectoryManager.tempDir, "temporary atlas download", true); + if (!Settings.FILE.exists()) { + try { + FIRST_START = true; + Settings.save(); + } catch (Exception e) { + log.error("Error while creating settings.xml: " + e.getMessage(), e); + String[] options = { "Exit", "Show error report" }; + int a = JOptionPane.showOptionDialog(null, "Could not create file settings.xml - program will exit.", + "Error", 0, JOptionPane.ERROR_MESSAGE, null, options, options[0]); + if (a == 1) + GUIExceptionHandler.showExceptionDialog(e); + System.exit(1); + } + } + } + + protected static void checkDirectory(File dir, String dirName, boolean checkIsWriteable) { + try { + Utilities.mkDirs(dir); + } catch (IOException e) { + GUIExceptionHandler.processFatalExceptionSimpleDialog( + String.format(I18nUtils.localizedStringForKey("msg_environment_error_create_dir"), dirName, + dir.getAbsolutePath()), e); + } + if (!checkIsWriteable) + return; + try { + // test if we can write into that directory + File testFile = File.createTempFile("MOBAC", "", dir); + testFile.createNewFile(); + testFile.deleteOnExit(); + testFile.delete(); + } catch (IOException e) { + GUIExceptionHandler.processFatalExceptionSimpleDialog( + String.format(I18nUtils.localizedStringForKey("msg_environment_error_write_file"), dirName, + dir.getAbsolutePath()), e); + } + } + + public static void createDefaultAtlases() { + if (!FIRST_START) + return; + // TODO:MP change sample to Chinese + Profile p = new Profile("Google Maps New York"); + Atlas atlas = Atlas.newInstance(); + try { + EastNorthCoordinate max = new EastNorthCoordinate(40.97264, -74.142609); + EastNorthCoordinate min = new EastNorthCoordinate(40.541982, -73.699036); + Layer layer = new Layer(atlas, "GM New York"); + MapSource ms = MapSourcesManager.getInstance().getSourceByName("Mapnik"); + if (ms == null) + return; + layer.addMapsAutocut("GM New York 16", ms, max, min, 16, null, 32000); + layer.addMapsAutocut("GM New York 14", ms, max, min, 14, null, 32000); + atlas.addLayer(layer); + p.save(atlas); + } catch (Exception e) { + log.error("Creation for example profiles failed", e); + GUIExceptionHandler.showExceptionDialog(e); + } + } +} diff --git a/src/main/java/mobac/program/JobDispatcher.java b/src/main/java/mobac/program/JobDispatcher.java new file mode 100644 index 0000000..fd44019 --- /dev/null +++ b/src/main/java/mobac/program/JobDispatcher.java @@ -0,0 +1,204 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +import java.io.FileNotFoundException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import mobac.exceptions.StopAllDownloadsException; +import mobac.program.interfaces.MapSourceListener; +import mobac.program.tilestore.berkeleydb.DelayedInterruptThread; + +import org.apache.log4j.Logger; + +/** + * Controls the worker threads that are downloading the map tiles in parallel. Additionally the job queue containing the + * unprocessed tile download jobs can be accessed via this class. + */ +public class JobDispatcher { + + private static Logger log = Logger.getLogger(JobDispatcher.class); + + protected int maxJobsInQueue = 100; + protected int minJobsInQueue = 50; + + protected WorkerThread[] workers; + + protected PauseResumeHandler pauseResumeHandler; + + protected MapSourceListener mapSourceListener; + + protected BlockingQueue jobQueue = new LinkedBlockingQueue(); + + public JobDispatcher(int threadCount, PauseResumeHandler pauseResumeHandler, MapSourceListener mapSourceListener) { + this.pauseResumeHandler = pauseResumeHandler; + this.mapSourceListener = mapSourceListener; + workers = new WorkerThread[threadCount]; + for (int i = 0; i < threadCount; i++) + workers[i] = new WorkerThread(i); + } + + @Override + protected void finalize() throws Throwable { + terminateAllWorkerThreads(); + super.finalize(); + } + + public void terminateAllWorkerThreads() { + cancelOutstandingJobs(); + log.trace("Killing all worker threads"); + for (int i = 0; i < workers.length; i++) { + try { + WorkerThread w = workers[i]; + if (w != null) { + w.interrupt(); + } + workers[i] = null; + } catch (Exception e) { + // We don't care about exception here + } + } + } + + public void cancelOutstandingJobs() { + jobQueue.clear(); + } + + /** + * Blocks if more than 100 jobs are already scheduled. + * + * @param job + * @throws InterruptedException + */ + public void addJob(Job job) throws InterruptedException { + while (jobQueue.size() > maxJobsInQueue) { + Thread.sleep(200); + if ((jobQueue.size() < minJobsInQueue) && (maxJobsInQueue < 2000)) { + // System and download connection is very fast - we have to + // increase the maximum job count in the queue + maxJobsInQueue *= 2; + minJobsInQueue *= 2; + } + } + jobQueue.put(job); + } + + /** + * Adds the job to the job-queue and returns. This method will never block! + * + * @param job + */ + public void addErrorJob(Job job) { + try { + jobQueue.put(job); + } catch (InterruptedException e) { + // Can never happen with LinkedBlockingQueue + } + } + + public int getWaitingJobCount() { + return jobQueue.size(); + } + + public static interface Job { + public void run(JobDispatcher dispatcher) throws Exception; + } + + public boolean isAtLeastOneWorkerActive() { + for (int i = 0; i < workers.length; i++) { + WorkerThread w = workers[i]; + if (w != null) { + if ((!w.idle) && (w.getState() != Thread.State.WAITING)) + return true; + } + } + log.debug("All worker threads are idle"); + return false; + } + + /** + * Each worker thread takes the first job from the job queue and executes it. If the queue is empty the worker + * blocks, waiting for the next job. + */ + protected class WorkerThread extends DelayedInterruptThread implements MapSourceListener { + + Job job = null; + + boolean idle = true; + + private Logger log = Logger.getLogger(WorkerThread.class); + + public WorkerThread(int threadNum) { + super(String.format("WorkerThread %02d", threadNum)); + setDaemon(true); + start(); + } + + @Override + public void run() { + try { + executeJobs(); + } catch (InterruptedException e) { + } + log.trace("Thread is terminating"); + } + + protected void executeJobs() throws InterruptedException { + while (!isInterrupted()) { + try { + pauseResumeHandler.pauseWait(); + idle = true; + job = jobQueue.take(); + idle = false; + } catch (InterruptedException e) { + return; + } + if (job == null) + return; + try { + job.run(JobDispatcher.this); + job = null; + } catch (InterruptedException e) { + } catch (StopAllDownloadsException e) { + JobDispatcher.this.terminateAllWorkerThreads(); + JobDispatcher.this.cancelOutstandingJobs(); + log.warn("All downloads has been stoppened: " + e.getMessage()); + return; + } catch (FileNotFoundException e) { + log.error("Download failed: " + e.getMessage()); + } catch (Exception e) { + log.error("Unknown error occured while executing the job: ", e); + } catch (OutOfMemoryError e) { + log.error("", e); + Thread.sleep(5000); + System.gc(); + } + } + } + + public void tileDownloaded(int size) { + mapSourceListener.tileDownloaded(size); + } + + public void tileLoadedFromCache(int size) { + mapSourceListener.tileLoadedFromCache(size); + } + + } + +} diff --git a/src/main/java/mobac/program/Logging.java b/src/main/java/mobac/program/Logging.java new file mode 100644 index 0000000..8d1b577 --- /dev/null +++ b/src/main/java/mobac/program/Logging.java @@ -0,0 +1,220 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +import java.awt.Desktop; +import java.awt.Desktop.Action; +import java.io.File; +import java.io.StringWriter; +import java.util.Enumeration; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.TreeMap; +import java.util.logging.Handler; + +import mobac.StartMOBAC; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.Juli2Log4jHandler; +import mobac.utilities.OSUtilities; + +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.FileAppender; +import org.apache.log4j.Layout; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.SimpleLayout; +import org.apache.log4j.xml.DOMConfigurator; + +public class Logging { + + protected static final String CONFIG_FILENAME = "log4j.xml"; + + protected static final String LOG_FILENAME = "Mobile Atlas Creator.log"; + + protected static File CONFIG_FILE = null; + + public static final Logger LOG = Logger.getLogger("MAC"); + + public static final Layout ADVANCED_LAYOUT = new PatternLayout("%d{ISO8601} %-5p [%t] %c{1}: %m%n"); + + protected static boolean CONFIGURED = false; + + public static void configureLogging() { + // We test for the configuration file, if it exists we use it, otherwise + // we perform simple logging to the console + if (!loadLog4JConfigXml()) { + configureDefaultErrorLogging(); + Logger logger = Logger.getRootLogger(); + logger.info("log4.xml not found - enabling default error log to console"); + } + } + + public static boolean loadLog4JConfigXml() { + if (loadLog4JConfigXml(DirectoryManager.userAppDataDir)) + return true; + if (loadLog4JConfigXml(DirectoryManager.userSettingsDir)) + return true; + if (loadLog4JConfigXml(DirectoryManager.currentDir)) + return true; + if (loadLog4JConfigXml(DirectoryManager.programDir)) + return true; + return false; + } + + public static boolean loadLog4JConfigXml(File directory) { + File f = new File(directory, CONFIG_FILENAME); + if (!f.isFile()) + return false; + try { + DOMConfigurator.configure(f.getAbsolutePath()); + } catch (Exception e) { + System.err.println("Error loading log4j config file \"" + f.getAbsolutePath() + "\""); + return false; + } + Logger logger = Logger.getLogger("LogSystem"); + logger.setLevel(Level.INFO); + logger.info("Logging configured by \"" + f.getAbsolutePath() + "\""); + CONFIGURED = true; + return true; + } + + public static void configureDefaultErrorLogging() { + Logger.getRootLogger().setLevel(Level.INFO); + configureConsoleLogging(Level.TRACE, new SimpleLayout()); + configureLogFileLogging(Level.TRACE); + } + + public static void configureConsoleLogging() { + configureConsoleLogging(Level.ERROR, new SimpleLayout()); + } + + public static void configureConsoleLogging(Level level) { + configureConsoleLogging(level, new SimpleLayout()); + } + + public static void configureConsoleLogging(Level level, Layout layout) { + Logger logger = Logger.getRootLogger(); + ConsoleAppender consoleAppender = new ConsoleAppender(layout); + if (level != null) + consoleAppender.setThreshold(level); + logger.addAppender(consoleAppender); + CONFIGURED = true; + } + + public static void configureLogFileLogging(Level level) { + Logger logger = Logger.getRootLogger(); + File logFileDir = DirectoryManager.userAppDataDir; + String logFilename = new File(logFileDir, LOG_FILENAME).getAbsolutePath(); + Layout layout = new PatternLayout("%d{ISO8601} %-5p [%t] %c{1}: %m%n"); + FileAppender fileAppender; + try { + fileAppender = new FileAppender(layout, logFilename, false); + if (level != null) + fileAppender.setThreshold(level); + logger.addAppender(fileAppender); + } catch (Exception e) { + Logger log = Logger.getLogger("LogSystem"); + log.error("", e); + } + } + + public static void disableLogging() { + Logger logger = Logger.getRootLogger(); + logger.setLevel(Level.OFF); + } + + public static void enableJAXBLogging() { + java.util.logging.Logger logger; + Handler h = new Juli2Log4jHandler(); + logger = java.util.logging.Logger.getLogger("javax.xml.bind"); + logger.setLevel(java.util.logging.Level.ALL); + logger.addHandler(h); + logger = java.util.logging.Logger.getLogger("com.sun.xml.internal.bind"); + logger.setLevel(java.util.logging.Level.ALL); + logger.addHandler(h); + } + + public static void logSystemInfo() { + Logger log = Logger.getLogger("SysInfo"); + if (log.isInfoEnabled()) { + String n = System.getProperty("line.separator"); + log.info("Version: " + ProgramInfo.getCompleteTitle()); + log.info("Platform: " + GUIExceptionHandler.prop("os.name") + " (" + GUIExceptionHandler.prop("os.version") + + ")"); + log.info("Java VM: " + GUIExceptionHandler.prop("java.vm.name") + " (" + + GUIExceptionHandler.prop("java.runtime.version") + ")"); + log.info("Directories:" /**/ + + n + "currentDir: \t\t" + DirectoryManager.currentDir /**/ + + n + "programDir: \t\t" + DirectoryManager.programDir /**/ + + n + "tempDir: \t\t" + DirectoryManager.tempDir /**/ + + n + "userHomeDir: \t\t" + DirectoryManager.userHomeDir /**/ + + n + "userSettingsDir: \t" + DirectoryManager.userSettingsDir /**/ + + n + "atlasProfilesDir: \t" + DirectoryManager.atlasProfilesDir /**/ + + n + "userAppDataDir: \t" + DirectoryManager.userAppDataDir /**/ + ); + log.info("System console available: " + (System.console() != null)); + log.info("Startup arguments (count=" + StartMOBAC.ARGS.length + "):"); + for (int i = 0; i < StartMOBAC.ARGS.length; i++) + log.info("\t" + i + ": " + StartMOBAC.ARGS[i]); + } + if (log.isDebugEnabled()) { + log.debug("Detected operating system: " + OSUtilities.detectOs() + " (" + System.getProperty("os.name") + + ")"); + boolean desktopSupport = Desktop.isDesktopSupported(); + log.debug("Desktop support: " + desktopSupport); + if (desktopSupport) { + Desktop d = Desktop.getDesktop(); + for (Action a : Action.values()) { + log.debug("Desktop action " + a + " supported: " + d.isSupported(a)); + } + } + } + if (log.isTraceEnabled()) { + Properties props = System.getProperties(); + StringWriter sw = new StringWriter(2 << 13); + sw.write("System properties:\n"); + TreeMap sortedProps = new TreeMap(props); + for (Entry entry : sortedProps.entrySet()) { + sw.write(entry.getKey() + " = " + entry.getValue() + "\n"); + } + log.trace(sw.toString()); + } + } + + /** + * returns the first configured {@link FileAppender} or null. + * + * @return + */ + public static String getLogFile() { + Enumeration enu = Logger.getRootLogger().getAllAppenders(); + while (enu.hasMoreElements()) { + Object o = enu.nextElement(); + if (o instanceof FileAppender) { + FileAppender fa = (FileAppender) o; + return fa.getFile(); + } + } + return null; + } + + public static boolean isCONFIGURED() { + return CONFIGURED; + } + +} diff --git a/src/main/java/mobac/program/PauseResumeHandler.java b/src/main/java/mobac/program/PauseResumeHandler.java new file mode 100644 index 0000000..9e63c2b --- /dev/null +++ b/src/main/java/mobac/program/PauseResumeHandler.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +/** + * Central instance that allows to pause/resume multiple threads at once. Used + * in MOBAC for pausing/resuming map tile download and map creation process. + */ +public class PauseResumeHandler { + + protected boolean paused = false; + + public boolean isPaused() { + return paused; + } + + /** + * Enters the pause state. + */ + public void pause() { + paused = true; + } + + /** + * End the pause state and resumes all waiting threads. + */ + public void resume() { + paused = false; + synchronized (this) { + this.notifyAll(); + } + } + + /** + * If {@link #isPaused()}== true this method will not return + * until {@link #resume()} has been called. Otherwise this method returns + * immediately. + * + * @throws InterruptedException + * Thrown if the calling {@link Thread} is interrupted while + * waiting for resume + */ + public void pauseWait() throws InterruptedException { + if (paused) { + synchronized (this) { + if (paused) + this.wait(); + } + } + } + +} diff --git a/src/main/java/mobac/program/ProgramInfo.java b/src/main/java/mobac/program/ProgramInfo.java new file mode 100644 index 0000000..f5d48ab --- /dev/null +++ b/src/main/java/mobac/program/ProgramInfo.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program; + +import java.io.InputStream; +import java.util.Properties; + +import mobac.Main; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.Utilities; + +public class ProgramInfo { + + public static String PROG_NAME = "Mobile Atlas Creator"; + public static String PROG_NAME_SHORT = "MOBAC"; + + private static String VERSION = null; + private static String SVN_REVISION = "unknown"; + private static String userAgent = ""; + + /** + * Show or hide the detailed revision info in the main windows title + */ + private static boolean titleHideRevision = false; + + public static void initialize() { + InputStream propIn = Main.class.getResourceAsStream("mobac.properties"); + try { + Properties props = new Properties(); + props.load(propIn); + VERSION = props.getProperty("mobac.version"); + titleHideRevision = Boolean.parseBoolean(props.getProperty("mobac.revision.hide", "false")); + System.getProperties().putAll(props); + } catch (Exception e) { + String msg = "Error reading mobac.properties"; + GUIExceptionHandler.processFatalExceptionSimpleDialog(msg, e); + } finally { + Utilities.closeStream(propIn); + } + propIn = Main.class.getResourceAsStream("mobac-rev.properties"); + try { + String rev; + if (propIn != null) { + Properties props = new Properties(); + props.load(propIn); + rev = props.getProperty("mobac.revision"); + SVN_REVISION = Integer.toString(Utilities.parseSVNRevision(rev)); + } else { + rev = System.getProperty("mobac.revision.fallback"); + SVN_REVISION = Integer.toString(Utilities.parseSVNRevision(rev)) + " exported"; + } + } catch (Exception e) { + Logging.LOG.error("Error reading mobac-rev.properties", e); + } finally { + Utilities.closeStream(propIn); + } + userAgent = PROG_NAME_SHORT + "/" + (getVersion().replaceAll(" ", "_")); + } + + public static String getVersion() { + if (VERSION != null) + return VERSION; + else + return "UNKNOWN"; + } + + public static String getRevisionStr() { + return SVN_REVISION; + } + + public static String getVersionTitle() { + String title = PROG_NAME; + if (PROG_NAME_SHORT != null) + title += " (" + PROG_NAME_SHORT + ") "; + else + title += " "; + if (VERSION != null) { + title += getVersion(); + } else + title += "unknown version"; + return title; + } + + public static String getCompleteTitle() { + String title = getVersionTitle(); + if (!titleHideRevision) + title += " (" + SVN_REVISION + ")"; + return title; + } + + public static String getUserAgent() { + return userAgent; + } + +} diff --git a/src/main/java/mobac/program/annotations/AtlasCreatorName.java b/src/main/java/mobac/program/annotations/AtlasCreatorName.java new file mode 100644 index 0000000..2ec4684 --- /dev/null +++ b/src/main/java/mobac/program/annotations/AtlasCreatorName.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.model.TileImageParameters.Name; + +/** + * Annotation for {@link AtlasCreator} implementations. The {@link #names()} field holds the parameter names supported + * by the specific atlas format. The full list of available parameters is defined in {@link Name}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AtlasCreatorName { + String value(); + + String type() default ""; +} diff --git a/src/main/java/mobac/program/annotations/SupportedParameters.java b/src/main/java/mobac/program/annotations/SupportedParameters.java new file mode 100644 index 0000000..b2339fa --- /dev/null +++ b/src/main/java/mobac/program/annotations/SupportedParameters.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; + +/** + * Annotation for {@link AtlasCreator} implementations. The {@link #names()} field holds the parameter names supported + * by the specific atlas format. The full list of available parameters is defined in {@link Name}. + */ +@Inherited +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface SupportedParameters { + public TileImageParameters.Name[] names(); +} diff --git a/src/main/java/mobac/program/atlascreators/AFTrack.java b/src/main/java/mobac/program/atlascreators/AFTrack.java new file mode 100644 index 0000000..6300f68 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/AFTrack.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Point; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Collections; + +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ZipStoreOutputStream; + +/** + * AFTrack OSZ Atlas format + */ +@AtlasCreatorName("AFTrack (OSZ)") +public class AFTrack extends OSMTracker { + + private ArrayList zoomLevel = new ArrayList(); + + private int maxZoom; + private Point min; + private Point max; + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + File oszFile = new File(atlasDir, layer.getName() + ".osz"); + mapTileWriter = new OszTileWriter(oszFile); + zoomLevel.clear(); + min = new Point(); + max = new Point(); + maxZoom = -1; + } + + @Override + public void finishLayerCreation() throws IOException { + mapTileWriter.finalizeMap(); + mapTileWriter = null; + + super.finishLayerCreation(); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + zoomLevel.add(new Integer(map.getZoom())); + if (map.getZoom() > maxZoom) { + maxZoom = map.getZoom(); + min.x = map.getMinTileCoordinate().x / 256; + min.y = map.getMinTileCoordinate().y / 256; + max.x = map.getMaxTileCoordinate().x / 256; + max.y = map.getMaxTileCoordinate().y / 256; + } + } + + private class OszTileWriter extends OSMTileWriter { + + ZipStoreOutputStream zipStream; + FileOutputStream out; + + public OszTileWriter(File oszFile) throws FileNotFoundException { + super(); + out = new FileOutputStream(oszFile); + zipStream = new ZipStoreOutputStream(out); + } + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + String entryName = String.format(tileFileNamePattern, zoom, tilex, tiley, tileType); + zipStream.writeStoredEntry(entryName, tileData); + } + + public void finalizeMap() throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(100); + OutputStreamWriter writer = new OutputStreamWriter(bout); + + Collections.sort(zoomLevel); + for (Integer zoom : zoomLevel) + writer.append(String.format("zoom=%d\r\n", zoom.intValue())); + writer.append(String.format("minx=%d\r\n", min.x)); + writer.append(String.format("maxx=%d\r\n", max.x)); + writer.append(String.format("miny=%d\r\n", min.y)); + writer.append(String.format("maxy=%d\r\n", max.y)); + writer.close(); + zipStream.writeStoredEntry("Manifest.txt", bout.toByteArray()); + Utilities.closeStream(zipStream); + } + + } + +} diff --git a/src/main/java/mobac/program/atlascreators/AbstractPlainImage.java b/src/main/java/mobac/program/atlascreators/AbstractPlainImage.java new file mode 100644 index 0000000..e454e65 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/AbstractPlainImage.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.utilities.Utilities; + +public abstract class AbstractPlainImage extends AtlasCreator { + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + } + + @Override + protected void testAtlas() throws AtlasTestException { + Runtime r = Runtime.getRuntime(); + long heapMaxSize = r.maxMemory(); + int maxMapSize = (int) (Math.sqrt(heapMaxSize / 3d) * 0.8); // reduce maximum by 20% + maxMapSize = (maxMapSize / 100) * 100; // round by 100; + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + int w = map.getMaxTileCoordinate().x - map.getMinTileCoordinate().x; + int h = map.getMaxTileCoordinate().y - map.getMinTileCoordinate().y; + if (w > maxMapSize || h > maxMapSize) + throw new AtlasTestException("Map size too large for memory (is: " + Math.max(w, h) + " max: " + + maxMapSize + ")", map); + } + } + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + createImage(); + } catch (InterruptedException e) { + throw e; + } catch (MapCreationException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + /** + * @return maximum image height and width. In case an image is larger it will be scaled to fit. + */ + protected int getMaxImageSize() { + return Integer.MAX_VALUE; + } + + protected int getBufferedImageType() { + return BufferedImage.TYPE_4BYTE_ABGR; + } + + protected void createImage() throws InterruptedException, MapCreationException { + + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + int mapWidth = (xMax - xMin + 1) * tileSize; + int mapHeight = (yMax - yMin + 1) * tileSize; + + int maxImageSize = getMaxImageSize(); + int imageWidth = Math.min(maxImageSize, mapWidth); + int imageHeight = Math.min(maxImageSize, mapHeight); + + int len = Math.max(mapWidth, mapHeight); + double scaleFactor = 1.0; + boolean scaleImage = (len > maxImageSize); + if (scaleImage) { + scaleFactor = (double) getMaxImageSize() / (double) len; + if (mapWidth != mapHeight) { + // Map is not rectangle -> adapt height or width + if (mapWidth > mapHeight) + imageHeight = (int) (scaleFactor * mapHeight); + else + imageWidth = (int) (scaleFactor * mapWidth); + } + } + if (imageHeight < 0 || imageWidth < 0) + throw new MapCreationException("Invalid map size: (width/height: " + imageWidth + "/" + imageHeight + ")", + map); + long imageSize = 3l * ((long) imageWidth) * ((long) imageHeight); + if (imageSize > Integer.MAX_VALUE) + throw new MapCreationException("Map image too large: (width/height: " + imageWidth + "/" + imageHeight + + ") - reduce the map size and try again", map); + BufferedImage tileImage = Utilities.safeCreateBufferedImage(imageWidth, imageHeight, + BufferedImage.TYPE_3BYTE_BGR); + Graphics2D graphics = tileImage.createGraphics(); + try { + if (scaleImage) { + graphics.setTransform(AffineTransform.getScaleInstance(scaleFactor, scaleFactor)); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + int lineY = 0; + for (int y = yMin; y <= yMax; y++) { + int lineX = 0; + for (int x = xMin; x <= xMax; x++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + BufferedImage tile = ImageIO.read(new ByteArrayInputStream(sourceTileData)); + graphics.drawImage(tile, lineX, lineY, tileSize, tileSize, Color.WHITE, null); + } + } catch (IOException e) { + log.error("", e); + } + lineX += tileSize; + } + lineY += tileSize; + } + } finally { + graphics.dispose(); + } + writeTileImage(tileImage); + } + + protected abstract void writeTileImage(BufferedImage tileImage) throws MapCreationException; + +} diff --git a/src/main/java/mobac/program/atlascreators/AlpineQuestMap.java b/src/main/java/mobac/program/atlascreators/AlpineQuestMap.java new file mode 100644 index 0000000..f25c896 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/AlpineQuestMap.java @@ -0,0 +1,399 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.ProgramInfo; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.aqm.FlatPackCreator; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ArrayOutputStream; + +/** + * Creates maps using the AlpineQuestMap atlas format (AQM v2 complient). + * + * AQM format pack tiles in a unique file using the FlatPack format. Supports multi-layers, tile resizing. + * + * @author Camille + */ +@AtlasCreatorName("AlpineQuestMap (AQM)") +@SupportedParameters(names = { Name.format, Name.height, Name.width }) +public class AlpineQuestMap extends AtlasCreator { + + public static final String AQM_VERSION = "2"; + + public static final String AQM_HEADER = "V" + AQM_VERSION + "HEADER"; + public static final String AQM_LEVEL = "V" + AQM_VERSION + "LEVEL"; + public static final String AQM_LEVEL_DELIMITER = "@LEVEL"; + public static final String AQM_END_DELIMITER = "#END"; + + private static final String[] SCALES = new String[] { "1:512 000 000", // 00 + "1:256 000 000", // 01 + "1:128 000 000", // 02 + "1:64 000 000", // 03 + "1:32 000 000", // 04 + "1:16 000 000", // 05 + "1:8 000 000", // 06 + "1:4 000 000", // 07 + "1:2 000 000", // 08 + "1:1 000 000", // 09 + "1:512 000", // 10 + "1:256 000", // 11 + "1:128 000", // 12 + "1:64 000", // 13 + "1:32 000", // 14 + "1:16 000", // 15 + "1:8 000", // 16 + "1:4 000", // 17 + "1:2 000", // 18 + "1:1 000", // 19 + "1:512", // 20 + "1:128", // 21 + "1:64", // 22 + "1:32", // 23 + "1:16", // 24 + "1:8", // 25 + "1:4", // 26 + "1:2", // 27 + "1:1" // 28 + }; + + private FlatPackCreator packCreator = null; + private File filePack = null; + + private double xResizeRatio = 1.0; + private double yResizeRatio = 1.0; + + private int lastZoomLevel = 0; + + @Override + public void initLayerCreation(final LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + + if (layer.getMapCount() > 0) { + // create the file + this.filePack = new File(atlasDir + "/" + layer.getName() + ".AQM"); + this.packCreator = new FlatPackCreator(filePack); + this.lastZoomLevel = -1; + + // add map header + this.addMapHeader(layer.getMap(0).getMapSource().toString(), layer.getName()); + + // add level headers + for (int i = 0; i < layer.getMapCount(); i++) { + // needed to merge splitted maps due to map size (map split not needed by AQM format) + Insets bounds = new Insets(layer.getMap(i).getMinTileCoordinate().y, layer.getMap(i) + .getMinTileCoordinate().x, layer.getMap(i).getMaxTileCoordinate().y, layer.getMap(i) + .getMaxTileCoordinate().x); + + // loops over all maps with the same level and add the bounds + while (((i + 1) < layer.getMapCount()) && (layer.getMap(i).getZoom() == layer.getMap(i + 1).getZoom())) { + i++; + bounds.top = Math.min(bounds.top, layer.getMap(i).getMinTileCoordinate().y); + bounds.left = Math.min(bounds.left, layer.getMap(i).getMinTileCoordinate().x); + bounds.bottom = Math.max(bounds.bottom, layer.getMap(i).getMaxTileCoordinate().y); + bounds.right = Math.max(bounds.right, layer.getMap(i).getMaxTileCoordinate().x); + } + + this.addLevelHeader(layer.getMap(i), bounds); + } + } + } + + @Override + public void finishLayerCreation() throws IOException { + // add end of file delimiter + packCreator.add(new byte[0], AQM_END_DELIMITER); + packCreator.close(); + packCreator = null; + + super.finishLayerCreation(); + } + + @Override + public void abortAtlasCreation() throws IOException { + if (packCreator != null) + packCreator.close(); + packCreator = null; + + if (filePack != null) + Utilities.deleteFile(filePack); + filePack = null; + + super.abortAtlasCreation(); + } + + private final void addMapHeader(final String strID, final String strName) throws IOException { + // version of the AQM format (internal use) + final String strVersion = AQM_VERSION; + + // software used to create the map (displayed to user) + final String strSoftware = ProgramInfo.getCompleteTitle(); + + // date of creation (displayed to user) + final String strDate = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); + + // name of the person that created the map (displayed to user) + final String strCreator = ""; + + StringWriter w = new StringWriter(); + w.write("[map]\n"); + w.write("id = " + strID + "\n"); + w.write("name = " + strName + "\n"); + w.write("version = " + strVersion + "\n"); + w.write("date = " + strDate + "\n"); + w.write("creator = " + strCreator + "\n"); + w.write("software = " + strSoftware + "\n"); + w.write("\n"); + w.flush(); + w.close(); + + // add the metadata file into map + packCreator.add(w.getBuffer().toString().getBytes(), AQM_HEADER); + } + + private final void addLevelHeader(final MapInterface map, final Insets bounds) throws IOException { + final int tileSize = map.getMapSource().getMapSpace().getTileSize(); + final int xMin = bounds.left / tileSize; + final int xMax = bounds.right / tileSize; + final int yMin = bounds.top / tileSize; + final int yMax = bounds.bottom / tileSize; + + // unique identifier for a data source / zoom (internal use) + final String strID = new DecimalFormat("00").format(map.getZoom()); + + // name of this specific map (displayed to user) + String strName = map.getLayer().getName(); + if (strName == null || strName.length() == 0) + strName = I18nUtils.localizedStringForKey("Unnamed"); + + // scale of the map (displayed to user) + String strScale = ""; + if (map.getZoom() >= 0 && map.getZoom() < SCALES.length) + strScale = SCALES[map.getZoom()]; + + // source of the map data (displayed to user) + final String strDataSource = map.getMapSource().toString(); + + // copyright of map data (displayed to user) + final String strCopyright = map.getMapSource().toString(); + + // projection of tiles (internal use) + final String strProjection = "mercator"; + + String strGeoid = ""; + if (ProjectionCategory.SPHERE.equals(map.getMapSource().getMapSpace().getProjectionCategory())) + strGeoid = "sphere"; + else if (ProjectionCategory.ELLIPSOID.equals(map.getMapSource().getMapSpace().getProjectionCategory())) + strGeoid = "wgs84"; + + // number of tiles (internal use) + final long nbTotalTiles = (256 * Math.round(Math.pow(2, map.getZoom()))) / tileSize; + + // check resize or resample parameters + String strImageFormat = null; + Dimension tilesSize = null; + + if (map.getParameters() != null) { + strImageFormat = map.getParameters().getFormat().getFileExt(); + tilesSize = map.getParameters().getDimension(); + } else { + strImageFormat = map.getMapSource().getTileImageType().getFileExt(); + tilesSize = map.getTileSize(); + } + + if (strImageFormat != null) + strImageFormat = strImageFormat.toUpperCase(); + + // write metadata + StringWriter w = new StringWriter(); + w.write("[level]\n"); + w.write("id = " + strID + "\n"); + w.write("name = " + strName + "\n"); + w.write("scale = " + strScale + "\n"); + w.write("datasource = " + strDataSource + "\n"); + w.write("copyright = " + strCopyright + "\n"); + w.write("projection = " + strProjection + "\n"); + w.write("geoid = " + strGeoid + "\n"); + w.write("xtsize = " + (int) tilesSize.getWidth() + "\n"); + w.write("ytsize = " + (int) tilesSize.getHeight() + "\n"); + w.write("xtratio = " + (nbTotalTiles / 360.0) + "\n"); + w.write("ytratio = " + (nbTotalTiles / 360.0) + "\n"); + w.write("xtoffset = " + (nbTotalTiles / 2.0) + "\n"); + w.write("ytoffset = " + (nbTotalTiles / 2.0) + "\n"); + w.write("xtmin = " + xMin + "\n"); + w.write("xtmax = " + xMax + "\n"); + w.write("ytmin = " + (nbTotalTiles - yMax) + "\n"); + w.write("ytmax = " + (nbTotalTiles - yMin) + "\n"); + w.write("background = " + "#FFFFFF" + "\n"); + w.write("imgformat = " + strImageFormat + "\n"); + w.write("\n"); + w.flush(); + w.close(); + + // add the metadata file into map + packCreator.add(w.getBuffer().toString().getBytes(), AQM_LEVEL); + } + + @Override + public boolean testMapSource(final MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace) + && (ProjectionCategory.SPHERE.equals(mapSource.getMapSpace().getProjectionCategory()) || ProjectionCategory.ELLIPSOID + .equals(mapSource.getMapSpace().getProjectionCategory())); + } + + @Override + public void initializeMap(final MapInterface map, final TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + + xResizeRatio = 1.0; + yResizeRatio = 1.0; + + if (parameters != null) { + int mapTileSize = map.getMapSource().getMapSpace().getTileSize(); + if ((parameters.getWidth() != mapTileSize) || (parameters.getHeight() != mapTileSize)) { + // handle image re-sampling + image re-sizing + xResizeRatio = (double) parameters.getWidth() / (double) mapTileSize; + yResizeRatio = (double) parameters.getHeight() / (double) mapTileSize; + } else { + // handle only image re-sampling + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + } + } + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + if (map.getZoom() > lastZoomLevel) { + // metadata information at the beginning + this.addLevelDelimiter(); + this.lastZoomLevel = map.getZoom(); + } + + // add tiles + this.addLevelTiles(); + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + private final void addLevelDelimiter() throws IOException { + // add empty level delimiter file + packCreator.add(new byte[0], AQM_LEVEL_DELIMITER); + } + + private final void addLevelTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + + // number of tiles for this zoom level + final long nbTotalTiles = (256 * Math.round(Math.pow(2, map.getZoom()))) / tileSize; + + // tile resizing + BufferedImage tileImage = null; + Graphics2D graphics = null; + ArrayOutputStream buffer = null; + TileImageDataWriter writer = null; + + if ((parameters != null) || (xResizeRatio != 1.0) || (yResizeRatio != 1.0)) { + // resize image + tileImage = new BufferedImage(parameters.getWidth(), parameters.getHeight(), BufferedImage.TYPE_3BYTE_BGR); + + // associated graphics with affine transform + graphics = tileImage.createGraphics(); + graphics.setTransform(AffineTransform.getScaleInstance(xResizeRatio, yResizeRatio)); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + // image compression writer + writer = parameters.getFormat().getDataWriter(); + + // buffer to store compressed image + buffer = new ArrayOutputStream(3 * parameters.getWidth() * parameters.getHeight()); + + ImageIO.setUseCache(false); + writer.initialize(); + } + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + + atlasProgress.incMapCreationProgress(); + + try { + // retrieve the tile data (already re-sampled if needed) + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + + if (sourceTileData != null) { + // there is some data + if ((graphics != null) && (buffer != null) && (writer != null)) { + // need to resize the tile + final BufferedImage tile = ImageIO.read(new ByteArrayInputStream(sourceTileData)); + graphics.drawImage(tile, 0, 0, null); + + buffer.reset(); + + writer.processImage(tileImage, buffer); + + sourceTileData = buffer.toByteArray(); + + if (sourceTileData == null) + throw new MapCreationException("Image resizing failed.", map); + } + + packCreator.add(sourceTileData, "" + x + "_" + (nbTotalTiles - y)); // y tiles count began by + // bottom in AQM + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/AndNav.java b/src/main/java/mobac/program/atlascreators/AndNav.java new file mode 100644 index 0000000..2517c0c --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/AndNav.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.program.annotations.AtlasCreatorName; + + +/** + * Creates maps using the AndNav atlas format. + * + * Please note that this atlas format ignores the defined atlas structure. It + * uses a separate directory for each used map source and inside one directory + * for each zoom level. + */ +@AtlasCreatorName("AndNav atlas format") +public class AndNav extends OSMTracker { + + public AndNav() { + super(); + tileFileNamePattern += ".andnav"; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/AtlasCreator.java b/src/main/java/mobac/program/atlascreators/AtlasCreator.java new file mode 100644 index 0000000..2c26d48 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/AtlasCreator.java @@ -0,0 +1,268 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.EnumSet; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.gui.AtlasProgress; +import mobac.program.AtlasThread; +import mobac.program.PauseResumeHandler; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.AtlasOutputFormat; +import mobac.program.model.Settings; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageParameters; +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +/** + * Abstract base class for all AtlasCreator implementations. + * + * The general call schema is as follows: + *
    + *
  1. AtlasCreator instantiation via {@link AtlasOutputFormat#createAtlasCreatorInstance()}
  2. + *
  3. AtlasCreator atlas initialization via {@link #startAtlasCreation(AtlasInterface, File)}
  4. + *
  5. 1 to n times {@link #initializeMap(MapInterface, TileProvider)} followed by {@link #createMap()}
  6. + *
  7. AtlasCreator atlas finalization via {@link #finishAtlasCreation()}
  8. + *
+ */ +public abstract class AtlasCreator { + + public static final Charset TEXT_FILE_CHARSET = Charsets.ISO_8859_1; + + protected final Logger log; + + /************************************************************/ + /** atlas specific fields **/ + /************************************************************/ + + protected AtlasInterface atlas; + + protected File atlasDir; + + protected AtlasProgress atlasProgress = null; + + protected PauseResumeHandler pauseResumeHandler = null; + + /************************************************************/ + /** map specific fields **/ + /************************************************************/ + + protected MapInterface map; + protected int xMin; + protected int xMax; + protected int yMin; + protected int yMax; + protected int zoom; + protected MapSource mapSource; + protected int tileSize; + + /** + * Custom tile processing parameters. null if disabled in GUI + */ + protected TileImageParameters parameters; + + protected AtlasOutputFormat atlasOutputFormat; + + protected TileProvider mapDlTileProvider; + + private boolean aborted = false; + + /** + * Default constructor - initializes the logging environment + */ + protected AtlasCreator() { + log = Logger.getLogger(this.getClass()); + }; + + /** + * @param customAtlasDir + * if not null the customAtlasDir is used instead of the generated atlas directory name + * @throws InterruptedException + * @see AtlasCreator + */ + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws AtlasTestException, IOException, + InterruptedException { + this.atlas = atlas; + testAtlas(); + + if (customAtlasDir == null) { + // No explicit atlas output directory has been set - generate a new directory name + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss"); + String atlasDirName = atlas.getName() + "_" + sdf.format(new Date()); + File atlasOutputDir = Settings.getInstance().getAtlasOutputDirectory(); + atlasDir = new File(atlasOutputDir, atlasDirName); + } else + atlasDir = customAtlasDir; + Utilities.mkDirs(atlasDir); + } + + protected void testAtlas() throws AtlasTestException { + } + + public void initLayerCreation(LayerInterface layer) throws IOException { + + } + + public void finishLayerCreation() throws IOException { + + } + + /** + * @throws InterruptedException + * @see AtlasCreator + */ + public void finishAtlasCreation() throws IOException, InterruptedException { + } + + public void abortAtlasCreation() throws IOException { + this.aborted = true; + } + + public boolean isAborted() { + return aborted; + } + + /** + * Test if the {@link AtlasCreator} instance supports the selected {@link MapSource} + * + * @param mapSource + * @return true if supported otherwise false + * @see AtlasCreator + */ + public abstract boolean testMapSource(MapSource mapSource); + + /** + * @see AtlasCreator + */ + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + LayerInterface layer = map.getLayer(); + if (mapTileProvider == null) + throw new NullPointerException(); + this.mapDlTileProvider = mapTileProvider; + this.map = map; + this.mapSource = map.getMapSource(); + this.tileSize = mapSource.getMapSpace().getTileSize(); + this.parameters = map.getParameters(); + xMin = map.getMinTileCoordinate().x / tileSize; + xMax = map.getMaxTileCoordinate().x / tileSize; + yMin = map.getMinTileCoordinate().y / tileSize; + yMax = map.getMaxTileCoordinate().y / tileSize; + this.zoom = map.getZoom(); + this.atlasOutputFormat = layer.getAtlas().getOutputFormat(); + + Thread t = Thread.currentThread(); + if (!(t instanceof AtlasThread)) + throw new RuntimeException("Calling thread must be AtlasThread!"); + AtlasThread at = (AtlasThread) t; + atlasProgress = at.getAtlasProgress(); + pauseResumeHandler = at.getPauseResumeHandler(); + } + + /** + * @throws InterruptedException + * @see AtlasCreator + */ + public abstract void createMap() throws MapCreationException, InterruptedException; + + /** + * Checks if the user has aborted atlas creation and if true an {@link InterruptedException} is thrown. + * + * @throws InterruptedException + */ + public void checkUserAbort() throws InterruptedException { + if (Thread.currentThread().isInterrupted()) + throw new InterruptedException(); + pauseResumeHandler.pauseWait(); + } + + public AtlasProgress getAtlasProgress() { + return atlasProgress; + } + + public int getXMin() { + return xMin; + } + + public int getXMax() { + return xMax; + } + + public int getYMin() { + return yMin; + } + + public int getYMax() { + return yMax; + } + + public MapInterface getMap() { + return map; + } + + public TileImageParameters getParameters() { + return parameters; + } + + public TileProvider getMapDlTileProvider() { + return mapDlTileProvider; + } + + /** + * Tests all maps of the currently active atlas if a custom tile image format has been specified and if the + * specified format is equal to the allowedFormat. + * + * @param allowedFormat + * @throws AtlasTestException + */ + protected void performTest_AtlasTileFormat(EnumSet allowedFormats) throws AtlasTestException { + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + TileImageParameters parameters = map.getParameters(); + if (parameters == null) + continue; + if (!allowedFormats.contains(parameters.getFormat())) + throw new AtlasTestException( + "Selected custom tile format not supported - only the following format(s) are supported: " + + allowedFormats, map); + } + } + } + + protected void performTest_MaxMapZoom(int maxZoom) throws AtlasTestException { + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + if (map.getZoom() > maxZoom) + throw new AtlasTestException("Maximum zoom is " + maxZoom + " for this atlas format", map); + } + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/BackCountryNavigator.java b/src/main/java/mobac/program/atlascreators/BackCountryNavigator.java new file mode 100644 index 0000000..1517f89 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/BackCountryNavigator.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.program.annotations.AtlasCreatorName; + +/** + * Creates maps using the BCNav SQLite atlas format. + * + * + * @see http://www.backcountrynavigator.com + */ +@AtlasCreatorName("BackCountry Navigator (SQLite)") +public class BackCountryNavigator extends RMapsSQLite { + + public BackCountryNavigator() { + super(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/BigPlanetTracks.java b/src/main/java/mobac/program/atlascreators/BigPlanetTracks.java new file mode 100644 index 0000000..8083ff5 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/BigPlanetTracks.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.interfaces.MapSource; + +/** + * Atlas/Map creator for "BigPlanet-Maps application for Android" (offline SQLite maps) + * http://code.google.com/p/bigplanet/ + *

+ * Some source parts are taken from the "android-map.blogspot.com Version of Mobile Atlas Creator": + * http://code.google.com/p/android-map/ + *

+ */ +@AtlasCreatorName(value = "Big Planet Tracks SQLite", type = "BigPlanet") +public class BigPlanetTracks extends RMapsSQLite { + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/CacheBox.java b/src/main/java/mobac/program/atlascreators/CacheBox.java new file mode 100644 index 0000000..9e9b5d8 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/CacheBox.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; + +@AtlasCreatorName("CacheBox (PACK)") +@SupportedParameters(names = { Name.format }) +public class CacheBox extends AtlasCreator { + + private File packFile = null; + private RandomAccessFile packRaFile = null; + private MapInfo[] mapInfos; + + private int nextMapOffsetIndex = 0; + private MapInfo activeMapInfo; + + @Override + protected void testAtlas() throws AtlasTestException { + for (LayerInterface layer : atlas) { + if (layer.getMapCount() == 0) + throw new AtlasTestException("Empty layers are not allowed", layer); + Class mapSourceClass = layer.getMap(0).getMapSource().getClass(); + for (MapInterface map : layer) { + if (!mapSourceClass.equals(map.getMapSource().getClass())) + throw new AtlasTestException( + "Different map sources are not allowed within one layer", map); + } + } + } + + @Override + public void finishAtlasCreation() throws IOException { + } + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + nextMapOffsetIndex = 0; + packFile = new File(atlasDir, layer.getName() + ".pack"); + if (packFile.exists()) + Utilities.deleteFile(packFile); + packRaFile = new RandomAccessFile(packFile, "rw"); + /* + * We use the mapsource name as layer name. See feature request #2987674 + * for details. + */ + writeString(layer.getMap(0).getMapSource().getName(), 32); // layer + // name + writeString(layer.getName(), 128); // layer friendly name + writeString("", 256); // layer url - unused + writeLong(0); // int64 ticks + int mapCount = layer.getMapCount(); + writeInt(mapCount); // int32 number of bounding boxes / maps + + long offset = 32 + 128 + 256 + 8 + 4 + 8; // = 436 + offset += mapCount * 28; + mapInfos = new MapInfo[mapCount + 1]; + + int i = 0; + for (MapInterface map : layer) { + // For each map: + + int minX = map.getMinTileCoordinate().x / 256; + int minY = map.getMinTileCoordinate().y / 256; + int maxX = map.getMaxTileCoordinate().x / 256; + int maxY = map.getMaxTileCoordinate().y / 256; + int tilesInMap = (maxX - minX + 1) * (maxY - minY + 1); + + writeInt(map.getZoom()); // int32 zoom + writeInt(minX); // int32 minX + writeInt(maxX); // int32 maxX + writeInt(minY); // int32 minY + writeInt(maxY); // int32 maxY + + writeLong(offset); // int64 offset to mapIndexTable + mapInfos[i++] = new MapInfo(map, offset, tilesInMap, minX, minY, maxX, maxY); + log.trace(String.format("Offset to index table [%d]: 0x%X", i, offset)); + + offset += tilesInMap * 8; + } + // We need to keep the offset to the last index table + // -> required for index table finalization. + mapInfos[i] = new MapInfo(null, offset, 0, 0, 0, 0, 0); + log.trace(String.format("End of bounding boxes table: 0x%X", packRaFile.getFilePointer())); + packRaFile.seek(offset); + log.trace(String.format("Start of tile data: 0x%X", packRaFile.getFilePointer())); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + TileImageParameters param = map.getParameters(); + if (param != null) + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, param.getFormat()); + activeMapInfo = mapInfos[nextMapOffsetIndex++]; + if (!activeMapInfo.map.equals(map)) + throw new RuntimeException("Map does not match offset info!"); + // Just to make sure we use the xy values from mapInfo + xMin = activeMapInfo.minX; + xMax = activeMapInfo.maxX; + yMin = activeMapInfo.minY; + yMax = activeMapInfo.maxY; + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + + ImageIO.setUseCache(false); + + int offsetIndex = 0; + long[] offsets = new long[activeMapInfo.tileCount]; + + try { + for (int y = yMin; y <= yMax; y++) { + for (int x = xMin; x <= xMax; x++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + offsets[offsetIndex++] = packRaFile.getFilePointer(); + if (sourceTileData != null) { + packRaFile.write(sourceTileData); + } + } + } + long pos = packRaFile.getFilePointer(); + // Write the offsets of all tiles in this map to the correspondent + // offset index table + // Due to a bug in CacheBox we have to subtract 8 from the offset + packRaFile.seek(activeMapInfo.indexTableOffset - 8); + for (long tileoffset : offsets) + writeLong(tileoffset); + packRaFile.seek(pos); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + } + + @Override + public void finishLayerCreation() throws IOException { + long tableOffset = mapInfos[mapInfos.length - 1].indexTableOffset; + long offset = packRaFile.getFilePointer(); + // Due to a bug in CacheBox we have to subtract 8 from the offset + packRaFile.seek(tableOffset - 8); + // write the offset to the end of the file (after the last image) + // required by CacheBox for length calculation of the last tile + packRaFile.writeLong(swapLong(offset)); + mapInfos = null; + packFile = null; + packRaFile.close(); + packRaFile = null; + } + + @Override + public void abortAtlasCreation() throws IOException { + mapInfos = null; + Utilities.closeFile(packRaFile); + packRaFile = null; + if (packFile != null) + Utilities.deleteFile(packFile); + packFile = null; + } + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + private void writeString(String text, int length) throws IOException { + byte[] buf = new byte[length]; + byte[] asciiBytes = text.getBytes("ASCII"); + System.arraycopy(asciiBytes, 0, buf, 0, Math.min(length, asciiBytes.length)); + for (int i = asciiBytes.length; i < length; i++) + buf[i] = ' '; + packRaFile.write(buf); + } + + private void writeInt(int v) throws IOException { + packRaFile.writeInt(swapInt(v)); + } + + private void writeLong(long v) throws IOException { + packRaFile.writeLong(swapLong(v)); + } + + public final static int swapInt(int v) { + return (v >>> 24) | (v << 24) | ((v << 8) & 0x00FF0000) | ((v >> 8) & 0x0000FF00); + } + + public final static long swapLong(long v) { + long b1 = (v >> 0) & 0xff; + long b2 = (v >> 8) & 0xff; + long b3 = (v >> 16) & 0xff; + long b4 = (v >> 24) & 0xff; + long b5 = (v >> 32) & 0xff; + long b6 = (v >> 40) & 0xff; + long b7 = (v >> 48) & 0xff; + long b8 = (v >> 56) & 0xff; + + return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0; + } + + private class MapInfo { + + final MapInterface map; + final long indexTableOffset; + final int tileCount; + + final int minX; + final int minY; + final int maxX; + final int maxY; + + public MapInfo(MapInterface map, long indexOffset, int tileCount, int minX, int minY, + int maxX, int maxY) { + super(); + this.map = map; + this.indexTableOffset = indexOffset; + this.tileCount = tileCount; + this.minX = minX; + this.maxX = maxX; + this.minY = minY; + this.maxY = maxY; + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/CacheWolf.java b/src/main/java/mobac/program/atlascreators/CacheWolf.java new file mode 100644 index 0000000..372b29d --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/CacheWolf.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileBuilder; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.CacheTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; + +@AtlasCreatorName("CacheWolf (WFL)") +@SupportedParameters(names = { Name.format, Name.height, Name.width }) +public class CacheWolf extends Ozi { + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDirs(layerDir); + } catch (IOException e1) { + throw new MapCreationException(map, e1); + } + if (parameters == null) { + // One image per map + super.createTiles(); + writeWflFile(); + } else + // Use automatic tiling as specified in the parameters + createTiles(); + } + + @Override + protected void createTiles() throws InterruptedException, MapCreationException { + MapTileWriter mapTileWriter; + + CacheTileProvider ctp = new CacheTileProvider(mapDlTileProvider); + try { + mapDlTileProvider = ctp; + mapTileWriter = new CWFileTileWriter(); + MapTileBuilder mapTileBuilder = new MapTileBuilder(this, mapTileWriter, true); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + ctp.cleanup(); + } + } + + private void writeWflFile() throws MapCreationException { + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + try { + writeWflFile(mapName, 0, 0, width, height); + } catch (IOException e) { + throw new MapCreationException("Error writing wfl file: " + e.getMessage(), map, e); + } + } + + private void writeWflFile(String filename, int tilex, int tiley, int width, int height) throws IOException { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, filename + ".wfl")); + OutputStreamWriter mapWriter = new OutputStreamWriter(fout, TEXT_FILE_CHARSET); + + MapSpace mapSpace = mapSource.getMapSpace(); + + int xStart = xMin * tileSize; + int yStart = yMin * tileSize; + + if (parameters != null) { + xStart += tilex * parameters.getWidth(); + yStart += tiley * parameters.getHeight(); + } + + double topLeftLon = mapSpace.cXToLon(xStart, zoom); + double topLeftLat = mapSpace.cYToLat(yStart, zoom); + + double bottomRightLon = mapSpace.cXToLon(xStart + width, zoom); + double bottomRightLat = mapSpace.cYToLat(yStart + height, zoom); + + double[] affine = { 0, 0, 0, 0 }; + + // Mobile Atlas Creator does only output maps with north at top + // (no rotation). Therefore we should be able to simplify the affine + // calculation process: + affine[1] = (bottomRightLon - topLeftLon) / width; + affine[2] = (bottomRightLat - topLeftLat) / height; + + for (double d : affine) + mapWriter.write(Double.toString(d) + "\n"); + + mapWriter.write(Double.toString(topLeftLat) + "\n"); + mapWriter.write(Double.toString(topLeftLon) + "\n"); + mapWriter.write(Double.toString(bottomRightLat) + "\n"); + mapWriter.write(Double.toString(bottomRightLon) + "\n"); + + mapWriter.flush(); + mapWriter.close(); + } finally { + Utilities.closeStream(fout); + } + } + + public class CWFileTileWriter implements MapTileWriter { + + public CWFileTileWriter() throws IOException { + super(); + log.debug("Writing tiles to set folder: " + layerDir); + } + + public void writeTile(int tilex, int tiley, String imageFormat, byte[] tileData) throws IOException { + String tileFileName = String.format("%s_%dx%d", mapName, tilex, tiley); + File f = new File(layerDir, tileFileName + '.' + imageFormat); + FileOutputStream out = new FileOutputStream(f); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + writeWflFile(tileFileName, tilex, tiley, parameters.getWidth(), parameters.getHeight()); + } + + public void finalizeMap() { + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/GCLive.java b/src/main/java/mobac/program/atlascreators/GCLive.java new file mode 100644 index 0000000..5eaa5f1 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/GCLive.java @@ -0,0 +1,311 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Collections; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; + +/** + * File format documentation + * + *
+ * ===
+ * === Reengineering of the Geocaching Live Tile Database ===
+ * ===
+ * 
+ * index file:
+ * ===========
+ * 
+ * 16 Bytes Header:
+ * ----------------
+ * 
+ * 00 00 00 0A		Highest index used currently for the data files. Index is incremented starting with 0.
+ * 00 00 4E 20		Max. number of tiles index. Current max. number is 20000.
+ * 00 00 01 4D		Number of tile entries (16 bytes) indexed.
+ * 00 01 0C FF		Size of the data file with the currently used highest index.
+ * 
+ * 16 Bytes per tile:
+ * ------------------
+ * 
+ * 00 00			Inverse zoom level = 17 - Z. 
+ * 01 0E 16			Number of the X tile.
+ * 00 B1 0E			Number of the Y tile.
+ * 00 00 50 92		Start offset of the PNG data in the data file.
+ * 00 9D 6 			Size of the PNG data.
+ *        0 01		Index of the data file which contains the PNG data.
+ * 
+ * The tiles are sorted starting by zoom level 17 (INVZ = 0).
+ * 
+ * data file:
+ * ----------
+ * 
+ * Max. 32 PNGs concated directly together.
+ * 
+ * 
+ * [0] - [x] directories:
+ * ----------------------
+ * 
+ * Max. 32 data files per directory. The data files are named from 'data0' to 'dataN'.
+ * 
+ */ +@AtlasCreatorName("Geocaching Live offline map") +@SupportedParameters(names = { Name.format }) +public class GCLive extends AtlasCreator { + + private static final int MAX_TILES = 65535; + + private MapTileWriter mapTileWriter = null; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + protected void testAtlas() throws AtlasTestException { + long tileCount = 0; + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + // We can not use map.calculateTilesToDownload() because be need the full tile count not the sparse + int tileSize_t = 256; // Everything else is not allowed + int xMin_t = map.getMinTileCoordinate().x / tileSize_t; + int xMax_t = map.getMaxTileCoordinate().x / tileSize_t; + int yMin_t = map.getMinTileCoordinate().y / tileSize_t; + int yMax_t = map.getMaxTileCoordinate().y / tileSize_t; + tileCount += (xMax_t - xMin_t + 1) * (yMax_t - yMin_t + 1); + } + // Check for max tile count <= 65535 + if (tileCount > MAX_TILES) + throw new AtlasTestException("Tile count too high in layer " + layer.getName() + + "\n - please select smaller/fewer areas"); + } + } + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + mapTileWriter = new GCLiveWriter(new File(atlasDir, layer.getName())); + } + + @Override + public void finishLayerCreation() throws IOException { + mapTileWriter.finalizeMap(); + mapTileWriter = null; + super.finishLayerCreation(); + } + + @Override + public void abortAtlasCreation() throws IOException { + mapTileWriter.finalizeMap(); + mapTileWriter = null; + super.abortAtlasCreation(); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + if (parameters != null) { + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + } + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + //byte[] emptyTileData = Utilities.createEmptyTileData(mapSource); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) + mapTileWriter.writeTile(x, y, null, sourceTileData); + // else + // mapTileWriter.writeTile(x, y, null, emptyTileData); + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + + protected class GCLiveWriter implements MapTileWriter { + + private File mapDir; + + private int dataDirCounter = 0; + private int dataFileCounter = 0; + private int imageCounter = 0; + + private RandomAccessFile currentDataFile; + + private ArrayList headerEntries; + + public GCLiveWriter(File mapDir) throws IOException { + super(); + this.mapDir = mapDir; + Utilities.mkDir(mapDir); + headerEntries = new ArrayList(MAX_TILES); + prepareDataFile(); + } + + private void prepareDataFile() throws IOException { + if (currentDataFile != null) + Utilities.closeFile(currentDataFile); + currentDataFile = null; + File dataDir = new File(mapDir, Integer.toString(dataDirCounter)); + Utilities.mkDir(dataDir); + File dataFile = new File(dataDir, "data" + Integer.toString(dataFileCounter)); + currentDataFile = new RandomAccessFile(dataFile, "rw"); + imageCounter = 0; + } + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + imageCounter++; + if (imageCounter >= 32) { + dataFileCounter++; + if (dataFileCounter % 32 == 0) { + dataDirCounter++; + if (dataDirCounter >= 32) + throw new RuntimeException("Maximum number of tiles exceeded"); + } + prepareDataFile(); + } + long offset = currentDataFile.getFilePointer(); + currentDataFile.write(tileData); + int len = tileData.length; + + GCHeaderEntry header = new GCHeaderEntry(zoom, tilex, tiley, dataFileCounter, (int) offset, len); + headerEntries.add(header); + } + + public void finalizeMap() throws IOException { + int dataPos = (int) currentDataFile.getFilePointer(); + Utilities.closeFile(currentDataFile); + Collections.sort(headerEntries); + + RandomAccessFile indexFile; + indexFile = new RandomAccessFile(new File(mapDir, "index"), "rw"); + + // Write index header (first 16 bytes) + indexFile.seek(0); + int dataFileIndex = dataDirCounter * 32 + dataFileCounter; + indexFile.writeInt(dataFileIndex); // Highest index used currently for the data files. Index is incremented + // starting with 0. + indexFile.writeInt(headerEntries.size()); // Max. number of tiles index. Current max. number is 20000. + indexFile.writeInt(headerEntries.size()); // Number of tile entries (16 bytes) indexed. + indexFile.writeInt(dataPos); // Size of the data file with the currently used highest index. + + for (GCHeaderEntry entry : headerEntries) { + entry.writeHeader(indexFile); + System.out.println(entry); + } + headerEntries = null; + Utilities.closeFile(indexFile); + } + } + + public static class GCHeaderEntry implements Comparable { + public final int zoom; + public final int tilex; + public final int tiley; + public final int dataFileIndex; + public final int offset; + public final int len; + + public GCHeaderEntry(int zoom, int tilex, int tiley, int dataFileIndex, int offset, int len) { + super(); + this.zoom = zoom; + this.tilex = tilex; + this.tiley = tiley; + this.dataFileIndex = dataFileIndex; + this.offset = offset; + this.len = len; + } + + public void writeHeader(RandomAccessFile file) throws IOException { + file.writeShort((short) (17 - zoom)); + file.write((tilex >> 16) & 0xFF); + file.write((tilex >> 8) & 0xFF); + file.write(tilex & 0xFF); + file.write((tiley >> 16) & 0xFF); + file.write((tiley >> 8) & 0xFF); + file.write(tiley & 0xFF); + file.writeInt(offset); + + int tmp = (len << 4); + tmp = tmp | ((dataFileIndex >> 8) & 0x0F); + + file.write((tmp >> 16) & 0xFF); + file.write((tmp >> 8) & 0xFF); + file.write(tmp & 0xFF); + file.write(dataFileIndex & 0xFF); + } + + public int compareTo(GCHeaderEntry o) { + if (zoom > o.zoom) + return -1; + if (zoom < o.zoom) + return 1; + if (tilex > o.tilex) + return 1; + if (tilex < o.tilex) + return -1; + if (tiley > o.tiley) + return 1; + if (tiley < o.tiley) + return -1; + return 0; + } + + @Override + public String toString() { + return "GCHeaderEntry [zoom=" + zoom + ", tilex=" + tilex + ", tiley=" + tiley + ", dataFileIndex=" + + dataFileIndex + ", offset=" + offset + ", len=" + len + "]"; + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/Galileo.java b/src/main/java/mobac/program/atlascreators/Galileo.java new file mode 100644 index 0000000..ccfc305 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Galileo.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.program.annotations.AtlasCreatorName; + +@AtlasCreatorName("Galileo Offline Maps") +public class Galileo extends BigPlanetTracks { + +} diff --git a/src/main/java/mobac/program/atlascreators/GarminCustom.java b/src/main/java/mobac/program/atlascreators/GarminCustom.java new file mode 100644 index 0000000..72e545a --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/GarminCustom.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.interfaces.LayerInterface; +import mobac.program.model.Settings; +import mobac.program.model.TileImageParameters.Name; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; +import mobac.utilities.stream.ArrayOutputStream; + +@AtlasCreatorName("Garmin Custom Map (KMZ)") +@SupportedParameters(names = { Name.format_jpg }) +public class GarminCustom extends GoogleEarthOverlay { + + /** + * Each jpeg should be less than 3MB. https://forums.garmin.com/showthread.php?t=2646 + */ + private static final int MAX_FILE_SIZE = 3 * 1024 * 1024; + + @Override + protected void testAtlas() throws AtlasTestException { + int maxMap = Settings.getInstance().atlasFormatSpecificSettings.garminCustomMaxMapCount; + for (LayerInterface layer : atlas) { + if (layer.getMapCount() > maxMap) + throw new AtlasTestException("Layer exceeeds the maximum map count of " + maxMap, layer); + } + } + + @Override + protected void writeTileImage(BufferedImage tileImage) throws MapCreationException { + try { + TileImageJpegDataWriter writer; + if (parameters != null) { + writer = (TileImageJpegDataWriter) parameters.getFormat().getDataWriter(); + writer = new TileImageJpegDataWriter(writer); + } else + writer = new TileImageJpegDataWriter(0.9); + + writer.initialize(); + // The maximum file size for the jpg image is 3 MB + // This OutputStream will fail if the resulting image is larger than + // 3 MB - then we retry using a higher JPEG compression level + ArrayOutputStream buf = new ArrayOutputStream(MAX_FILE_SIZE); + byte[] data = null; + for (int c = 99; c > 50; c -= 5) { + buf.reset(); + try { + writer.processImage(tileImage, buf); + data = buf.toByteArray(); + break; + } catch (IOException e) { + log.trace("Image size too large, increasing compression to " + c); + } + writer.setJpegCompressionLevel(c / 100f); + } + if (data == null) + throw new MapCreationException("Unable to create an image with less than 3 MB!", map); + String imageFileName = "files/" + cleanedMapName + "." + writer.getType(); + kmzOutputStream.writeStoredEntry(imageFileName, data); + addMapToKmz(imageFileName); + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + @Override + protected int getMaxImageSize() { + return 1024; + } +} diff --git a/src/main/java/mobac/program/atlascreators/Glopus.java b/src/main/java/mobac/program/atlascreators/Glopus.java new file mode 100644 index 0000000..2d26756 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Glopus.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.Utilities; + +@AtlasCreatorName("Glopus (PNG & KAL)") +public class Glopus extends Ozi { + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + String layerName = map.getLayer().getName().replaceAll(" ", "_"); + mapName = map.getName().replaceAll(" ", "_"); + layerDir = new File(atlasDir, layerName); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(layerDir); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + createTiles(); + writeKalFile(); + } + + private void writeKalFile() throws MapCreationException { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, mapName + ".kal")); + OutputStreamWriter mapWriter = new OutputStreamWriter(fout, TEXT_FILE_CHARSET); + + MapSpace mapSpace = mapSource.getMapSpace(); + + String longitudeMin = Double.toString(mapSpace.cXToLon(xMin * tileSize, zoom)); + String longitudeMax = Double.toString(mapSpace.cXToLon((xMax + 1) * tileSize, zoom)); + String latitudeMin = Double.toString(mapSpace.cYToLat((yMax + 1) * tileSize, zoom)); + String latitudeMax = Double.toString(mapSpace.cYToLat(yMin * tileSize, zoom)); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + + mapWriter.write("[Calibration Point 1]\n"); + mapWriter.write(String.format("Longitude = %s\n", longitudeMin)); + mapWriter.write(String.format("Latitude = %s\n", latitudeMax)); + mapWriter.write("Pixel = POINT(0,0)\n"); + + mapWriter.write("[Calibration Point 2]\n"); + mapWriter.write(String.format("Longitude = %s\n", longitudeMax)); + mapWriter.write(String.format("Latitude = %s\n", latitudeMin)); + mapWriter.write(String.format("Pixel = POINT(%d,%d)\n", width, height)); + + mapWriter.write("[Calibration Point 3]\n"); + mapWriter.write(String.format("Longitude = %s\n", longitudeMax)); + mapWriter.write(String.format("Latitude = %s\n", latitudeMax)); + mapWriter.write(String.format("Pixel = POINT(%d,%d)\n", width, 0)); + + mapWriter.write("[Calibration Point 4]\n"); + mapWriter.write(String.format("Longitude = %s\n", longitudeMin)); + mapWriter.write(String.format("Latitude = %s\n", latitudeMin)); + mapWriter.write(String.format("Pixel = POINT(%d,%d)\n", 0, height)); + + mapWriter.write("[Map]\n"); + mapWriter.write(String.format("Bitmap = %s.png\n", mapName)); + mapWriter.write(String.format("Size = SIZE(%d,%d)\n", width, height)); + + mapWriter.flush(); + mapWriter.close(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + Utilities.closeStream(fout); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/GlopusMapFile.java b/src/main/java/mobac/program/atlascreators/GlopusMapFile.java new file mode 100644 index 0000000..d31ad62 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/GlopusMapFile.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.LinkedList; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.Utilities; +import mobac.utilities.stream.LittleEndianOutputStream; + +/** + * General structure of an GMF file (Little Endian) + * + *
+ * DWORD Version // 0xff000002
+ * DWORD cnt // Number of tiles in the file
+ * 
+ * for each tile: 
+ *   DWORD len;         // number of characters in tile name
+ *   wchar_t name[len]  // map/tile name in UTF_16LE
+ *   DWORD filepos      // offset where image data starts in this file
+ *   DWORD width        // tile width in pixel
+ *   DWORD height       // tile height in pixel
+ *   DWORD cntCalPoints // calibration point count (usually 2 or 4)
+ *   for each tile calibration point
+ *     DWORD x      // calibration point x position in tile
+ *     DWORD y      // calibration point y position in tile
+ *     double dLong // longitude of calibration point
+ *     double dLat  // latitude of calibration point
+ * END OF FILE HEADER
+ * Afterwards the tile image data follows as specified by each filepos 
+ * offset.
+ * 
+ * + */ +@AtlasCreatorName(value = "Glopus Map File (GMF)", type = "Gmf") +public class GlopusMapFile extends TrekBuddy { + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + mapTileWriter = new GlopusTileWriter(layer); + } + + @Override + public void finishLayerCreation() throws IOException { + mapTileWriter.finalizeMap(); + mapTileWriter = null; + super.finishLayerCreation(); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + ((GlopusTileWriter) mapTileWriter).initMap(); + // Select the tile creator instance based on whether tile image + // parameters has been set or not + if (parameters != null) + createCustomTiles(); + else + createTiles(); + } catch (MapCreationException e) { + throw e; + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + @Override + public void createAtlasTbaFile(String name) { + } + + @Override + public void abortAtlasCreation() throws IOException { + mapTileWriter = null; + super.abortAtlasCreation(); + } + + private class GlopusTileWriter implements MapTileWriter { + + final LayerInterface layer; + LinkedList tiles; + int xCoordStart; + int yCoordStart; + int tileHeight = 256; + int tileWidth = 256; + int zoom; + MapSpace mapSpace; + String tileType; + + public GlopusTileWriter(LayerInterface layer) { + super(); + this.layer = layer; + tiles = new LinkedList(); + } + + public void initMap() { + if (parameters != null) { + tileHeight = parameters.getHeight(); + tileWidth = parameters.getWidth(); + } + zoom = map.getZoom(); + mapSpace = mapSource.getMapSpace(); + xCoordStart = GlopusMapFile.this.xMin * mapSpace.getTileSize(); + yCoordStart = GlopusMapFile.this.yMin * mapSpace.getTileSize(); + } + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + this.tileType = tileType; + int xCooord = xCoordStart + tilex * tileWidth; + int yCooord = yCoordStart + tiley * tileHeight; + + double calWLon = mapSpace.cXToLon(xCooord, zoom); + double calNLat = mapSpace.cYToLat(yCooord, zoom); + double calELon = mapSpace.cXToLon(xCooord + tileWidth, zoom); + double calSLat = mapSpace.cYToLat(yCooord + tileHeight, zoom); + GlopusTile gt = new GlopusTile(tileData, calNLat, calWLon, calSLat, calELon); + tiles.add(gt); + } + + public void finalizeMap() { + + File gmfFile = new File(atlasDir, layer.getName() + ".gmf"); + FileOutputStream fout = null; + try { + int count = tiles.size(); + int offset = 8 + count * ( // + 20 // nameLength, offset and calibration point count, + // tile height & width + + (12 * 2) // name bytes + + (4 * 24) // four calibration points + ); + fout = new FileOutputStream(gmfFile); + LittleEndianOutputStream out = new LittleEndianOutputStream(new BufferedOutputStream(fout, 16384)); + out.writeInt((int) 0xff000002); + out.writeInt(count); + int mapNumber = 0; + Charset charset = Charset.forName("UTF-16LE"); + for (GlopusTile gt : tiles) { + String mapName = String.format("%08d.%s", mapNumber++, tileType); + byte[] nameBytes = mapName.getBytes(charset); + out.writeInt(mapName.length());// Name length + out.write(nameBytes); + out.writeInt(offset); + out.writeInt(tileWidth); + out.writeInt(tileHeight); + out.writeInt(4); // number of calibration points + out.writeInt(0); + out.writeInt(0); + out.writeDouble(gt.calWLon); + out.writeDouble(gt.calNLat); + out.writeInt(tileHeight); + out.writeInt(tileWidth); + out.writeDouble(gt.calELon); + out.writeDouble(gt.calSLat); + out.writeInt(tileHeight); + out.writeInt(0); + out.writeDouble(gt.calELon); + out.writeDouble(gt.calNLat); + out.writeInt(0); + out.writeInt(tileWidth); + out.writeDouble(gt.calWLon); + out.writeDouble(gt.calSLat); + if (log.isTraceEnabled()) + log.trace(String.format("Offset %f %f %f %f \"%s\": 0x%x", gt.calWLon, gt.calNLat, gt.calELon, + gt.calELon, mapName, offset)); + offset += gt.data.length; + } + out.flush(); + out = null; + for (GlopusTile gt : tiles) { + fout.write(gt.data); + } + fout.flush(); + } catch (IOException e) { + GUIExceptionHandler.showExceptionDialog(e); + } finally { + Utilities.closeStream(fout); + } + } + + } + + private static class GlopusTile { + byte[] data; + double calNLat; + double calWLon; + double calSLat; + double calELon; + + public GlopusTile(byte[] data, double calNLat, double calWLon, double calSLat, double calELon) { + super(); + this.data = data; + this.calNLat = calNLat; + this.calWLon = calWLon; + this.calSLat = calSLat; + this.calELon = calELon; + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/GoogleEarthOverlay.java b/src/main/java/mobac/program/atlascreators/GoogleEarthOverlay.java new file mode 100644 index 0000000..9c6d9aa --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/GoogleEarthOverlay.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.text.NumberFormat; +import java.util.zip.ZipOutputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ZipStoreOutputStream; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +@AtlasCreatorName(value = "Google Earth Overlay (KMZ)", type = "GoogleEarthRasterOverlay") +public class GoogleEarthOverlay extends AbstractPlainImage { + + protected File mapDir; + protected String cleanedMapName; + + protected File kmzFile = null; + protected ZipStoreOutputStream kmzOutputStream = null; + + private Document kmlDoc = null; + private Element groundOverlayRoot = null; + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + Utilities.mkDirs(atlasDir); + kmzFile = new File(atlasDir, layer.getName() + ".kmz"); + kmzOutputStream = new ZipStoreOutputStream(kmzFile); + kmzOutputStream.setMethod(ZipOutputStream.STORED); + try { + if (layer.getMapCount() <= 1) + initKmlDoc(null); + else + initKmlDoc(layer.getName()); + } catch (ParserConfigurationException e) { + throw new IOException(e); + } + } + + @Override + public void finishLayerCreation() throws IOException { + try { + writeKmlToZip(); + } catch (Exception e) { + throw new IOException(e); + } + Utilities.closeStream(kmzOutputStream); + kmzOutputStream = null; + kmzFile = null; + super.finishLayerCreation(); + } + + @Override + public void abortAtlasCreation() throws IOException { + Utilities.closeStream(kmzOutputStream); + kmzOutputStream = null; + kmzFile = null; + super.abortAtlasCreation(); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + mapDir = new File(atlasDir, map.getLayer().getName()); + cleanedMapName = map.getName(); + cleanedMapName = cleanedMapName.replaceAll("[[^\\p{Alnum}-_]]+", "_"); + cleanedMapName = cleanedMapName.replaceAll("_{2,}", "_"); + if (cleanedMapName.endsWith("_")) + cleanedMapName = cleanedMapName.substring(0, cleanedMapName.length() - 1); + if (cleanedMapName.startsWith("_")) + cleanedMapName = cleanedMapName.substring(1, cleanedMapName.length()); + } + + protected int getBufferedImageType() { + return BufferedImage.TYPE_3BYTE_BGR; + } + + protected void writeTileImage(BufferedImage tileImage) throws MapCreationException { + TileImageDataWriter writer; + if (parameters != null) { + writer = parameters.getFormat().getDataWriter(); + } else + writer = new TileImageJpegDataWriter(0.9); + writer.initialize(); + try { + int initialBufferSize = tileImage.getWidth() * tileImage.getHeight() / 4; + ByteArrayOutputStream buf = new ByteArrayOutputStream(initialBufferSize); + writer.processImage(tileImage, buf); + String imageFileName = "files/" + cleanedMapName + "." + writer.getType(); + kmzOutputStream.writeStoredEntry(imageFileName, buf.toByteArray()); + addMapToKmz(imageFileName); + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + protected void addMapToKmz(String imageFileName) throws ParserConfigurationException, + TransformerFactoryConfigurationError, TransformerException, IOException { + int startX = xMin * tileSize; + int endX = (xMax + 1) * tileSize; + int startY = yMin * tileSize; + int endY = (yMax + 1) * tileSize; + addKmlEntry(map.getName(), imageFileName, startX, startY, endX - startX, endY - startY); + } + + private void initKmlDoc(String folderName) throws ParserConfigurationException { + + DocumentBuilder builder; + builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + kmlDoc = builder.newDocument(); + + Element kml = kmlDoc.createElementNS("http://www.opengis.net/kml/2.2", "kml"); + kmlDoc.appendChild(kml); + groundOverlayRoot = kml; + if (folderName != null) { + groundOverlayRoot = kmlDoc.createElement("Folder"); + kml.appendChild(groundOverlayRoot); + Element name = kmlDoc.createElement("name"); + name.setTextContent(folderName); + Element open = kmlDoc.createElement("open"); + open.setTextContent("1"); + groundOverlayRoot.appendChild(name); + groundOverlayRoot.appendChild(open); + } + + } + + private void addKmlEntry(String imageName, String imageFileName, int startX, int startY, int width, int height) { + Element go = kmlDoc.createElement("GroundOverlay"); + Element name = kmlDoc.createElement("name"); + Element ico = kmlDoc.createElement("Icon"); + Element href = kmlDoc.createElement("href"); + Element drawOrder = kmlDoc.createElement("drawOrder"); + Element latLonBox = kmlDoc.createElement("LatLonBox"); + Element north = kmlDoc.createElement("north"); + Element south = kmlDoc.createElement("south"); + Element east = kmlDoc.createElement("east"); + Element west = kmlDoc.createElement("west"); + Element rotation = kmlDoc.createElement("rotation"); + + name.setTextContent(imageName); + href.setTextContent(imageFileName); + drawOrder.setTextContent("0"); + + MapSpace mapSpace = mapSource.getMapSpace(); + NumberFormat df = Utilities.FORMAT_6_DEC_ENG; + + String longitudeMin = df.format(mapSpace.cXToLon(startX, zoom)); + String longitudeMax = df.format(mapSpace.cXToLon(startX + width, zoom)); + String latitudeMin = df.format(mapSpace.cYToLat(startY + height, zoom)); + String latitudeMax = df.format(mapSpace.cYToLat(startY, zoom)); + + north.setTextContent(latitudeMax); + south.setTextContent(latitudeMin); + west.setTextContent(longitudeMin); + east.setTextContent(longitudeMax); + rotation.setTextContent("0.0"); + + groundOverlayRoot.appendChild(go); + go.appendChild(name); + go.appendChild(ico); + go.appendChild(latLonBox); + ico.appendChild(href); + ico.appendChild(drawOrder); + + latLonBox.appendChild(north); + latLonBox.appendChild(south); + latLonBox.appendChild(east); + latLonBox.appendChild(west); + latLonBox.appendChild(rotation); + } + + private void writeKmlToZip() throws TransformerFactoryConfigurationError, TransformerException, IOException { + Transformer serializer; + serializer = TransformerFactory.newInstance().newTransformer(); + serializer.setOutputProperty(OutputKeys.INDENT, "yes"); + serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(16000); + serializer.transform(new DOMSource(kmlDoc), new StreamResult(bos)); + kmzOutputStream.writeStoredEntry("doc.kml", bos.toByteArray()); + kmlDoc = null; + groundOverlayRoot = null; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/GpsSportsTracker.java b/src/main/java/mobac/program/atlascreators/GpsSportsTracker.java new file mode 100644 index 0000000..0613af3 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/GpsSportsTracker.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageType; +import mobac.utilities.Utilities; + +/** + * Derived from {@link MobileTrailExplorer} + */ +@AtlasCreatorName("GPS Sports Tracker") +public class GpsSportsTracker extends AtlasCreator { + + private File mapDir = null; + + protected String appendFileExt = ""; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + String mapName = map.getMapSource().getName().replaceAll(" ", "_"); + mapDir = new File(atlasDir, mapName); + } + + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(mapDir); + } catch (IOException e1) { + throw new MapCreationException(map, e1); + } + if (mapSource.getTileImageType() != TileImageType.PNG) + // If the tile image format is not png we have to convert it + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, TileImageFormat.PNG); + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + String tileFileName = map.getZoom() + "_" + x + "_" + y + ".png"; + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + File f = new File(mapDir, tileFileName); + FileOutputStream out = new FileOutputStream(f); + try { + out.write(sourceTileData); + } finally { + Utilities.closeStream(out); + } + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/IPhone3MapTiles5.java b/src/main/java/mobac/program/atlascreators/IPhone3MapTiles5.java new file mode 100644 index 0000000..1fffad3 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/IPhone3MapTiles5.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Point; +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.EnumSet; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileBuilder; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageType; + +/** + * The following SQL statements create SQLite database for background map: + * + * BEGIN TRANSACTION; CREATE TABLE images(zoom int, x int, y int, flags int, length int, data blob); CREATE TABLE + * version(version int); INSERT INTO version VALUES(5); INSERT INTO version VALUES(0); + * + * CREATE INDEX index1 on images (zoom,x,y,flags); COMMIT; + * + * The columns in "images" table have the following meaning: + * + * zoom - zoom level from 1 (top level) to 23 (most detailed level). On the top level we have four squares 128x128 + * pixels, it gives 512x512 pixels. So map width = map height = 256 * 2zoom pixels. + * + * x, y - coordinates for given zoom level: + * + * + * + * flags - layer number. Theoretically map could contain number of layers: street, satellite, hybrid and so on. + * Practically the program shows only one layer with smallest flags value. + * + * length - size of binary image in bytes. + * + * data - binary tile image in PNG or JPEG format. Practically the program supports only PNG for now. + * + * For more details regarding Web Mercator projection and coordinates please see: + * + * http://msdn.microsoft.com/en-us/library/bb259689.aspx + */ +@AtlasCreatorName(value = "iPhone 3 Map Tiles v5") +@SupportedParameters(names = {}) +public class IPhone3MapTiles5 extends RMapsSQLite { + + private static final String INSERT_SQL = "INSERT or REPLACE INTO images(x,y,zoom,data,length,flags) VALUES (?,?,?,?,?,0);"; + private static final String TABLE_IMAGES = "CREATE TABLE IF NOT EXISTS images(zoom int, x int, y int, flags int, length int, data blob); "; + private static final String TABLE_VERSION = "CREATE TABLE IF NOT EXISTS version(version int);"; + private static final String TABLE_VERSION_DATA = "INSERT INTO version VALUES(5);"; + private static final String INDEX_IMAGES = "CREATE INDEX IF NOT EXISTS index1 on images (zoom,x,y,flags);"; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + protected void testAtlas() throws AtlasTestException { + EnumSet allowed = EnumSet.of(TileImageType.JPG, TileImageType.PNG); + // Test of output format - only jpg xor png is allowed + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + TileImageParameters parameters = map.getParameters(); + TileImageType currentTit; + if (parameters == null) { + currentTit = map.getMapSource().getTileImageType(); + if (!allowed.contains(currentTit)) + throw new AtlasTestException( + "Map source format incompatible - tile format conversion to PNG or JPG is required for this map.", + map); + } else { + currentTit = parameters.getFormat().getType(); + if (!allowed.contains(currentTit)) + throw new AtlasTestException( + "Selected custom tile format not supported - only JPG and PNG formats are supported.", + map); + } + } + } + } + + @Override + protected void openConnection() throws SQLException { + if (databaseFile.isFile()) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss"); + databaseFile = new File(atlasDir, atlas.getName() + "_" + sdf.format(new Date()) + ".sqlitedb"); + } + super.openConnection(); + } + + @Override + protected void initializeDB() throws SQLException { + Statement stat = conn.createStatement(); + stat.executeUpdate(TABLE_IMAGES); + stat.executeUpdate(INDEX_IMAGES); + if (stat.executeUpdate(TABLE_VERSION) == 0) + stat.execute(TABLE_VERSION_DATA); + stat.close(); + } + + @Override + protected void createTiles() throws InterruptedException, MapCreationException { + + try { + parameters = new TileImageParameters(128, 128, TileImageFormat.PNG); + conn.setAutoCommit(false); + prepStmt = conn.prepareStatement(getTileInsertSQL()); + + MapTileWriter mapTileWriter = new SQLiteMapTileWriter(map); + MapTileBuilder mapTileBuilder = new MapTileBuilder(this, mapTileWriter, true); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + mapTileWriter.finalizeMap(); + prepStmt.close(); + } catch (SQLException e) { + throw new MapCreationException(map, e); + } catch (IOException e) { + Throwable t = e; + if (t.getCause() instanceof SQLException) + t = t.getCause(); + throw new MapCreationException(map, t); + } + } + + @Override + protected void updateTileMetaInfo() throws SQLException { + } + + @Override + protected String getTileInsertSQL() { + return INSERT_SQL; + } + + protected String getDatabaseFileName() { + return atlas.getName() + ".sqlitedb"; + } + + private class SQLiteMapTileWriter implements MapTileWriter { + + private final int z; + private final int baseTileX; + private final int baseTileY; + + SQLiteMapTileWriter(MapInterface map) { + this.z = map.getZoom() + 1; + Point topLeft = map.getMinTileCoordinate(); + baseTileX = topLeft.x / 128; + baseTileY = topLeft.y / 128; + } + + @Override + public void writeTile(int x, int y, String tileType, byte[] tileData) throws IOException { + x += baseTileX; + y += baseTileY; + try { + prepStmt.setInt(1, x); + prepStmt.setInt(2, y); + prepStmt.setInt(3, z); + prepStmt.setBytes(4, tileData); + prepStmt.setInt(5, tileData.length); + prepStmt.addBatch(); + } catch (SQLException e) { + throw new IOException(e); + } + } + + @Override + public void finalizeMap() throws IOException { + try { + prepStmt.executeBatch(); + prepStmt.clearBatch(); + conn.commit(); + } catch (SQLException e) { + throw new IOException(e); + } + } + + } + +} diff --git a/src/main/java/mobac/program/atlascreators/MBTiles.java b/src/main/java/mobac/program/atlascreators/MBTiles.java new file mode 100644 index 0000000..a5a37f1 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/MBTiles.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.EnumSet; +import java.util.Locale; + +import mobac.exceptions.AtlasTestException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageType; + +/** + * https://github.com/mapbox/mbtiles-spec/tree/master/1.1 + */ +@AtlasCreatorName(value = "MBTiles SQLite") +public class MBTiles extends RMapsSQLite { + + private static final String INSERT_SQL = "INSERT or REPLACE INTO tiles (tile_column,tile_row,zoom_level,tile_data) VALUES (?,?,?,?)"; + private static final String TABLE_TILES = "CREATE TABLE IF NOT EXISTS tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob);"; + private static final String INDEX_TILES = "CREATE INDEX IF NOT EXISTS tiles_idx on tiles (zoom_level, tile_column, tile_row)"; + private static final String TABLE_METADATA = "CREATE TABLE IF NOT EXISTS metadata (name text, value text);"; + private static final String INSERT_METADATA = "INSERT INTO metadata (name,value) VALUES (?,?);"; + private static final String INDEX_METADATA = "CREATE UNIQUE INDEX IF NOT EXISTS metadata_idx ON metadata (name);"; + + private double boundsLatMin; + private double boundsLatMax; + private double boundsLonMin; + private double boundsLonMax; + + private TileImageType atlasTileImageType; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + protected void testAtlas() throws AtlasTestException { + EnumSet allowed = EnumSet.of(TileImageType.JPG, TileImageType.PNG); + // Test of output format - only jpg xor png is allowed + TileImageType tit = null; + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + TileImageParameters parameters = map.getParameters(); + TileImageType currentTit; + if (parameters == null) { + currentTit = map.getMapSource().getTileImageType(); + if (!allowed.contains(currentTit)) + throw new AtlasTestException( + "Map source format incompatible - tile format conversion to PNG or JPG is required for this map.", + map); + } else { + currentTit = parameters.getFormat().getType(); + if (!allowed.contains(currentTit)) + throw new AtlasTestException( + "Selected custom tile format not supported - only JPG and PNG formats are supported.", + map); + } + if (tit != null && !currentTit.equals(tit)) { + throw new AtlasTestException("All maps within one atlas must use the same format (PNG or JPG). " + + "Use tile format conversion converting maps with a different format.", map); + } + tit = currentTit; + } + } + atlasTileImageType = tit; + } + + @Override + protected void openConnection() throws SQLException { + if (databaseFile.isFile()) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss"); + databaseFile = new File(atlasDir, atlas.getName() + "_" + sdf.format(new Date()) + ".mbtiles"); + } + super.openConnection(); + } + + @Override + protected void initializeDB() throws SQLException { + Statement stat = conn.createStatement(); + stat.executeUpdate(TABLE_TILES); + stat.executeUpdate(INDEX_TILES); + stat.executeUpdate(TABLE_METADATA); + stat.executeUpdate(INDEX_METADATA); + stat.close(); + boundsLatMin = Double.MAX_VALUE; + boundsLatMax = Double.MIN_VALUE; + boundsLonMin = Double.MAX_VALUE; + boundsLonMax = Double.MIN_VALUE; + } + + @Override + protected void updateTileMetaInfo() throws SQLException { + MapSpace ms = map.getMapSource().getMapSpace(); + double lon1 = ms.cXToLon(map.getMinTileCoordinate().x, zoom); + double lon2 = ms.cXToLon(map.getMaxTileCoordinate().x, zoom); + double lat1 = ms.cYToLat(map.getMinTileCoordinate().y, zoom); + double lat2 = ms.cYToLat(map.getMaxTileCoordinate().y, zoom); + + boundsLatMin = Math.min(boundsLatMin, Math.min(lat1, lat2)); + boundsLatMax = Math.max(boundsLatMax, Math.max(lat1, lat2)); + boundsLonMin = Math.min(boundsLonMin, Math.min(lon1, lon2)); + boundsLonMax = Math.max(boundsLonMin, Math.max(lon1, lon2)); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + PreparedStatement st; + try { + st = conn.prepareStatement(INSERT_METADATA); + st.setString(1, "bounds"); + st.setString(2, String.format(Locale.ENGLISH, "%.3f,%.3f,%.3f,%.3f", boundsLonMin, boundsLatMin, + boundsLonMax, boundsLatMax)); + st.execute(); + st.setString(1, "name"); + st.setString(2, atlas.getName()); + st.execute(); + st.setString(1, "type"); + st.setString(2, "baselayer"); + st.execute(); + st.setString(1, "version"); + st.setString(2, "1.1"); + st.execute(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + st.setString(1, "description"); + st.setString(2, atlas.getName() + " created on " + sdf.format(new Date()) + " by MOBAC"); + st.execute(); + st.setString(1, "format"); + st.setString(2, atlasTileImageType.getFileExt()); + st.execute(); + st.close(); + conn.commit(); + } catch (SQLException e) { + throw new IOException(e); + } + super.finishAtlasCreation(); + } + + @Override + protected String getTileInsertSQL() { + return INSERT_SQL; + } + + @Override + protected void writeTile(int x, int y, int z, byte[] tileData) throws SQLException, IOException { + y = (1 << z) - y - 1; + prepStmt.setInt(1, x); + prepStmt.setInt(2, y); + prepStmt.setInt(3, z); + prepStmt.setBytes(4, tileData); + prepStmt.addBatch(); + } + + protected String getDatabaseFileName() { + return atlas.getName() + ".mbtiles"; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/MGMaps.java b/src/main/java/mobac/program/atlascreators/MGMaps.java new file mode 100644 index 0000000..e9aa98c --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/MGMaps.java @@ -0,0 +1,287 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.RandomAccessFile; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ArrayOutputStream; + +/** + * Creates maps using the MGM pack file format (.mgm). + * + * Each zoom level in a different directory, 64 tiles per mgm file. + * + *

Format

+ *
    + *
  • 2 bytes: number of tiles in this file for each tile (even for tiles that are not used, in which case the data is + * left null)
  • + *
  • 1 byte: tile x within this file; add tileX * tilesPerFileX to get global tile number
  • + *
  • 1 byte: tile y within this file; add tileY * tilesPerFileY to get global tile number
  • + *
  • 4 bytes: offset of the end of the tile data within this file (to get the offset for the start of the tile data, + * subtract the value for the previous tile, or 2 + 6 * tilesPerFile tile data 1 tile data 2 ...
  • + *
+ * + * @author paour + */ +@AtlasCreatorName(value = "MGMaps/MyTrails (MGM)", type = "MGM") +@SupportedParameters(names = { Name.format }) +public class MGMaps extends AtlasCreator { + + private static final int TILES_PER_FILE_X = 8; + private static final int TILES_PER_FILE_Y = 8; + private static final int TILES_PER_FILE = TILES_PER_FILE_X * TILES_PER_FILE_Y; + + private double xResizeRatio = 1.0; + private double yResizeRatio = 1.0; + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws AtlasTestException, IOException, + InterruptedException { + super.startAtlasCreation(atlas, customAtlasDir); + + File cache_conf = new File(atlasDir, "cache.conf"); + PrintWriter pw = new PrintWriter(new FileWriter(cache_conf)); + try { + pw.println("version=3"); + pw.println("tiles_per_file=" + TILES_PER_FILE); + pw.println("hash_size=1"); + } finally { + pw.close(); + } + } + + @Override + public void initializeMap(final MapInterface map, final TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + + xResizeRatio = 1.0; + yResizeRatio = 1.0; + + if (parameters != null) { + int mapTileSize = map.getMapSource().getMapSpace().getTileSize(); + if ((parameters.getWidth() != mapTileSize) || (parameters.getHeight() != mapTileSize)) { + // handle image re-sampling + image re-sizing + xResizeRatio = (double) parameters.getWidth() / (double) mapTileSize; + yResizeRatio = (double) parameters.getHeight() / (double) mapTileSize; + } else { + // handle only image re-sampling + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + } + } + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + MGMTileWriter mgmTileWriter = null; + try { + if ((xResizeRatio != 1.0) || (yResizeRatio != 1.0)) + mgmTileWriter = new MGMResizeTileWriter(); + else + mgmTileWriter = new MGMTileWriter(); + + String name = map.getLayer().getName(); + + // safe naming: replace all non-word characters: [^a-zA-Z_0-9] + name = name.replaceAll("[^a-zA-Z_0-9]", "_"); + + // crate directory if necessary + File folder = new File(atlasDir, name + "_" + map.getZoom()); + Utilities.mkDirs(folder); + + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + + ImageIO.setUseCache(false); + + int pxMin = xMin / TILES_PER_FILE_X; + int pxMax = xMax / TILES_PER_FILE_X; + int pyMin = yMin / TILES_PER_FILE_Y; + int pyMax = yMax / TILES_PER_FILE_Y; + + for (int px = pxMin; px <= pxMax; px++) { + for (int py = pyMin; py <= pyMax; py++) { + int count = 0; + int pos = 2 + TILES_PER_FILE * 6; + File pack = new File(folder, px + "_" + py + ".mgm"); + RandomAccessFile raf = null; + + try { + for (int i = 0; i < TILES_PER_FILE_X; i++) { + int x = px * TILES_PER_FILE_X + i; + if (x < xMin || x > xMax) { + continue; + } + + for (int j = 0; j < TILES_PER_FILE_Y; j++) { + int y = py * TILES_PER_FILE_Y + j; + if (y < yMin || y > yMax) { + continue; + } + + if (raf == null) + // Only create a file when needed + raf = new RandomAccessFile(pack, "rw"); + + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + int res = mgmTileWriter.writeTile(x, y, i, j, raf, pos, count); + if (res >= 0) { + pos = res; + count++; + } + } + } + + if (raf != null) { + // POSITION 0: number of tiles + raf.seek(0); + raf.writeChar(count); + } + } finally { + Utilities.closeFile(raf); + } + if (count == 0) { + // the file doesn't contain any tiles + if (pack.exists()) + Utilities.deleteFile(pack); + } + } + } + + } catch (Exception e) { + throw new MapCreationException(map, e); + } finally { + if (mgmTileWriter != null) + mgmTileWriter.dispose(); + } + } + + @Override + public boolean testMapSource(final MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace) + && (ProjectionCategory.SPHERE.equals(mapSource.getMapSpace().getProjectionCategory()) || ProjectionCategory.ELLIPSOID + .equals(mapSource.getMapSpace().getProjectionCategory())); + } + + /** + * Simply writes the tile to the file without resizing + */ + private class MGMTileWriter { + + protected byte[] getSourceTileData(int x, int y) throws IOException { + return mapDlTileProvider.getTileData(x, y); + } + + public int writeTile(int x, int y, int i, int j, RandomAccessFile raf, int startPos, int count) + throws MapCreationException { + try { + byte[] sourceTileData = getSourceTileData(x, y); + if (sourceTileData == null) + return -1; + raf.seek(startPos); + raf.write(sourceTileData); + + // write the tile index + raf.seek(2 + count * 6); + raf.writeByte(i); + raf.writeByte(j); + int pos = startPos + sourceTileData.length; + raf.writeInt(pos); + return pos; + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + + public void dispose() { + // Nothing to do + } + } + + /** + * Resizes the tile and saves it to the file + */ + private class MGMResizeTileWriter extends MGMTileWriter { + + private final BufferedImage tileImage; + private final Graphics2D graphics; + private final TileImageDataWriter writer; + private final ArrayOutputStream buffer; + + public MGMResizeTileWriter() { + // resize image + tileImage = new BufferedImage(parameters.getWidth(), parameters.getHeight(), BufferedImage.TYPE_3BYTE_BGR); + + // associated graphics with affine transform + graphics = tileImage.createGraphics(); + graphics.setTransform(AffineTransform.getScaleInstance(xResizeRatio, yResizeRatio)); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + // image compression writer + writer = parameters.getFormat().getDataWriter(); + + // buffer to store compressed image + buffer = new ArrayOutputStream(3 * parameters.getWidth() * parameters.getHeight()); + } + + @Override + protected byte[] getSourceTileData(int x, int y) throws IOException { + // need to resize the tile + final BufferedImage tile = mapDlTileProvider.getTileImage(x, y); + graphics.drawImage(tile, 0, 0, null); + buffer.reset(); + writer.initialize(); + writer.processImage(tileImage, buffer); + + byte[] processedTileData = buffer.toByteArray(); + buffer.reset(); + return processedTileData; + } + + @Override + public void dispose() { + buffer.reset(); + graphics.dispose(); + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/MagellanRmp.java b/src/main/java/mobac/program/atlascreators/MagellanRmp.java new file mode 100644 index 0000000..03fee29 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/MagellanRmp.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Point; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.rmp.BoundingRect; +import mobac.program.atlascreators.impl.rmp.MultiImage; +import mobac.program.atlascreators.impl.rmp.RmpLayer; +import mobac.program.atlascreators.impl.rmp.RmpLayer.TLMEntry; +import mobac.program.atlascreators.impl.rmp.RmpTools; +import mobac.program.atlascreators.impl.rmp.RmpWriter; +import mobac.program.atlascreators.impl.rmp.Tiledata; +import mobac.program.atlascreators.impl.rmp.rmpfile.Bmp2bit; +import mobac.program.atlascreators.impl.rmp.rmpfile.Bmp4bit; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageParameters.Name; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; + +@AtlasCreatorName(value = "Magellan (RMP)", type = "Rmp") +@SupportedParameters(names = { Name.format_jpg }) +public class MagellanRmp extends AtlasCreator { + + TileImageDataWriter tileWriter = null; + RmpWriter rmpWriter = null; + String imageName = null; + int layerNum = 0; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, InterruptedException, + AtlasTestException { + super.startAtlasCreation(atlas, customAtlasDir); + int mapCount = 0; + for (LayerInterface layer : atlas) + mapCount += layer.getMapCount(); + imageName = RmpTools.buildImageName(atlas.getName()); + rmpWriter = new RmpWriter(imageName, mapCount, new File(atlasDir, imageName + ".rmp")); + } + + @Override + protected void testAtlas() throws AtlasTestException { + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + Point max = map.getMaxTileCoordinate(); + Point min = map.getMinTileCoordinate(); + // if (max.x - min.x > 18000 || max.y - min.y > 18000) + // throw new AtlasTestException("Map too large. Max size 18000x18000"); + if (map.getParameters() != null) { + if (!(map.getParameters().getFormat().getDataWriter() instanceof TileImageJpegDataWriter)) + throw new AtlasTestException("Only JPEG formats are supported", map); + } + } + } + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + if (parameters != null) { + tileWriter = parameters.getFormat().getDataWriter(); + } else + tileWriter = new TileImageJpegDataWriter(0.9); + tileWriter.initialize(); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation(1000); + ImageIO.setUseCache(false); + + MultiImage layerImage = new MultiImage(mapSource, mapDlTileProvider, map); + try { + RmpLayer layer = createLayer(layerImage, layerNum); + String layerName = RmpTools.buildTileName(imageName, layerNum); + TLMEntry tlmEntry = layer.getTLMFile(layerName); + rmpWriter.prepareFileEntry(tlmEntry); + rmpWriter.writeFileEntry(layer.getA00File(layerName)); + tlmEntry.updateContent(); + rmpWriter.writePreparedFileEntry(tlmEntry); + atlasProgress.setMapCreationProgress(1000); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + layerNum++; + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + if (rmpWriter == null) + return; // Creation already aborted + try { + rmpWriter.writeFileEntry(new Bmp2bit()); + rmpWriter.writeFileEntry(new Bmp4bit()); + rmpWriter.writeDirectory(); + } finally { + rmpWriter.close(); + rmpWriter = null; + } + } + + @Override + public void abortAtlasCreation() throws IOException { + super.abortAtlasCreation(); + rmpWriter.delete(); + rmpWriter = null; + } + + /** + * Create a new instance of a TLM file and fill it with the data of a calibrated image + * + * @param si + * image to get data from + * @param layer + * Layer number - for status output only + * @return TLM instance + * @throws InterruptedException + * @throws MapCreationException + * @throws IOException + */ + public RmpLayer createLayer(MultiImage si, int layer) throws InterruptedException, MapCreationException, + IOException { + + int count = 0; + + /* --- Create instance --- */ + RmpLayer rmpLayer = new RmpLayer(this); + + /* --- Get the coordinate space of the image --- */ + + MapSpace mapSpace = mapSource.getMapSpace(); + + double north = mapSpace.cYToLat(map.getMinTileCoordinate().y, zoom); + double south = mapSpace.cYToLat(map.getMaxTileCoordinate().y, zoom); + double west = mapSpace.cXToLon(map.getMinTileCoordinate().x, zoom); + double east = mapSpace.cXToLon(map.getMaxTileCoordinate().x, zoom); + + BoundingRect rect = new BoundingRect(-north, -south, west, east); + + Point max = map.getMaxTileCoordinate(); + Point min = map.getMinTileCoordinate(); + int imageWidth = max.x - min.x; + int imageHeight = max.y - min.y; + + /* --- Calculate tile dimensions --- */ + double tile_width = (rect.getEast() - rect.getWest()) * 256 / imageWidth; + double tile_height = (rect.getSouth() - rect.getNorth()) * 256 / imageHeight; + + /* --- Calculate the positions of the upper left tile --- */ + int x_start = (int) Math.floor((rect.getWest() + 180) / tile_width); + int y_start = (int) Math.floor((rect.getNorth() + 90) / tile_height); + + double x_end = (rect.getEast() + 180.0) / tile_width; + double y_end = (rect.getSouth() + 90.0) / tile_height; + + /* + * Create the tiles - process works column wise, starting on the top left corner of the destination area. + */ + for (int x = x_start; x < x_end; x++) { + for (int y = y_start; y < y_end; y++) { + count++; + + /* --- Create tile --- */ + BoundingRect subrect = new BoundingRect(y * tile_height - 90, (y + 1) * tile_height - 90, x + * tile_width - 180, (x + 1) * tile_width - 180); + Tiledata td = new Tiledata(tileWriter); + td.posx = x; + td.posy = y; + td.rect = subrect; + td.si = si; + rmpLayer.addPreparedImage(td); + } + } + + /* --- Build the TLM file --- */ + rmpLayer.buildTLMFile(tile_width, tile_height, rect.getWest(), rect.getEast(), rect.getNorth(), rect.getSouth()); + + return rmpLayer; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/Maplorer.java b/src/main/java/mobac/program/atlascreators/Maplorer.java new file mode 100644 index 0000000..7bc941e --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Maplorer.java @@ -0,0 +1,254 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Locale; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileBuilder; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.CacheTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; + +/** + * AtlasCreator for MAPLORER ( http://maplorer.com ) + * + * @author Werner Keilholz + */ +@AtlasCreatorName("Maplorer atlas format") +@SupportedParameters(names = { Name.format, Name.height, Name.width }) +public class Maplorer extends AtlasCreator { + + private static final String FILENAME_PATTERN = "map_%s%d.%s"; + + protected File layerFolder = null; + protected File mapFolder = null; + protected MapTileWriter mapTileWriter; + + protected int tileXmax = 0; + protected int tileYmax = 0; + + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + } + + protected void createCustomTiles() throws InterruptedException, MapCreationException { + log.debug("Starting map creation using custom parameters: " + parameters); + + CacheTileProvider ctp = new CacheTileProvider(mapDlTileProvider); + try { + mapDlTileProvider = ctp; + + MapTileBuilder mapTileBuilder = new MapTileBuilder(this, mapTileWriter, true); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + } finally { + ctp.cleanup(); + } + } + + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDirs(mapFolder); + + mapTileWriter = new FileTileWriter(); + + // Select the tile creator instance based on whether tile image + // parameters has been set or not + if (parameters != null) + createCustomTiles(); + else + createTiles(); + + mapTileWriter.finalizeMap(); + } catch (MapCreationException e) { + throw e; + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + LayerInterface layer = map.getLayer(); + layerFolder = new File(atlasDir, layer.getName()); + mapFolder = new File(layerFolder, map.getName()); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + int tilex = 0; + int tiley = 0; + + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + + ImageIO.setUseCache(false); + byte[] emptyTileData = Utilities.createEmptyTileData(mapSource); + String tileType = mapSource.getTileImageType().getFileExt(); + for (int x = xMin; x <= xMax; x++) { + tiley = 0; + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + mapTileWriter.writeTile(tilex, tiley, tileType, sourceTileData); + } else { + log.trace(String.format("Tile x=%d y=%d not found in tile archive - creating default", tilex, + tiley)); + mapTileWriter.writeTile(tilex, tiley, tileType, emptyTileData); + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + tiley++; + } + tilex++; + } + } + + public class FileTileWriter implements MapTileWriter { + + File setFolder; + + int tileHeight = 256; + int tileWidth = 256; + + public FileTileWriter() throws IOException { + super(); + setFolder = mapFolder; // don't need an extra sub folder for MAPLORER maps + log.debug("Writing tiles to set folder: " + setFolder); + + if (parameters != null) { + tileHeight = parameters.getHeight(); + tileWidth = parameters.getWidth(); + } + } + + public void writeTile(int tilex, int tiley, String imageFormat, byte[] tileData) throws IOException { + String tileFileName = String.format(FILENAME_PATTERN, IntToLetter(tilex + 1), tiley + 1, imageFormat); + + File f = new File(setFolder, tileFileName); + FileOutputStream out = new FileOutputStream(f); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + + tileXmax = tilex; + tileYmax = tiley; + } + + private String IntToLetter(int i) throws IOException { + if (i > 26) + throw new IOException("Maximum tile column overflow - map too wide!"); + char c = (char) (i + 64); + return Character.toString(c); + } + + public void finalizeMap() throws IOException { + + MapSpace mapSpace = mapSource.getMapSpace(); + + // compute corner coordinates for the entire map (all .JPG files) + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize - 1, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize - 1, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + + double widthInPixel = map.getMaxTileCoordinate().x - map.getMinTileCoordinate().x; + double heihgtInPixel = map.getMaxTileCoordinate().y - map.getMinTileCoordinate().y; + + double pix2longitude = (longitudeMax - longitudeMin) / widthInPixel; + double pix2latitude = (latitudeMax - latitudeMin) / heihgtInPixel; + + // compute corner coordinates for one tile (one JPG file) + double lonBL = longitudeMin; + double latBL = latitudeMax - tileHeight * pix2latitude; + double lonTR = longitudeMin + tileWidth * pix2longitude; + double latTR = latitudeMax; + + // write POS files + for (int tilex = 0; tilex <= tileXmax; tilex++) { + latBL = latitudeMax - tileHeight * pix2latitude; + latTR = latitudeMax; + + for (int tiley = 0; tiley <= tileYmax; tiley++) { + String posFileName = String.format(FILENAME_PATTERN, IntToLetter(tilex + 1), tiley + 1, "POS"); + File posFile = new File(setFolder, posFileName); + + lonBL = Math.max(longitudeMin, lonBL); // for tiles at the edges (might be smaller) + latBL = Math.max(latitudeMin, latBL); + lonTR = Math.min(longitudeMax, lonTR); + latTR = Math.min(latitudeMax, latTR); + + FileWriter outFile = new FileWriter(posFile); + try { + PrintWriter out2 = new PrintWriter(outFile); + + String posLine = "LonBL = %2.6f"; + out2.println(String.format(Locale.ENGLISH, posLine, lonBL)); + + posLine = "LatBL = %2.6f"; + out2.println(String.format(Locale.ENGLISH, posLine, latBL)); + + posLine = "LonTR = %2.6f"; + out2.println(String.format(Locale.ENGLISH, posLine, lonTR)); + + posLine = "LatTR = %2.6f"; + out2.println(String.format(Locale.ENGLISH, posLine, latTR)); + out2.close(); + } finally { + Utilities.closeWriter(outFile); + } + + latBL = latBL - tileHeight * pix2latitude; + latTR = latTR - tileHeight * pix2latitude; + + } // for y + + lonBL = lonBL + tileWidth * pix2longitude; + lonTR = lonTR + tileWidth * pix2longitude; + + } // for x + + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/Maverick.java b/src/main/java/mobac/program/atlascreators/Maverick.java new file mode 100644 index 0000000..05faf1a --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Maverick.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.program.annotations.AtlasCreatorName; + +/** + * Creates maps using the Maverick atlas format (Android + * application). + * + * Map/Atlas format see http://help.codesector.com/MapsCache + */ +@AtlasCreatorName("Maverick atlas format") +public class Maverick extends RMapsSQLite { + + public Maverick() { + super(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/MobileTrailExplorer.java b/src/main/java/mobac/program/atlascreators/MobileTrailExplorer.java new file mode 100644 index 0000000..9aebbc5 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/MobileTrailExplorer.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageType; +import mobac.utilities.Utilities; + +/** + * Creates maps using the Mobile Trail Explorer (MTE) / JTileDownloader atlas format (converted to PNG, size 256x256 + * pixel). + * + * Please note that this atlas format ignores the defined atlas structure. It uses a separate directory for each used + * map source and inside one directory for each zoom level. + */ +@AtlasCreatorName(value = "Mobile Trail Explorer", type = "MTE") +public class MobileTrailExplorer extends AtlasCreator { + + private File mapDir = null; + private File mapZoomDir = null; + + protected String appendFileExt = ""; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + String mapName = map.getMapSource().getName().replaceAll(" ", "_"); + mapDir = new File(atlasDir, mapName); + mapZoomDir = new File(mapDir, Integer.toString(map.getZoom())); + } + + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(mapDir); + Utilities.mkDir(mapZoomDir); + } catch (IOException e1) { + throw new MapCreationException(map, e1); + } + if (mapSource.getTileImageType() != TileImageType.PNG) + // If the tile image format is not png we have to convert it + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, TileImageFormat.PNG); + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + for (int x = xMin; x <= xMax; x++) { + File xDir = new File(mapZoomDir, Integer.toString(x)); + try { + Utilities.mkDir(xDir); + } catch (IOException e1) { + throw new MapCreationException(map, e1); + } + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + String tileFileName = x + "/" + y + ".png"; + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + File f = new File(mapZoomDir, tileFileName); + FileOutputStream out = new FileOutputStream(f); + try { + out.write(sourceTileData); + } finally { + Utilities.closeStream(out); + } + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/MobileTrailExplorerCache.java b/src/main/java/mobac/program/atlascreators/MobileTrailExplorerCache.java new file mode 100644 index 0000000..76ab203 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/MobileTrailExplorerCache.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageType; +import mobac.utilities.Utilities; + +/** + * Creates maps using the Mobile Trail Explorer (MTE) cache format. + * + * Please note that this atlas format ignores the defined atlas structure. + * + */ +@AtlasCreatorName(value = "Mobile Trail Explorer Cache", type = "MTECache") +public class MobileTrailExplorerCache extends AtlasCreator { + + protected DataOutputStream cacheOutStream = null; + protected long lastTileOffset = 0; + protected Set availableTileList = new HashSet(); + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, InterruptedException, + AtlasTestException { + super.startAtlasCreation(atlas, customAtlasDir); + File cacheFile = new File(atlasDir, "MTEFileCache"); + OutputStream out = new BufferedOutputStream(new FileOutputStream(cacheFile), 8216); + cacheOutStream = new DataOutputStream(out); + } + + @Override + public void abortAtlasCreation() throws IOException { + Utilities.closeStream(cacheOutStream); + super.abortAtlasCreation(); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + super.finishAtlasCreation(); + cacheOutStream.close(); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + } + + public void createMap() throws MapCreationException, InterruptedException { + if (mapSource.getTileImageType() != TileImageType.PNG) + // If the tile image format is not png we have to convert it + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, TileImageFormat.PNG); + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + String mapName = map.getMapSource().getName().replaceAll(" ", "_"); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) + writeTile(mapName, sourceTileData, x, y, zoom); + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + + protected boolean writeTile(String cache, byte[] tileData, int x, int y, int zoom) throws IOException { + String url = "not used"; + String cacheKey = cache + "-" + zoom + "-" + x + "-" + y; + + if (availableTileList.contains(cacheKey)) { + log.warn("Map tile already in cache: " + cacheKey + " -> ignoring"); + return false; + } + + cacheOutStream.writeInt(x); + cacheOutStream.writeInt(y); + cacheOutStream.writeInt(zoom); + + byte[] urlBytes = url.getBytes(); + cacheOutStream.writeShort(urlBytes.length); + cacheOutStream.write(urlBytes); + + byte[] keyBytes = cacheKey.getBytes(); + cacheOutStream.writeShort(keyBytes.length); + cacheOutStream.write(keyBytes); + cacheOutStream.writeLong(lastTileOffset); + + lastTileOffset += 12 + // x, y and z + 2 + urlBytes.length + // strings and their lengths + 2 + keyBytes.length + 8 + // tile offset (long) + 4 + // image byte array length (int) + tileData.length; + + cacheOutStream.writeInt(tileData.length); + cacheOutStream.write(tileData); + return true; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/NFComPass.java b/src/main/java/mobac/program/atlascreators/NFComPass.java new file mode 100644 index 0000000..980aae6 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/NFComPass.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileBuilder; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.CacheTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; +import mobac.program.tiledatawriter.TileImagePngDataWriter; +import mobac.utilities.Utilities; + +@AtlasCreatorName(value = "nfComPass") +@SupportedParameters(names = { Name.height, Name.width }) +public class NFComPass extends AtlasCreator { + + private File layerDir; + private File mapDir; + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + LayerInterface layer = map.getLayer(); + layerDir = new File(atlasDir, layer.getName()); + mapDir = new File(layerDir, map.getName()); + if (parameters == null) { + parameters = new TileImageParameters(64, 64, TileImageFormat.PNG); + } + } + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + } + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + if (layer.getMapCount() == 0) + return; + int lastZoom = layer.getMap(0).getZoom(); + Writer w = new BufferedWriter(new FileWriter(new File(atlasDir, "nfComPass.dat"), true)); + w.append("[" + layer.getName() + "]\r\n"); + w.append("SIZEXY = extern\r\n"); + w.append("MAPPATH =\r\n"); + w.append("VMAX = 160\r\n"); + w.append("WIDTH = 5\r\n"); + w.append("LASTZOOM = " + lastZoom + "\r\n\r\n"); + w.flush(); + w.close(); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDirs(mapDir); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + createKalFile(map); + CacheTileProvider ctp = new CacheTileProvider(mapDlTileProvider); + try { + mapDlTileProvider = ctp; + + MapTileBuilder mapTileBuilder = new MapTileBuilder(this, new TileImagePngDataWriter(), + new NFCompassTileWriter(), true); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + } finally { + ctp.cleanup(); + } + + } + + protected void createKalFile(MapInterface map) throws MapCreationException { + Writer w = null; + MapSpace mapSpace = map.getMapSource().getMapSpace(); + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize - 1, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize - 1, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + + try { + w = new BufferedWriter(new FileWriter(new File(mapDir, map.getName() + ".kal"), true)); + w.append("[" + map.getName() + "]\r\n"); + w.append(String.format("TILEXY = %dx%d\r\n", parameters.getWidth(), parameters.getHeight())); + w.append("X0LON = " + longitudeMin + "\r\n"); + w.append("Y0LAT = " + latitudeMax + "\r\n"); + w.append("X1LON = " + longitudeMax + "\r\n"); + w.append("Y1LAT = " + latitudeMin + "\r\n"); + w.append(String.format("SIZEXY = %dx%d\r\n", width, height)); + w.flush(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + Utilities.closeWriter(w); + } + + } + + public class NFCompassTileWriter implements MapTileWriter { + int tileHeight = 256; + int tileWidth = 256; + + int ff_x; + int ff_y; + + public NFCompassTileWriter() { + super(); + if (parameters != null) { + tileHeight = parameters.getHeight(); + tileWidth = parameters.getWidth(); + } + int highest_bit_x = Utilities.getHighestBitSet(tileWidth) + 2; + int highest_bit_y = Utilities.getHighestBitSet(tileHeight) + 2; + + ff_x = Integer.MAX_VALUE << (highest_bit_x); + ff_y = Integer.MAX_VALUE << (highest_bit_y); + } + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + int x = tilex * tileWidth; + int y = tiley * tileHeight; + String folderName = String.format("%dx%d", (x & ff_x), (y & ff_y)); + String tileFileName = String.format("%d_%d.png", x, y); + + File folder = new File(mapDir, folderName); + Utilities.mkDir(folder); + File f = new File(folder, tileFileName); + FileOutputStream out = new FileOutputStream(f); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + } + + public void finalizeMap() throws IOException { + + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/NaviComputer.java b/src/main/java/mobac/program/atlascreators/NaviComputer.java new file mode 100644 index 0000000..3f296fe --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/NaviComputer.java @@ -0,0 +1,235 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.util.Date; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.RequiresSQLite; +import mobac.program.model.Settings; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; +import mobac.utilities.jdbc.SQLiteLoader; + +@AtlasCreatorName("NaviComputer (NMAP)") +@SupportedParameters(names = { Name.format }) +public class NaviComputer extends AtlasCreator implements RequiresSQLite { + + private static final String NAVI_TABLES = "CREATE TABLE MapInfo (MapType TEXT, Zoom INTEGER NOT NULL, MinX INTEGER, MaxX INTEGER, MinY INTEGER, MaxY INTEGER);\n" + + "CREATE TABLE Tiles (id INTEGER NOT NULL PRIMARY KEY, X INTEGER NOT NULL, Y INTEGER NOT NULL, Zoom INTEGER NOT NULL);\n" + + "CREATE TABLE TilesData (id INTEGER NOT NULL PRIMARY KEY CONSTRAINT fk_Tiles_id REFERENCES Tiles(id) ON DELETE CASCADE, Tile BLOB NULL);\n" + + + "CREATE TRIGGER fkdc_TilesData_id_Tiles_id " + + "BEFORE DELETE ON Tiles " + + "FOR EACH ROW BEGIN " + + "DELETE FROM TilesData WHERE TilesData.id = OLD.id; " + + "END;\n" + + "CREATE TRIGGER fki_TilesData_id_Tiles_id " + + "BEFORE INSERT ON [TilesData] " + + "FOR EACH ROW BEGIN " + + "SELECT RAISE(ROLLBACK, 'insert on table TilesData violates foreign key constraint fki_TilesData_id_Tiles_id') " + + "WHERE (SELECT id FROM Tiles WHERE id = NEW.id) IS NULL; " + + "END;\n" + + "CREATE TRIGGER fku_TilesData_id_Tiles_id " + + "BEFORE UPDATE ON [TilesData] " + + "FOR EACH ROW BEGIN " + + "SELECT RAISE(ROLLBACK, 'update on table TilesData violates foreign key constraint fku_TilesData_id_Tiles_id') " + + "WHERE (SELECT id FROM Tiles WHERE id = NEW.id) IS NULL; " + + "END;\n" + + "CREATE INDEX IndexOfTiles ON Tiles (X, Y, Zoom);"; + + private static final String INSERT_TILES = "INSERT INTO Tiles (id,X,Y,Zoom) VALUES (?,?,?,?)"; + private static final String INSERT_TILES_DATA = "INSERT INTO TilesData (id,Tile) VALUES (?,?)"; + private static final String INSERT_MAP_INFO = "INSERT INTO MapInfo (MapType,Zoom,MinX,MaxX,MinY,MaxY) " + + "SELECT ?,Min(Zoom),Min(x),Max(x),Min(y),Max(y) FROM Tiles WHERE Zoom=?;"; + + private String databaseFile; + private int wmsTileCount = 1; + + private static final int COMMIT_RATE = 100; + private int tileCommitCounter = 0; + protected Connection conn = null; + private PreparedStatement prepTilesData = null, prepTiles = null; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + protected void testAtlas() throws AtlasTestException { + performTest_MaxMapZoom(18); + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, AtlasTestException, + InterruptedException { + if (customAtlasDir == null) + customAtlasDir = Settings.getInstance().getAtlasOutputDirectory(); + super.startAtlasCreation(atlas, customAtlasDir); + + databaseFile = getDatabaseFileName(); + log.debug("SQLite Database file: " + databaseFile); + + try { + SQLiteLoader.loadSQLite(); + } catch (SQLException e) { + throw new IOException(SQLiteLoader.getMsgSqliteMissing(), e); + } + try { + Utilities.mkDir(atlasDir); + openConnection(); + initializeDB(); + + prepTilesData = conn.prepareStatement(INSERT_TILES_DATA); + prepTiles = conn.prepareStatement(INSERT_TILES); + + } catch (SQLException e) { + throw new AtlasTestException("Error creating SQL database \"" + databaseFile + "\": " + e.getMessage(), e); + } + + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + if (parameters != null) + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + + createTiles(); + + } + + private void openConnection() throws SQLException { + if (conn == null || conn.isClosed()) { + String url = "jdbc:sqlite:/" + this.databaseFile; + conn = DriverManager.getConnection(url); + } + } + + @Override + public void abortAtlasCreation() throws IOException { + SQLiteLoader.closeConnection(conn); + conn = null; + super.abortAtlasCreation(); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + String mapName = mapSource.getName(); + try { + PreparedStatement prepStat = conn.prepareStatement(INSERT_MAP_INFO); + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery("SELECT Distinct Zoom From Tiles"); + while (rs.next()) { + int zoom = rs.getInt(1); + prepStat.setString(1, mapName); + prepStat.setInt(2, zoom); + prepStat.execute(); + } + conn.commit(); + prepStat.close(); + } catch (SQLException e) { + log.error(e.getMessage()); + } finally { + SQLiteLoader.closeConnection(conn); + conn = null; + } + super.finishAtlasCreation(); + } + + protected void initializeDB() throws SQLException { + Statement stat = conn.createStatement(); + String[] sqlList = NAVI_TABLES.split("\\n"); + for (String sql : sqlList) + stat.addBatch(sql); + stat.executeBatch(); + stat.close(); + log.debug("Database initialization complete: tables, trigges and index created"); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + int maxMapProgress = (xMax - xMin + 1) * (yMax - yMin + 1); + atlasProgress.initMapCreation(maxMapProgress); + try { + tileCommitCounter = 0; + conn.setAutoCommit(false); + Runtime r = Runtime.getRuntime(); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + tileCommitCounter++; + writeTile(wmsTileCount++, x, y, zoom, sourceTileData); + + long heapAvailable = r.maxMemory() - r.totalMemory() + r.freeMemory(); + if (heapAvailable < HEAP_MIN || tileCommitCounter > COMMIT_RATE) { + conn.commit(); + tileCommitCounter = 0; + System.gc(); + } + } + } catch (IOException e) { + throw new MapCreationException(map, e); + } + } + } + conn.commit(); + atlasProgress.setMapCreationProgress(maxMapProgress); + } catch (SQLException e) { + throw new MapCreationException(map, e); + } + } + + protected void writeTile(int id, int x, int y, int z, byte[] tileData) throws SQLException, IOException { + prepTiles.setInt(1, id); + prepTiles.setInt(2, x); + prepTiles.setInt(3, y); + prepTiles.setInt(4, z); + prepTiles.execute(); + prepTilesData.setInt(1, id); + prepTilesData.setBytes(2, tileData); + prepTilesData.execute(); + } + + protected String getDatabaseFileName() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss_"); + databaseFile = new File(atlasDir, sdf.format(new Date()) + atlas.getName() + ".nmap").getAbsolutePath(); + return databaseFile; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/OSMAND.java b/src/main/java/mobac/program/atlascreators/OSMAND.java new file mode 100644 index 0000000..58defc9 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OSMAND.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.program.annotations.AtlasCreatorName; + +@AtlasCreatorName("OSMAND tile storage") +public class OSMAND extends OSMTracker { + public OSMAND() { + super(); + tileFileNamePattern += ".tile"; + } +} diff --git a/src/main/java/mobac/program/atlascreators/OSMAND_SQlite.java b/src/main/java/mobac/program/atlascreators/OSMAND_SQlite.java new file mode 100644 index 0000000..291de6e --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OSMAND_SQlite.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.interfaces.MapSource; + +@AtlasCreatorName(value = "OSMAND SQLite", type = "OSMAND_SQlite") +public class OSMAND_SQlite extends RMapsSQLite { + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/OSMTracker.java b/src/main/java/mobac/program/atlascreators/OSMTracker.java new file mode 100644 index 0000000..fb9eb01 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OSMTracker.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; + +/** + * Creates maps identical to the atlas format used by OSMTracker. + * + * Please note that this atlas format ignores the defined atlas structure. It uses a separate directory for each used + * map source and inside one directory for each zoom level. + */ +@AtlasCreatorName("OSMTracker tile storage") +@SupportedParameters(names = { Name.format }) +public class OSMTracker extends AtlasCreator { + + protected String tileFileNamePattern = "%d/%d/%d.%s"; + + protected File mapDir = null; + + protected String tileType = null; + + protected MapTileWriter mapTileWriter = null; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + mapDir = new File(atlasDir, map.getMapSource().getName()); + tileType = mapSource.getTileImageType().getFileExt(); + if (parameters != null) { + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + tileType = parameters.getFormat().getFileExt(); + } + } + + public void createMap() throws MapCreationException, InterruptedException { + // This means there should not be any resizing of the tiles. + if (mapTileWriter == null) + mapTileWriter = new OSMTileWriter(); + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) + mapTileWriter.writeTile(x, y, tileType, sourceTileData); + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + + protected class OSMTileWriter implements MapTileWriter { + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + File file = new File(mapDir, String.format(tileFileNamePattern, zoom, tilex, tiley, tileType)); + writeTile(file, tileData); + } + + protected void writeTile(File file, byte[] tileData) throws IOException { + Utilities.mkDirs(file.getParentFile()); + FileOutputStream out = new FileOutputStream(file); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + } + + public void finalizeMap() throws IOException { + // Nothing to do + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/OruxMaps.java b/src/main/java/mobac/program/atlascreators/OruxMaps.java new file mode 100644 index 0000000..f572785 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OruxMaps.java @@ -0,0 +1,318 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Locale; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileBuilder; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.CacheTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; + +/** + * Creates maps using the OruxMaps (Android) atlas format. + * + * @author orux + */ +@AtlasCreatorName("OruxMaps") +@SupportedParameters(names = { Name.format }) +public class OruxMaps extends AtlasCreator { + + // Calibration file extension + protected static final String ORUXMAPS_EXT = ".otrk2.xml"; + + // OruxMaps tile size + protected static final int TILE_SIZE = 512; + + protected String calVersionCode; + + // OruxMaps background color + protected static final Color BG_COLOR = new Color(0xcb, 0xd3, 0xf3); + + // Each layer is a Main map for OruxMaps + protected File oruxMapsMainDir; + + // Each map is a Layer map for OruxMaps + protected File oruxMapsLayerDir; + + // Images directory for each map + protected File oruxMapsImagesDir; + + protected LayerInterface currentLayer; + + // We need to override the map name, All maps must have the same prefix (layer name) + protected String mapName; + + public OruxMaps() { + super(); + calVersionCode = "2.1"; + } + + @Override + public boolean testMapSource(MapSource mapSource) { + + return (mapSource.getMapSpace() instanceof MercatorPower2MapSpace); + } + + @Override + protected void testAtlas() throws AtlasTestException { + + for (LayerInterface layer : atlas) { + int cont = layer.getMapCount(); + for (int i = 0; i < cont; i++){ + MapInterface currMap = layer.getMap(i); + int currZoomLevel = currMap.getZoom(); + for (int j = i + 1; j < cont; j++){ + MapInterface nextMap = layer.getMap(j); + int nextZoomLevel = nextMap.getZoom(); + if (currZoomLevel == nextZoomLevel) + throw new AtlasTestException( + "Unable to create a map with more than a layer with the same zoom level: " + + currMap + " & " + nextMap + + "\nPossible causes:\n" + + "You are combining several layers (using drag & drop in 'Atlas Content')\n" + + "You are creating a large map, and you have not selected the maximum value in 'Settings - Map size'"); + } + } + } + } + + + /* + * @see mobac.program.atlascreators.AtlasCreator#initLayerCreation(mobac.program .interfaces.LayerInterface) + */ + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + + super.initLayerCreation(layer); + currentLayer = layer; + oruxMapsMainDir = new File(atlasDir, layer.getName()); + Utilities.mkDirs(oruxMapsMainDir); + + } + + @Override + public void finishLayerCreation() throws IOException { + + super.finishLayerCreation(); + writeMainOtrk2File(currentLayer.getName()); + } + + /* + * (non-Javadoc) + * + * @see mobac.program.atlascreators.AtlasCreator#initializeMap(mobac.program. interfaces.MapInterface, + * mobac.utilities.tar.TarIndex) + */ + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + + super.initializeMap(map, mapTileProvider); + // OruxMaps default image format, jpeg90; always TILE_SIZE=512; + if (parameters == null) + parameters = new TileImageParameters(TILE_SIZE, TILE_SIZE, TileImageFormat.JPEG90); + else + parameters = new TileImageParameters(TILE_SIZE, TILE_SIZE, parameters.getFormat()); + mapName = String.format("%s %02d", currentLayer.getName(), map.getZoom()); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + + oruxMapsLayerDir = new File(oruxMapsMainDir, mapName); + oruxMapsImagesDir = new File(oruxMapsLayerDir, "set"); + try { + Utilities.mkDir(oruxMapsLayerDir); + Utilities.mkDir(oruxMapsImagesDir); + writeOtrk2File(); + createTiles(); + } catch (InterruptedException e) { + // User has aborted process + return; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + protected void createTiles() throws InterruptedException, MapCreationException { + + CacheTileProvider ctp = new CacheTileProvider(mapDlTileProvider); + try { + mapDlTileProvider = ctp; + + OruxMapTileBuilder mapTileBuilder = new OruxMapTileBuilder(this, new OruxMapTileWriter()); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + } finally { + ctp.cleanup(); + } + } + + /** + * Main calibration file + * + * @param name + */ + private void writeMainOtrk2File(String name) { + + OutputStreamWriter writer; + FileOutputStream otrk2FileStream = null; + File otrk2 = new File(oruxMapsMainDir, name + ORUXMAPS_EXT); + try { + writer = new OutputStreamWriter(new FileOutputStream(otrk2), Charsets.UTF_8); + writer.append("\n"); + writer.append("\n"); + writer.append("\n"); + writer.append("\n"); + + writer.append(appendMapContent()); + + writer.append("\n"); + writer.append("\n"); + writer.flush(); + } catch (IOException e) { + log.error("", e); + } finally { + Utilities.closeStream(otrk2FileStream); + } + } + + protected String appendMapContent() { + return ""; + } + + /** + * Main calibration file per layer + * + */ + protected void writeOtrk2File() { + + FileOutputStream stream = null; + OutputStreamWriter mapWriter; + File otrk2File = new File(oruxMapsLayerDir, mapName + ORUXMAPS_EXT); + try { + stream = new FileOutputStream(otrk2File); + mapWriter = new OutputStreamWriter(stream, "UTF8"); + mapWriter.append(prepareOtrk2File()); + mapWriter.flush(); + } catch (IOException e) { + log.error("", e); + } finally { + Utilities.closeStream(stream); + } + } + + /** + * Main calibration file per layer + * + */ + protected String prepareOtrk2File() { + + StringBuilder mapWriter = new StringBuilder(); + MapSpace mapSpace = mapSource.getMapSpace(); + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + mapWriter.append("\n"); + mapWriter.append("\n"); + mapWriter.append("\n"); + + // convert ampersands and others + String mapFileName = mapName; + mapFileName = mapFileName.replaceAll("&", "&"); + mapFileName = mapFileName.replaceAll("<", "<"); + mapFileName = mapFileName.replaceAll(">", ">"); + mapFileName = mapFileName.replaceAll("\"", """); + mapFileName = mapFileName.replaceAll("'", "'"); + + int mapWidth = (xMax - xMin + 1) * tileSize; + int mapHeight = (yMax - yMin + 1) * tileSize; + int numXimg = (mapWidth + TILE_SIZE - 1) / TILE_SIZE; + int numYimg = (mapHeight + TILE_SIZE - 1) / TILE_SIZE; + mapWriter.append("\n"); + mapWriter.append("\n"); + mapWriter.append("\n"); + mapWriter.append("\n"); + String cb = "\n"; + mapWriter.append(String.format(Locale.ENGLISH, cb, "TL", longitudeMin, latitudeMax)); + mapWriter.append(String.format(Locale.ENGLISH, cb, "BR", longitudeMax, latitudeMin)); + mapWriter.append(String.format(Locale.ENGLISH, cb, "TR", longitudeMax, latitudeMax)); + mapWriter.append(String.format(Locale.ENGLISH, cb, "BL", longitudeMin, latitudeMin)); + mapWriter.append("\n"); + mapWriter.append("\n"); + mapWriter.append("\n"); + return mapWriter.toString(); + } + + protected class OruxMapTileBuilder extends MapTileBuilder { + + public OruxMapTileBuilder(AtlasCreator atlasCreator, MapTileWriter mapTileWriter) { + super(atlasCreator, mapTileWriter, false); + } + + @Override + protected void prepareTile(Graphics2D graphics) { + graphics.setColor(BG_COLOR); + graphics.fillRect(0, 0, TILE_SIZE, TILE_SIZE); + } + + } + + private class OruxMapTileWriter implements MapTileWriter { + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + String tileFileName = String.format("%s_%d_%d.omc2", mapName, tilex, tiley); + FileOutputStream out = new FileOutputStream(new File(oruxMapsImagesDir, tileFileName)); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + } + + public void finalizeMap() { + // Nothing to do + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/OruxMapsSqlite.java b/src/main/java/mobac/program/atlascreators/OruxMapsSqlite.java new file mode 100644 index 0000000..5c8f514 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OruxMapsSqlite.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Locale; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.CacheTileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.RequiresSQLite; +import mobac.utilities.jdbc.SQLiteLoader; + +/** + * Creates maps using the OruxMaps (Android) atlas format. + * + * @author orux Some code based on BigPlanetSql atlascreator + */ +@AtlasCreatorName("OruxMaps Sqlite") +public class OruxMapsSqlite extends OruxMaps implements RequiresSQLite { + + private static final String TABLE_TILES_DDL = "CREATE TABLE IF NOT EXISTS tiles (x int, y int, z int, image blob, PRIMARY KEY (x,y,z))"; + private static final String INDEX_DDL = "CREATE INDEX IF NOT EXISTS IND on tiles (x,y,z)"; + private static final String INSERT_SQL = "INSERT or IGNORE INTO tiles (x,y,z,image) VALUES (?,?,?,?)"; + private static final String TABLE_ANDROID_METADATA_DDL = "CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)"; + + private static final String DATABASE_FILENAME = "OruxMapsImages.db"; + + private String databaseFile; + + private Connection conn = null; + private PreparedStatement prepStmt; + + private StringBuilder otrk2MapsContent; + + public OruxMapsSqlite() { + super(); + SQLiteLoader.loadSQLiteOrShowError(); + calVersionCode = "3.0"; + } + + /* + * (non-Javadoc) + * + * @see mobac.program.atlascreators.AtlasCreator#startAtlasCreation(mobac.program.interfaces.AtlasInterface) + */ + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws AtlasTestException, IOException, + InterruptedException { + super.startAtlasCreation(atlas, customAtlasDir); + try { + SQLiteLoader.loadSQLite(); + } catch (SQLException e) { + throw new AtlasTestException(SQLiteLoader.getMsgSqliteMissing(), e); + } + } + + /* + * @see mobac.program.atlascreators.AtlasCreator#initLayerCreation(mobac.program .interfaces.LayerInterface) + */ + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + + super.initLayerCreation(layer); + databaseFile = new File(oruxMapsMainDir, DATABASE_FILENAME).getAbsolutePath(); + log.debug("SQLite Database file: " + databaseFile); + otrk2MapsContent = new StringBuilder(); + try { + conn = getConnection(); + initializeDB(); + } catch (SQLException e) { + throw new IOException(e); + } finally { + closeConnection(); + } + + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + super.finishAtlasCreation(); + closeConnection(); + } + + @Override + public void abortAtlasCreation() throws IOException { + super.abortAtlasCreation(); + log.debug("Aborting OruxMapsSqlite atlas creation"); + closeConnection(); + } + + private Connection getConnection() throws SQLException { + String url = "jdbc:sqlite:/" + this.databaseFile; + Connection conn = DriverManager.getConnection(url); + return conn; + } + + private void closeConnection() { + try { + if (conn != null) { + log.debug("Closing database connection"); + conn.close(); + } + } catch (Exception e) { + } + conn = null; + } + + private void initializeDB() throws SQLException { + + Statement stat = conn.createStatement(); + stat.executeUpdate(TABLE_TILES_DDL); + stat.executeUpdate(INDEX_DDL); + stat.executeUpdate(TABLE_ANDROID_METADATA_DDL); + stat.executeUpdate("INSERT INTO android_metadata VALUES ('" + Locale.getDefault().toString() + "')"); + stat.close(); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + + otrk2MapsContent.append(prepareOtrk2File()); + + try { + conn = getConnection(); + conn.setAutoCommit(false); + prepStmt = conn.prepareStatement(INSERT_SQL); + createTiles(); + conn.close(); + } catch (InterruptedException e) { + // User has aborted process + return; + } catch (MapCreationException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + @Override + protected void createTiles() throws InterruptedException, MapCreationException { + + CacheTileProvider ctp = new CacheTileProvider(mapDlTileProvider); + try { + mapDlTileProvider = ctp; + MapTileWriter mtw = new OruxMapTileWriterDB(); + OruxMapTileBuilder mapTileBuilder = new OruxMapTileBuilder(this, mtw); + // customTileCount = mapTileBuilder.getCustomTileCount(); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + mtw.finalizeMap(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + ctp.cleanup(); + } + } + + @Override + protected String appendMapContent() { + return otrk2MapsContent.toString(); + } + + private class OruxMapTileWriterDB implements MapTileWriter { + + private static final int MAX_BATCH_SIZE = 1000; + + private int tileCounter = 0; + private Runtime r = Runtime.getRuntime(); + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + + try { + prepStmt.setInt(1, tilex); + prepStmt.setInt(2, tiley); + prepStmt.setInt(3, zoom); + prepStmt.setBytes(4, tileData); + prepStmt.addBatch(); + long heapAvailable = r.maxMemory() - r.totalMemory() + r.freeMemory(); + + tileCounter++; + if (heapAvailable < HEAP_MIN || tileCounter > MAX_BATCH_SIZE) { + commit(); + } + } catch (SQLException e) { + throw new IOException(e); + } + + } + + private void commit() throws SQLException { + prepStmt.executeBatch(); + prepStmt.clearBatch(); + atlasProgress.incMapCreationProgress(tileCounter); + tileCounter = 0; + conn.commit(); + System.gc(); + } + + public void finalizeMap() throws IOException { + try { + commit(); + } catch (SQLException e) { + throw new IOException(e); + } + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/Osmdroid.java b/src/main/java/mobac/program/atlascreators/Osmdroid.java new file mode 100644 index 0000000..42e64dd --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Osmdroid.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.model.Settings; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ZipStoreOutputStream; + +/** + * + * http://sourceforge.net/p/mobac/feature-requests/162/ + */ +@AtlasCreatorName("Osmdroid ZIP") +public class Osmdroid extends OSMTracker { + + protected ZipStoreOutputStream zipStream = null; + protected String currentMapStoreName = null; + + public void createMap() throws MapCreationException, InterruptedException { + createTiles(); + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws AtlasTestException, IOException, + InterruptedException { + if (customAtlasDir == null) + customAtlasDir = Settings.getInstance().getAtlasOutputDirectory(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss"); + String atlasDirName = atlas.getName() + "_" + sdf.format(new Date()); + super.startAtlasCreation(atlas, customAtlasDir); + zipStream = new ZipStoreOutputStream(new File(atlasDir, atlasDirName + ".zip")); + mapTileWriter = new OSMDroidTileWriter(); + } + + @Override + public void abortAtlasCreation() throws IOException { + Utilities.closeStream(zipStream); + super.abortAtlasCreation(); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + Utilities.closeStream(zipStream); + super.finishAtlasCreation(); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + currentMapStoreName = map.getMapSource().getName(); + if (currentMapStoreName.equals("TilesAtHome")) + currentMapStoreName = "Osmarender"; + else if (currentMapStoreName.equals("OSM Cycle Map")) + currentMapStoreName = "CycleMap"; + } + + private class OSMDroidTileWriter implements MapTileWriter { + + public void finalizeMap() throws IOException { + } + + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + String tileName = currentMapStoreName + "/" + + String.format(tileFileNamePattern, zoom, tilex, tiley, tileType); + zipStream.writeStoredEntry(tileName, tileData); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/OsmdroidGEMF.java b/src/main/java/mobac/program/atlascreators/OsmdroidGEMF.java new file mode 100644 index 0000000..61eb4d1 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OsmdroidGEMF.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * + */ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.impl.gemf.GEMFFileCreator; + +/** + * AtlasCreator implementation to create a GEMF archive file. For details about the format, please see the link in + * {@link GEMFFileCreator}. + * + * @author M. Reiter + * + */ +@AtlasCreatorName("Osmdroid GEMF") +public class OsmdroidGEMF extends OSMTracker { + + private static final String GEMF_FILE_EXTENSION = ".gemf"; + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + List tileFolders = new LinkedList(); + tileFolders.add(mapDir); + + String gemfLocation = new File(atlasDir, atlas.getName() + GEMF_FILE_EXTENSION).toString(); + + new GEMFFileCreator(gemfLocation, tileFolders, log); + + super.finishAtlasCreation(); + } + +} \ No newline at end of file diff --git a/src/main/java/mobac/program/atlascreators/OsmdroidSQLite.java b/src/main/java/mobac/program/atlascreators/OsmdroidSQLite.java new file mode 100644 index 0000000..a76d3ef --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/OsmdroidSQLite.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.interfaces.RequiresSQLite; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.jdbc.SQLiteLoader; + +/** + * http://sourceforge.net/p/mobac/patches/22/ + */ +@AtlasCreatorName("Osmdroid SQLite") +@SupportedParameters(names = { Name.format }) +public class OsmdroidSQLite extends AtlasCreator implements RequiresSQLite { + + private static final int MAX_BATCH_SIZE = 1000; + + protected Connection conn = null; + + public OsmdroidSQLite() { + super(); + SQLiteLoader.loadSQLiteOrShowError(); + } + + @Override + public boolean testMapSource(MapSource mapSource) { + return mapSource.getMapSpace().getProjectionCategory().equals(ProjectionCategory.SPHERE); + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, AtlasTestException, + InterruptedException { + super.startAtlasCreation(atlas, customAtlasDir); + String databaseFile = new File(atlasDir, atlas.getName() + ".sqlite").getAbsolutePath(); + try { + SQLiteLoader.loadSQLite(); + } catch (SQLException e) { + throw new AtlasTestException(SQLiteLoader.getMsgSqliteMissing()); + } + try { + conn = DriverManager.getConnection("jdbc:sqlite:" + databaseFile); + Statement stat = conn.createStatement(); + stat.executeUpdate("CREATE TABLE IF NOT EXISTS tiles (key INTEGER PRIMARY KEY, provider TEXT, tile BLOB)"); + stat.close(); + } catch (SQLException e) { + throw new IOException("Error creating SQL database \"" + databaseFile + "\": " + e.getMessage(), e); + } + log.debug("SQLite Database file: " + databaseFile); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + if (parameters != null) + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + String provider = map.getMapSource().getName(); + int maxMapProgress = 2 * (xMax - xMin + 1) * (yMax - yMin + 1); + atlasProgress.initMapCreation(maxMapProgress); + conn.setAutoCommit(false); + int batchTileCount = 0; + + ImageIO.setUseCache(false); + PreparedStatement prep = conn.prepareStatement("INSERT or REPLACE INTO tiles VALUES (?, ?, ?);"); + Runtime r = Runtime.getRuntime(); + long heapMaxSize = r.maxMemory(); + + for (long x = xMin; x <= xMax; x++) + for (long y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + byte[] sourceTileData = mapDlTileProvider.getTileData((int) x, (int) y); + if (sourceTileData != null) { + long z = zoom; + long index = (((z << z) + x) << z) + y; + prep.setLong(1, index); + prep.setString(2, provider); + prep.setBytes(3, sourceTileData); + prep.addBatch(); + + long heapAvailable = heapMaxSize - r.totalMemory() + r.freeMemory(); + + if ((heapAvailable < HEAP_MIN) || (batchTileCount >= MAX_BATCH_SIZE)) { + log.trace("Executing batch containing " + batchTileCount + " tiles"); + prep.executeBatch(); + prep.clearBatch(); + System.gc(); + conn.commit(); + atlasProgress.incMapCreationProgress(batchTileCount); + batchTileCount = 0; + } + + } + } + prep.executeBatch(); + conn.setAutoCommit(true); + atlasProgress.setMapCreationProgress(maxMapProgress); + } catch (SQLException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + + @Override + public void abortAtlasCreation() throws IOException { + SQLiteLoader.closeConnection(conn); + conn = null; + super.abortAtlasCreation(); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + SQLiteLoader.closeConnection(conn); + conn = null; + super.finishAtlasCreation(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/Ozi.java b/src/main/java/mobac/program/atlascreators/Ozi.java new file mode 100644 index 0000000..006030c --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Ozi.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Locale; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.utilities.Utilities; +import mobac.utilities.geo.GeoUtils; +import mobac.utilities.imageio.PngXxlWriter; + +@AtlasCreatorName(value = "OziExplorer (PNG & MAP)", type = "OziPng") +@SupportedParameters(names = {}) +public class Ozi extends AtlasCreator { + + protected File layerDir = null; + protected String mapName = null; + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + // TODO supports Mercator ellipsoid? + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + layerDir = new File(atlasDir, map.getLayer().getName()); + mapName = map.getName(); + } + + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(layerDir); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + createTiles(); + writeMapFile(); + } + + protected void writeMapFile() { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, mapName + ".map")); + writeMapFile(map.getName() + ".png", fout); + } catch (Exception e) { + log.error("", e); + } finally { + Utilities.closeStream(fout); + } + } + + protected void writeMapFile(String imageFileName, OutputStream stream) throws IOException { + log.trace("Writing map file"); + OutputStreamWriter mapWriter = new OutputStreamWriter(stream, TEXT_FILE_CHARSET); + + MapSpace mapSpace = mapSource.getMapSpace(); + + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize - 1, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize - 1, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + + // Calculate the 100 pixel scale here for MM1B + // Supplied by MrPete, based on suggestion from Des Newman, author of OziExplorer + // Set latitude to midpoint (maxLat+minLat/2) + double midLat = Math.toRadians((latitudeMax + latitudeMin) / 2.0); + + // Calculate 50 pixel Longitude for interpolation: Lon50 = 50/(ImagePixelWidth) * abs(maxLon - minLon) + double rlonMax = Math.toRadians(longitudeMax); + double rlonMin = Math.toRadians(longitudeMin); + double Lon50 = (50.0 / width) * Math.abs(rlonMax - rlonMin); + + // Calculate midpoint Lon: midLon = (maxLon+minLon)/2 + double midLon = (rlonMax + rlonMin) / 2.0; + + // Set lonW and lonE to midpoint +/1 50 pixels: lonW = midLon - Lon50; lonE = midLon+Lon50 + double lonW = midLon - Lon50; + double lonE = midLon + Lon50; + + // Now do the calculation: + // d=2*asin(sqrt((sin((lat1-lat2)/2))^2 + cos(lat1)*cos(lat2)*(sin((lon1-lon2)/2))^2)) + // d=2*asin(sqrt(0 + cos(lat1)*cos(lat2)*(sin((lon1-lon2)/2))^2)) + double mDistA = Math.cos(midLat); + double mDistB = Math.sin((lonW - lonE) / 2.0); + double mDist = 2.0 * Math.asin(Math.sqrt(mDistA * mDistA * mDistB * mDistB)); + + // For the final scaling, convert to distance in meters (multiply by earth radius), + // then simply divide by 100 (100 pixels between the two reference points) + // We're using the polar radius, as this gives results very close to OziExplorer + double mm1b = 6399592 * mDist / 100.0; + + mapWriter.write(prepareMapString(imageFileName, longitudeMin, longitudeMax, latitudeMin, latitudeMax, width, + height, mm1b)); + mapWriter.flush(); + } + + protected String prepareMapString(String fileName, double longitudeMin, double longitudeMax, double latitudeMin, + double latitudeMax, int width, int height, double mm1b) { + + StringBuffer sbMap = new StringBuffer(); + + sbMap.append("OziExplorer Map Data File Version 2.2\r\n"); + sbMap.append(fileName + "\r\n"); + sbMap.append(fileName + "\r\n"); + sbMap.append("1 ,Map Code,\r\n"); + sbMap.append("WGS 84,WGS 84, 0.0000, 0.0000,WGS 84\r\n"); + sbMap.append("Reserved 1\r\n"); + sbMap.append("Reserved 2\r\n"); + sbMap.append("Magnetic Variation,,,E\r\n"); + sbMap.append("Map Projection,Mercator,PolyCal,No," + "AutoCalOnly,No,BSBUseWPX,No\r\n"); + + String latMax = GeoUtils.getDegMinFormat(latitudeMax, true); + String latMin = GeoUtils.getDegMinFormat(latitudeMin, true); + String lonMax = GeoUtils.getDegMinFormat(longitudeMax, false); + String lonMin = GeoUtils.getDegMinFormat(longitudeMin, false); + + String pointLine = "Point%02d,xy, %4s, %4s,in, deg, %1s, %1s, grid, , , ,N\r\n"; + + sbMap.append(String.format(pointLine, 1, 0, 0, latMax, lonMin)); + sbMap.append(String.format(pointLine, 2, width - 1, 0, latMax, lonMax)); + sbMap.append(String.format(pointLine, 3, width - 1, height - 1, latMin, lonMax)); + sbMap.append(String.format(pointLine, 4, 0, height - 1, latMin, lonMin)); + + for (int i = 5; i <= 30; i++) { + String s = String.format(pointLine, i, "", "", "", ""); + sbMap.append(s); + } + sbMap.append("Projection Setup,,,,,,,,,,\r\n"); + sbMap.append("Map Feature = MF ; Map Comment = MC These follow if they exist\r\n"); + sbMap.append("Track File = TF These follow if they exist\r\n"); + sbMap.append("Moving Map Parameters = MM? These follow if they exist\r\n"); + + sbMap.append("MM0,Yes\r\n"); + sbMap.append("MMPNUM,4\r\n"); + + String mmpxLine = "MMPXY, %d, %5d, %5d\r\n"; + + sbMap.append(String.format(mmpxLine, 1, 0, 0)); + sbMap.append(String.format(mmpxLine, 2, width - 1, 0)); + sbMap.append(String.format(mmpxLine, 3, width - 1, height - 1)); + sbMap.append(String.format(mmpxLine, 4, 0, height - 1)); + + String mpllLine = "MMPLL, %d, %2.6f, %2.6f\r\n"; + + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 1, longitudeMin, latitudeMax)); + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 2, longitudeMax, latitudeMax)); + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 3, longitudeMax, latitudeMin)); + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 4, longitudeMin, latitudeMin)); + + sbMap.append("MOP,Map Open Position,0,0\r\n"); + + sbMap.append(String.format(Locale.ENGLISH, "MM1B, %2.6f\r\n", mm1b)); + + sbMap.append("IWH,Map Image Width/Height, " + width + ", " + height + "\r\n"); + + return sbMap.toString(); + } + + /** + * Writes the large picture (tile) line by line. Each line has the full width of the map and the height of one tile + * (256 pixels). + */ + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + int tileLineHeight = tileSize; + + FileOutputStream fileOs = null; + Color backgroundColor = mapSource.getBackgroundColor(); + try { + fileOs = new FileOutputStream(new File(layerDir, mapName + ".png")); + PngXxlWriter pngWriter = new PngXxlWriter(width, height, fileOs); + + for (int y = yMin; y <= yMax; y++) { + BufferedImage lineImage = new BufferedImage(width, tileLineHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = lineImage.createGraphics(); + try { + graphics.setColor(backgroundColor); + graphics.fillRect(0, 0, width, tileLineHeight); + int lineX = 0; + for (int x = xMin; x <= xMax; x++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + BufferedImage tile = mapDlTileProvider.getTileImage(x, y); + if (tile != null) + graphics.drawImage(tile, lineX, 0, backgroundColor, null); + lineX += tileSize; + } + } finally { + graphics.dispose(); + } + pngWriter.writeTileLine(lineImage); + } + pngWriter.finish(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + Utilities.closeStream(fileOs); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/PNGWorldfile.java b/src/main/java/mobac/program/atlascreators/PNGWorldfile.java new file mode 100644 index 0000000..4386cb2 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/PNGWorldfile.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Locale; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.utilities.Utilities; + +/** + * http://sourceforge.net/p/mobac/feature-requests/136/ + */ +@AtlasCreatorName("PNG + Worldfile (PNG & PGW)") +public class PNGWorldfile extends Glopus { + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(layerDir); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + createTiles(); + writeWorldFile(); + writeProjectionFile(); + } + + /** + * http://en.wikipedia.org/wiki/World_file + * + *
+	 * Format of Worldfile: 
+	 * 			   0.000085830078125  (size of pixel in x direction)                              =(east-west)/image width
+	 * 			   0.000000000000     (rotation term for row)
+	 * 			   0.000000000000     (rotation term for column)
+	 * 			   -0.00006612890625  (size of pixel in y direction)                              =-(north-south)/image height
+	 * 			   -106.54541         (x coordinate of centre of upper left pixel in map units)   =west
+	 * 			   39.622615          (y coordinate of centre of upper left pixel in map units)   =north
+	 * 
+ */ + private void writeWorldFile() throws MapCreationException { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, mapName + ".pgw")); + OutputStreamWriter mapWriter = new OutputStreamWriter(fout, TEXT_FILE_CHARSET); + + // MapSpace mapSpace = mapSource.getMapSpace(); + + double worldSize = 2 * Math.PI * 6378137; + double maxTiles = 1 << zoom; + double pixelSize = worldSize / (maxTiles * tileSize); + + mapWriter.write(String.format(Locale.ENGLISH, "%.15f\n", pixelSize)); + mapWriter.write("0.0\n"); + mapWriter.write("0.0\n"); + mapWriter.write(String.format(Locale.ENGLISH, "%.15f\n", -pixelSize)); + + double xMin1 = worldSize * (xMin / maxTiles - 0.5); + double yMax1 = worldSize * (0.5 - yMin / maxTiles); + + mapWriter.write(String.format(Locale.ENGLISH, "%.7f\n", xMin1 + 0.5 * pixelSize)); + mapWriter.write(String.format(Locale.ENGLISH, "%.7f\n", yMax1 - 0.5 * pixelSize)); + + mapWriter.flush(); + mapWriter.close(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + Utilities.closeStream(fout); + } + } + + private void writeProjectionFile() throws MapCreationException { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, mapName + ".png.aux.xml")); + OutputStreamWriter writer = new OutputStreamWriter(fout, TEXT_FILE_CHARSET); + + writer.write("PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84"," + + "DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563," + + "AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]]," + + "PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]," + + "AUTHORITY["EPSG","4326"]]," + + "PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0]," + + "PARAMETER["scale_factor",1],PARAMETER["false_easting",0]," + + "PARAMETER["false_northing",0],UNIT["metre",1," + + "AUTHORITY["EPSG","9001"]]," + + "EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"]," + + "AUTHORITY["EPSG","3857"]]"); + + writer.flush(); + writer.close(); + } catch (IOException e) { + throw new MapCreationException(map, e); + } finally { + Utilities.closeStream(fout); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/PaperAtlas.java b/src/main/java/mobac/program/atlascreators/PaperAtlas.java new file mode 100644 index 0000000..a3291f1 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/PaperAtlas.java @@ -0,0 +1,339 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.JComponent; +import javax.swing.JPanel; + +import mobac.exceptions.MapCreationException; +import mobac.gui.mapview.ScaleBar; +import mobac.gui.mapview.WgsGrid; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.model.Settings; +import mobac.program.model.SettingsPaperAtlas; +import mobac.program.model.SettingsWgsGrid; +import mobac.program.model.UnitSystem; +import mobac.utilities.Utilities; + +public abstract class PaperAtlas extends AtlasCreator { + + private static final Font PAGE_NUMBER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 24), LABEL_FONT = new Font( + Font.SANS_SERIF, Font.BOLD, 12); + + private static final Color LABEL_BACKGROUND = new Color(0, 127, 0), LABEL_FOREGROUND = Color.WHITE, + IMAGE_BACKGROUND = new Color(0, 0, 0, 0); + + private static final String UP = " \u2191", DOWN = " \u2193"; + + protected static class Page { + protected final BufferedImage image; + protected final int number, width, height; + + protected Page(int number, BufferedImage image) { + this.number = number; + this.image = image; + this.width = image.getWidth(); + this.height = image.getHeight(); + } + } + + private final JComponent dummy = new JPanel(); + private final WgsGrid wgsGrid; + private final int overlap; + private final Insets insets = new Insets(0, 0, 0, 0); + + protected final SettingsPaperAtlas s; + + private int tileImageScale = 1; + + private File layerFolder; + // base: page Size整数倍区域, + // bottom:最底下的不能被page Height整除的区域 + // right:最右侧的不能被page Width整除的区域 + // corner:右下角区域,不能被page Size整除的部分 + private Dimension base, bottom, right, corner; + + protected PaperAtlas(final boolean usePadding) { + + s = Settings.getInstance().paperAtlas.clone(); + overlap = (int) UnitSystem.pointsToPixels(s.overlap, s.dpi); + + if (s.wgsEnabled) { + SettingsWgsGrid sWgsGrid = Settings.getInstance().wgsGrid.clone(); + sWgsGrid.enabled = s.wgsEnabled; + sWgsGrid.density = s.wgsDensity; + wgsGrid = new WgsGrid(sWgsGrid, dummy); + } else { + wgsGrid = null; + } + + if (usePadding) { + insets.top = (int) UnitSystem.pointsToPixels(s.marginTop, s.dpi); + insets.left = (int) UnitSystem.pointsToPixels(s.marginLeft, s.dpi); + insets.bottom = (int) UnitSystem.pointsToPixels(s.marginBottom, s.dpi); + insets.right = (int) UnitSystem.pointsToPixels(s.marginTop, s.dpi); + } + + if (s.paperSize != null) { + double width = s.paperSize.width - s.marginLeft - s.marginRight; + double height = s.paperSize.height - s.marginTop - s.marginBottom; + width = UnitSystem.pointsToPixels(width, s.dpi); + height = UnitSystem.pointsToPixels(height, s.dpi); + base = new Dimension((int) width, (int) height); + } + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + corner = right = bottom = null; + + tileImageScale = getTileImageScale(); + + int mapWidth = (xMax - xMin + 1) * tileSize * tileImageScale; + int mapHeight = (yMax - yMin + 1) * tileSize * tileImageScale; + + if (base == null) { + base = new Dimension(mapWidth, mapHeight); + processPages(1, 1); + base = null; + } else { + + // 最后一页少于页宽度的crop百分百的,会裁剪掉,即抛弃。 + int sWidth = (mapWidth) % (base.width - overlap); + if ((double) sWidth / base.getWidth() * 100d < s.crop) { + sWidth = 0; + } + + int sHeight = (mapHeight) % (base.height - overlap); + if ((double) sHeight / base.getHeight() * 100d < s.crop) { + sHeight = 0; + } + + if (sHeight > 0 && sWidth > 0) { + corner = new Dimension(sWidth, sHeight); + } + if (sWidth > 0) { + right = new Dimension(sWidth, base.height); + } + if (sHeight > 0) { + bottom = new Dimension(base.width, sHeight); + } + int rows = (mapHeight) / (base.height - overlap) + (bottom != null ? 1 : 0); + int cols = (mapWidth) / (base.width - overlap) + (right != null ? 1 : 0); + processPages(rows, cols); + } + } + + protected File getLayerFolder() { + return layerFolder; + } + + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + layerFolder = new File(atlasDir, layer.getName()); + Utilities.mkDirs(layerFolder); + } + + /** + * check first existing image and return its scale. 2 means for retina( 512x512), 1 means normal + * + * @return + */ + protected int getTileImageScale() { + int tileImageScale = 1; + + if (mapDlTileProvider == null) { + return tileImageScale; + } + + // init tileScale for check if there has retina tile + try { + // use loop because xMin, xMax may has no data + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + BufferedImage tile = mapDlTileProvider.getTileImage(x, y); + if (tile != null) { + tileImageScale = tile.getWidth() / tileSize; + break; // skip when get + } + } + } + + // for invalid tileScale + if (tileImageScale != 2) { + log.debug("invalid tile Scale for Paper Atlas " + tileImageScale); + tileImageScale = 1; + } + } catch (IOException e) { + tileImageScale = 1; + } + return tileImageScale; + } + + protected abstract void processPage(BufferedImage image, int pageNumber) throws MapCreationException; + + private void processPages(final int ROWS, final int COLS) throws MapCreationException, InterruptedException { + try { + + atlasProgress.initMapCreation(ROWS * COLS * 2); + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + log.trace(String.format("cal=%d row=%d", col, row)); + + // Choose image + Dimension size; + + boolean firstRow = row == 0; + boolean lastRow = row + 1 == ROWS; + boolean lastCol = col + 1 == COLS; + if (corner != null && lastRow && lastCol) { + size = new Dimension(corner); + } else if (bottom != null && lastRow) { + size = new Dimension(bottom); + } else if (right != null && lastCol) { + size = new Dimension(right); + } else { + size = new Dimension(base); + } + + // Compute values + int pageXMin = col * base.width - col * overlap; + int pageYMin = row * base.height - row * overlap; + int firstTileX = pageXMin / (tileSize * tileImageScale) + xMin; + int firstTileY = pageYMin / (tileSize * tileImageScale) + yMin; + int firstTileXOffset = pageXMin % (tileSize * tileImageScale); + int firstTileYOffset = pageYMin % (tileSize * tileImageScale); + int tilesInCol = (size.height + firstTileYOffset - 1) / (tileSize * tileImageScale) + 1; + int tilesInRow = (size.width + firstTileXOffset - 1) / (tileSize * tileImageScale) + 1; + + // Create image and graphics + int imageWidth = size.width + insets.left + insets.right; + int imageHeight = size.height + insets.top + insets.bottom; + BufferedImage image = Utilities.safeCreateBufferedImage(imageWidth, imageHeight, + BufferedImage.TYPE_4BYTE_ABGR); + Graphics2D g = image.createGraphics(); + g.translate(insets.left, insets.top); + g.clipRect(0, 0, size.width, size.height); + // Paint image + + g.setBackground(IMAGE_BACKGROUND); + g.clearRect(0, 0, size.width, size.height); + g.translate(-firstTileXOffset, -firstTileYOffset); + for (int tileRow = 0; tileRow < tilesInCol; tileRow++) { + for (int tileCol = 0; tileCol < tilesInRow; tileCol++) { + int tileX = firstTileX + tileCol; + int tileY = firstTileY + tileRow; + int x = tileCol * tileSize * tileImageScale; + int y = tileRow * tileSize * tileImageScale; + BufferedImage tile = mapDlTileProvider.getTileImage(tileX, tileY); + if (tile != null) + g.drawImage(tile, x, y, tileSize * tileImageScale, tileSize * tileImageScale, null); + } + } + g.translate(firstTileXOffset, firstTileYOffset); + + // Paint additions + dummy.setSize(size); + Point tlc = new Point(firstTileX * tileSize * tileImageScale + firstTileXOffset, firstTileY + * tileSize * tileImageScale + firstTileYOffset); + if (s.wgsEnabled) + wgsGrid.paintWgsGrid(g, mapSource.getMapSpace(), tlc, zoom); + if (s.scaleBar) + ScaleBar.paintScaleBar(dummy, g, mapSource.getMapSpace(), tlc, zoom); + if (s.compass) { + Image compassRaw = ImageIO.read(Utilities.loadResourceAsStream("images/compass.png")); + Image compass = compassRaw.getScaledInstance(150, 150, Image.SCALE_SMOOTH); + g.drawImage(compass, 0, 0, null); + } + if (s.pageNumbers) { + g.setBackground(LABEL_BACKGROUND); + g.setColor(LABEL_FOREGROUND); + + String pageNumber = " " + getPageNumber(row, col, ROWS, COLS) + " "; + g.setFont(PAGE_NUMBER_FONT); + FontMetrics fontMetrics = g.getFontMetrics(); + int fontHeight = fontMetrics.getHeight(); + int pageNumberStringWidth = fontMetrics.stringWidth(pageNumber); + g.clearRect(0, 0, pageNumberStringWidth, fontHeight); + g.drawString(pageNumber, 0, fontHeight - fontMetrics.getDescent()); + + int centerX = size.width / 2; + g.setFont(LABEL_FONT); + fontMetrics = g.getFontMetrics(); + fontHeight = fontMetrics.getHeight(); + + if (!firstRow) { + String text = UP + getPageNumber(row - 1, col, ROWS, COLS) + " "; + int stringWidth = fontMetrics.stringWidth(text); + g.clearRect(centerX - stringWidth / 2, 8, stringWidth, fontHeight); + g.drawString(text, centerX - stringWidth / 2, 8 + fontHeight - fontMetrics.getDescent()); + } + if (!lastRow) { + String text = DOWN + getPageNumber(row + 1, col, ROWS, COLS) + " "; + int stringWidth = fontMetrics.stringWidth(text); + + g.clearRect(centerX - stringWidth / 2, size.height - 32 - fontHeight, stringWidth, + fontHeight); + g.drawString(text, centerX - stringWidth / 2, size.height - 32 - fontMetrics.getDescent()); + } + } + g.dispose(); + + // Process image + atlasProgress.incMapCreationProgress(); + checkUserAbort(); + processPage(image, getPageNumber(row, col, ROWS, COLS)); + atlasProgress.incMapCreationProgress(); + checkUserAbort(); + } + } + } catch (IOException e) { + throw new MapCreationException(map, e); + } + + } + + private int getPageNumber(int row, int col, int ROWS, int COLS) { + return row * COLS + col + 1; + } + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + ProjectionCategory projection = mapSpace.getProjectionCategory(); + return (mapSpace instanceof MercatorPower2MapSpace) + && (ProjectionCategory.SPHERE.equals(projection) || ProjectionCategory.ELLIPSOID.equals(projection)); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/PaperAtlasPdf.java b/src/main/java/mobac/program/atlascreators/PaperAtlasPdf.java new file mode 100644 index 0000000..1668bf1 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/PaperAtlasPdf.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import mobac.exceptions.MapCreationException; +import mobac.program.ProgramInfo; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.model.UnitSystem; + +import com.itextpdf.text.BadElementException; +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Image; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.PdfWriter; + +@AtlasCreatorName(value = "Paper Atlas (PDF)") +public class PaperAtlasPdf extends PaperAtlas { + + private Document document; + + public PaperAtlasPdf() { + super(false); + } + + private Document createDocument(Rectangle r) throws MapCreationException { + File pdfFile = new File(getLayerFolder(), map.getName() + ".pdf"); + float left = (float) s.marginLeft; + float right = (float) s.marginRight; + float top = (float) s.marginTop; + float bottom = (float) s.marginBottom; + Document document = new Document(r, left, right, top, bottom); + PdfWriter pdfWriter; + try { + pdfFile.createNewFile(); + pdfWriter = PdfWriter.getInstance(document, new FileOutputStream(pdfFile)); + } catch (IOException e) { + throw new MapCreationException(map, e); + } catch (DocumentException e) { + throw new MapCreationException(map, e); + } + pdfWriter.setCompressionLevel(s.compression); + document.open(); + document.addAuthor(ProgramInfo.PROG_NAME); + document.addCreationDate(); + document.addCreator(ProgramInfo.PROG_NAME); + document.addProducer(); + return document; + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + + if (s.paperSize != null) { + document = createDocument(s.paperSize.createRectangle()); + } + + try { + super.createMap(); + } finally { + try { + document.close(); + } catch (Exception e) { + new MapCreationException(map, e); + } + document = null; + } + } + + @Override + protected void processPage(BufferedImage image, int pageNumber) throws MapCreationException { + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + + if (document == null) { + double width = UnitSystem.pixelsToPoints(imageWidth, s.dpi); + double height = UnitSystem.pixelsToPoints(imageHeight, s.dpi); + width += s.marginLeft + s.marginRight; + height += s.marginTop + s.marginBottom; + Rectangle r = new Rectangle((float) width, (float) height); + document = createDocument(r); + } + + Image iTextImage; + try { + iTextImage = Image.getInstance(image, Color.WHITE); + } catch (BadElementException e) { + throw new MapCreationException(map, e); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + iTextImage.setCompressionLevel(s.compression); + iTextImage.setDpi(s.dpi, s.dpi); + + float width = (float) UnitSystem.pixelsToPoints(imageWidth, s.dpi); + float height = (float) UnitSystem.pixelsToPoints(imageHeight, s.dpi); + iTextImage.scaleAbsolute(width, height); + + try { + document.add(iTextImage); + } catch (DocumentException e) { + throw new MapCreationException(map, e); + } + document.newPage(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/PaperAtlasPng.java b/src/main/java/mobac/program/atlascreators/PaperAtlasPng.java new file mode 100644 index 0000000..6b7a1bd --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/PaperAtlasPng.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.text.DecimalFormat; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.utilities.Utilities; + +@AtlasCreatorName(value = "Paper Atlas (PNG)") +public class PaperAtlasPng extends PaperAtlas { + + private final DecimalFormat decimalFormat = new DecimalFormat("#000"); + + private File mapFolder; + + public PaperAtlasPng() { + super(true); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + mapFolder = new File(getLayerFolder(), map.getName()); + try { + Utilities.mkDirs(mapFolder); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + super.createMap(); + mapFolder = null; + } + + @Override + protected void processPage(BufferedImage image, int pageNumber) throws MapCreationException { + String fileName = decimalFormat.format(pageNumber) + ".png"; + File file = new File(mapFolder, fileName); + try { + ImageIO.write(image, "PNG", file); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/PathAway.java b/src/main/java/mobac/program/atlascreators/PathAway.java new file mode 100644 index 0000000..58a0011 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/PathAway.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; + +/** + * Creates a tile cache structure as used by PathAway (for WindowsMobile, + * Symbian, Palm) + */ +@AtlasCreatorName("PathAway tile cache") +public class PathAway extends OSMTracker { + + public PathAway() { + super(); + tileFileNamePattern = "%02X/%04X/%04X.%s"; + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + + MapSource mapSource = map.getMapSource(); + String shortMapDir = null; + if (mapSource.getName().equals("Google Maps")) + shortMapDir = "G1"; + else if (mapSource.getName().equals("Google Earth")) + shortMapDir = "G2"; + else if (mapSource.getName().equals("Google Terrain")) + shortMapDir = "G3"; + else if (mapSource.getName().equals("Mapnik")) + shortMapDir = "OSM1"; + else if (mapSource.getName().equals("OSM Cycle Map")) + shortMapDir = "OCM1"; + if (shortMapDir != null) + mapDir = new File(atlasDir, shortMapDir); + } + + public void createMap() throws MapCreationException, InterruptedException { + // This means there should not be any resizing of the tiles. + if (mapTileWriter == null) + mapTileWriter = new PathAwayTileWriter(); + createTiles(); + } + + @Override + protected void testAtlas() throws AtlasTestException { + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + if (map.getZoom() > 17) + throw new AtlasTestException("resolution too high - " + "highest possible zoom level is 17"); + } + } + } + + protected class PathAwayTileWriter extends OSMTileWriter { + + @Override + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) throws IOException { + File file = new File(mapDir, String.format(tileFileNamePattern, 17 - zoom, tilex, tiley, tileType)); + writeTile(file, tileData); + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/RMapsSQLite.java b/src/main/java/mobac/program/atlascreators/RMapsSQLite.java new file mode 100644 index 0000000..9a37888 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/RMapsSQLite.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Locale; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.interfaces.RequiresSQLite; +import mobac.program.model.Settings; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; +import mobac.utilities.jdbc.SQLiteLoader; + +/** + * Atlas/Map creator for "BigPlanet-Maps application for Android" (offline SQLite maps) + * http://code.google.com/p/bigplanet/ + *

+ * Some source parts are taken from the "android-map.blogspot.com Version of Mobile Atlas Creator": + * http://code.google.com/p/android-map/ + *

+ *

+ * Additionally the created BigPlanet SQLite database has one additional table containing special info needed by the + * Android application RMaps.
+ * (Database statements: {@link #RMAPS_TABLE_INFO_DDL} and {@link #RMAPS_UPDATE_INFO_SQL} ).
+ * Changes made by Robert, author of RMaps. + *

+ */ +@AtlasCreatorName(value = "RMaps SQLite", type = "RMaps") +@SupportedParameters(names = { Name.format }) +public class RMapsSQLite extends AtlasCreator implements RequiresSQLite { + + private static final int MAX_BATCH_SIZE = 1000; + + private static final String TABLE_DDL = "CREATE TABLE IF NOT EXISTS tiles (x int, y int, z int, s int, image blob, PRIMARY KEY (x,y,z,s))"; + private static final String INDEX_DDL = "CREATE INDEX IF NOT EXISTS IND on tiles (x,y,z,s)"; + private static final String INSERT_SQL = "INSERT or REPLACE INTO tiles (x,y,z,s,image) VALUES (?,?,?,0,?)"; + private static final String RMAPS_TABLE_INFO_DDL = "CREATE TABLE IF NOT EXISTS info AS SELECT 99 AS minzoom, 0 AS maxzoom"; + private static final String RMAPS_CLEAR_INFO_SQL = "DELETE FROM info;"; + private static final String RMAPS_UPDATE_INFO_MINMAX_SQL = "INSERT INTO info (minzoom,maxzoom) VALUES (?,?);"; + private static final String RMAPS_INFO_MAX_SQL = "SELECT DISTINCT z FROM tiles ORDER BY z DESC LIMIT 1;"; + private static final String RMAPS_INFO_MIN_SQL = "SELECT DISTINCT z FROM tiles ORDER BY z ASC LIMIT 1;"; + + protected File databaseFile; + + protected Connection conn = null; + protected PreparedStatement prepStmt; + + public RMapsSQLite() { + super(); + SQLiteLoader.loadSQLiteOrShowError(); + } + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + boolean correctTileSize = (256 == mapSpace.getTileSize()); + ProjectionCategory pc = mapSpace.getProjectionCategory(); + boolean correctProjection = (ProjectionCategory.SPHERE.equals(pc) || ProjectionCategory.ELLIPSOID.equals(pc)); + return correctTileSize && correctProjection; + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, AtlasTestException, + InterruptedException { + if (customAtlasDir == null) + customAtlasDir = Settings.getInstance().getAtlasOutputDirectory(); + super.startAtlasCreation(atlas, customAtlasDir); + databaseFile = new File(atlasDir, getDatabaseFileName()); + log.debug("SQLite Database file: " + databaseFile); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(atlasDir); + } catch (IOException e) { + throw new MapCreationException(map, e); + } + try { + SQLiteLoader.loadSQLite(); + } catch (SQLException e) { + throw new MapCreationException(SQLiteLoader.getMsgSqliteMissing(), map, e); + } + try { + openConnection(); + initializeDB(); + createTiles(); + } catch (SQLException e) { + throw new MapCreationException("Error creating SQL database \"" + databaseFile + "\": " + e.getMessage(), + map, e); + } + } + + protected void openConnection() throws SQLException { + if (conn == null || conn.isClosed()) { + String url = "jdbc:sqlite:/" + databaseFile.getAbsolutePath(); + conn = DriverManager.getConnection(url); + } + } + + @Override + public void abortAtlasCreation() throws IOException { + SQLiteLoader.closeConnection(conn); + conn = null; + super.abortAtlasCreation(); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + SQLiteLoader.closeConnection(conn); + conn = null; + super.finishAtlasCreation(); + } + + protected void initializeDB() throws SQLException { + Statement stat = conn.createStatement(); + stat.executeUpdate(TABLE_DDL); + stat.executeUpdate(INDEX_DDL); + createInfoTable(stat); + + stat.executeUpdate("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)"); + if (!(stat.executeQuery("SELECT * FROM android_metadata").next())) { + String locale = Locale.getDefault().toString(); + stat.executeUpdate("INSERT INTO android_metadata VALUES ('" + locale + "')"); + } + stat.close(); + } + + protected void createInfoTable(Statement stat) throws SQLException { + stat.executeUpdate(RMAPS_TABLE_INFO_DDL); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + int maxMapProgress = 2 * (xMax - xMin + 1) * (yMax - yMin + 1); + atlasProgress.initMapCreation(maxMapProgress); + TileImageParameters param = map.getParameters(); + if (param != null) + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, param.getFormat()); + try { + conn.setAutoCommit(false); + int batchTileCount = 0; + int tilesWritten = 0; + Runtime r = Runtime.getRuntime(); + long heapMaxSize = r.maxMemory(); + prepStmt = conn.prepareStatement(getTileInsertSQL()); + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + writeTile(x, y, zoom, sourceTileData); + tilesWritten++; + long heapAvailable = heapMaxSize - r.totalMemory() + r.freeMemory(); + + batchTileCount++; + if ((heapAvailable < HEAP_MIN) || (batchTileCount >= MAX_BATCH_SIZE)) { + log.trace("Executing batch containing " + batchTileCount + " tiles"); + prepStmt.executeBatch(); + prepStmt.clearBatch(); + System.gc(); + conn.commit(); + atlasProgress.incMapCreationProgress(batchTileCount); + batchTileCount = 0; + } + } + } catch (IOException e) { + throw new MapCreationException(map, e); + } + } + } + prepStmt.executeBatch(); + prepStmt.clearBatch(); + System.gc(); + if (tilesWritten > 0) + updateTileMetaInfo(); + log.trace("Final commit containing " + batchTileCount + " tiles"); + conn.commit(); + atlasProgress.setMapCreationProgress(maxMapProgress); + } catch (SQLException e) { + throw new MapCreationException(map, e); + } + } + + protected void updateTileMetaInfo() throws SQLException { + Statement stat = conn.createStatement(); + ResultSet rs = stat.executeQuery(RMAPS_INFO_MAX_SQL); + if (!rs.next()) + throw new SQLException("failed to retrieve max tile zoom info"); + int max = rs.getInt(1); + rs.close(); + rs = stat.executeQuery(RMAPS_INFO_MIN_SQL); + if (!rs.next()) + throw new SQLException("failed to retrieve min tile zoom info"); + int min = rs.getInt(1); + rs.close(); + PreparedStatement ps = conn.prepareStatement(RMAPS_UPDATE_INFO_MINMAX_SQL); + ps.setInt(1, min); + ps.setInt(2, max); + + stat.execute(RMAPS_CLEAR_INFO_SQL); + ps.execute(); + stat.close(); + ps.close(); + } + + protected void writeTile(int x, int y, int z, byte[] tileData) throws SQLException, IOException { + prepStmt.setInt(1, x); + prepStmt.setInt(2, y); + prepStmt.setInt(3, 17 - z); + prepStmt.setBytes(4, tileData); + prepStmt.addBatch(); + } + + protected String getDatabaseFileName() { + return atlas.getName() + ".sqlitedb"; + } + + protected String getTileInsertSQL() { + return INSERT_SQL; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/RunGPSAtlas.java b/src/main/java/mobac/program/atlascreators/RunGPSAtlas.java new file mode 100644 index 0000000..cb18637 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/RunGPSAtlas.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.impl.rungps.RunGPSAtlasFile; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageType; + +/** + * Creates maps using the Run.GPS Trainer atlas format. + * + * Please note that this atlas format ignores the defined atlas structure. + * + *

+ * Run.GPS Atlas format has been designed to support huge collections of maps (2 GB and more) and for very fast access + * (using a numeric index at the beginning of an atlas file). The file format can hold integer values, strings and + * binary data. The file format is described on this page: http://www.rungps.net/wiki/RunGPSAtlasFormat (full sample + * source code is available). + *

+ */ +@AtlasCreatorName(value = "Run.GPS Atlas", type = "RunGPS") +public class RunGPSAtlas extends AtlasCreator { + + protected RunGPSAtlasFile atlasIndex = null; + protected Set availableTileList = new HashSet(); + protected int minZoom, maxZoom; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, InterruptedException, + AtlasTestException { + super.startAtlasCreation(atlas, customAtlasDir); + String atlasName = this.atlas.getName().replace(' ', '_'); + atlasIndex = new RunGPSAtlasFile(atlasDir.getPath() + File.separatorChar + atlasName + RunGPSAtlasFile.SUFFIX, + true); + minZoom = Integer.MAX_VALUE; + maxZoom = Integer.MIN_VALUE; + } + + @Override + public void abortAtlasCreation() throws IOException { + atlasIndex.close(); + super.abortAtlasCreation(); + } + + @Override + public void finishAtlasCreation() throws IOException, InterruptedException { + super.finishAtlasCreation(); + + // log.debug(atlasIndex.listAll()); + + // archive data + + atlasIndex.setValue("/0/0/0", 3L); // file format version + atlasIndex.setString("/0/0/1", "Run.GPS Atlas File"); // type + atlasIndex.setString("/0/0/2", atlas.getName()); // atlas name + atlasIndex.setString("/0/0/3", mapSource.getName()); // map source name + atlasIndex.setString("/0/0/4", "Mobile Atlas Creator"); // created by + + // metadata + + atlasIndex.setValue("/0/1/1", minZoom); + atlasIndex.setValue("/0/1/2", maxZoom); + + // create file + + atlasIndex.finishArchive(); + + atlasIndex.close(); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + } + + public void createMap() throws MapCreationException, InterruptedException { + if (mapSource.getTileImageType() != TileImageType.PNG) + // If the tile image format is not png we have to convert it + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, TileImageFormat.PNG); + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + String mapName = map.getMapSource().getName().replaceAll(" ", "_"); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + writeTile(mapName, sourceTileData, x, y, zoom); + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + + protected boolean writeTile(String cache, byte[] tileData, int x, int y, int zoom) throws IOException { + if (zoom < minZoom) + minZoom = zoom; + if (zoom > maxZoom) + maxZoom = zoom; + + String cacheKey = cache + "-" + zoom + "-" + x + "-" + y; + + if (availableTileList.contains(cacheKey)) { + log.warn("Map tile already in cache: " + cacheKey + " -> ignoring"); + return false; + } + + ArrayList hierarchy = new ArrayList(); + hierarchy.add(zoom); + hierarchy.add(x); + hierarchy.add(y); + atlasIndex.addData(hierarchy, tileData); + + return true; + } +} diff --git a/src/main/java/mobac/program/atlascreators/SportsTracker.java b/src/main/java/mobac/program/atlascreators/SportsTracker.java new file mode 100644 index 0000000..783c93b --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/SportsTracker.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageParameters; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; + +/** + * [Nokia] Sports Tracker + * + * + */ +@AtlasCreatorName(value = "Sports Tracker", type = "NST") +@SupportedParameters(names = { Name.format }) +public class SportsTracker extends AtlasCreator { + + protected File mapDir = null; + + protected MapTileWriter mapTileWriter = null; + + protected String tileType = null; + + @Override + public boolean testMapSource(MapSource mapSource) { + return MercatorPower2MapSpace.INSTANCE_256.equals(mapSource.getMapSpace()); + } + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + mapDir = new File(atlasDir, layer.getName()); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + tileType = map.getMapSource().getTileImageType().getFileExt(); + TileImageParameters param = map.getParameters(); + if (param != null) { + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, param.getFormat()); + tileType = param.getFormat().getFileExt(); + } + + } + + public void createMap() throws MapCreationException, InterruptedException { + // This means there should not be any resizing of the tiles. + createTiles(); + } + + protected void createTiles() throws InterruptedException, MapCreationException { + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) + writeTile(x, y, sourceTileData); + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + } + } + } + + public void writeTile(int tilex, int tiley, byte[] tileData) throws IOException { + String tileName = getTileName(zoom, tilex, tiley); + int count = tileName.length(); + int dirCount = count / 3; + if ((count % 3 == 0) & (dirCount > 0)) + dirCount--; + File tileDir = mapDir; + for (int i = 0; i < dirCount; i++) { + int start = i * 3; + String dirName = tileName.substring(start, start + 3); + tileDir = new File(tileDir, dirName); + } + // File extension needs to be jpg (requested by telemaxx) + // see https://sourceforge.net/tracker/?func=detail&atid=1105497&aid=3066161&group_id=238075 + String fileName = tileName + ".jpg"; + File file = new File(tileDir, fileName); + writeTile(file, tileData); + } + + protected void writeTile(File file, byte[] tileData) throws IOException { + Utilities.mkDirs(file.getParentFile()); + FileOutputStream out = new FileOutputStream(file); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + } + + protected static final char[] NUM_CHAR = { 'q', 'r', 't', 's' }; + + public static String getTileName(int zoom, int tilex, int tiley) { + char[] tileNum = new char[zoom + 1]; + tileNum[0] = 't'; + for (int i = zoom; i > 0; i--) { + int num = (tilex % 2) | ((tiley % 2) << 1); + tileNum[i] = NUM_CHAR[num]; + tilex >>= 1; + tiley >>= 1; + } + return new String(tileNum); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/TTQV.java b/src/main/java/mobac/program/atlascreators/TTQV.java new file mode 100644 index 0000000..118989e --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/TTQV.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +import mobac.exceptions.MapCreationException; +import mobac.program.ProgramInfo; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.Utilities; + +/** + * Touratech QV + */ +@AtlasCreatorName(value = "Touratech QV", type = "Ttqv") +public class TTQV extends Ozi { + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDir(layerDir); + } catch (IOException e1) { + throw new MapCreationException(map, e1); + } + createTiles(); + writeCalFile(); + } + + private void writeCalFile() throws MapCreationException { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, mapName + "_png.cal")); + OutputStreamWriter mapWriter = new OutputStreamWriter(fout, TEXT_FILE_CHARSET); + + MapSpace mapSpace = mapSource.getMapSpace(); + + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + double scale = ((latitudeMax - latitudeMin) * (longitudeMax - longitudeMin)) / (width * height); + + String nsowLine = "%s = 6 = %2.6f\r\n"; + String cLine = "c%d_%s = 7 = %2.6f\r\n"; + + mapWriter.write("; Calibration File for QV Map\r\n"); + mapWriter.write("; generated by " + ProgramInfo.getCompleteTitle() + "\r\n"); + mapWriter.write("name = 10 = " + mapName + ".png\r\n"); + mapWriter.write("fname = 10 = " + mapName + ".png\r\n"); + mapWriter.write(String.format(Locale.ENGLISH, nsowLine, "nord", latitudeMax)); + mapWriter.write(String.format(Locale.ENGLISH, nsowLine, "sued", latitudeMin)); + mapWriter.write(String.format(Locale.ENGLISH, nsowLine, "ost", longitudeMax)); + mapWriter.write(String.format(Locale.ENGLISH, nsowLine, "west", longitudeMin)); + NumberFormat nf = new DecimalFormat("0.000000E000", Utilities.DFS_ENG); + mapWriter.write("scale_area = 6 = " + nf.format(scale).toLowerCase() + "\r\n"); + mapWriter.write("proj_mode = 10 = proj\r\n"); + mapWriter.write("projparams = 10 = proj=merc\r\n"); + mapWriter.write("datum1 = 10 = WGS 84# 6378137# 298.257223563# 0# 0# 0#\r\n"); + mapWriter.write("c1_x = 7 = 0\r\n"); + mapWriter.write("c1_y = 7 = 0\r\n"); + mapWriter.write("c2_x = 7 = " + (width - 1) + "\r\n"); + mapWriter.write("c2_y = 7 = 0\r\n"); + mapWriter.write("c3_x = 7 = " + (width - 1) + "\r\n"); + mapWriter.write("c3_y = 7 = " + (height - 1) + "\r\n"); + mapWriter.write("c4_x = 7 = 0\r\n"); + mapWriter.write("c4_y = 7 = " + (height - 1) + "\r\n"); + mapWriter.write("c5_x = 7 = 0\r\n"); + mapWriter.write("c5_y = 7 = 0\r\n"); + mapWriter.write("c6_x = 7 = 0\r\n"); + mapWriter.write("c6_y = 7 = 0\r\n"); + mapWriter.write("c7_x = 7 = 0\r\n"); + mapWriter.write("c7_y = 7 = 0\r\n"); + mapWriter.write("c8_x = 7 = 0\r\n"); + mapWriter.write("c8_y = 7 = 0\r\n"); + mapWriter.write("c9_x = 7 = 0\r\n"); + mapWriter.write("c9_y = 7 = 0\r\n"); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 1, "lat", latitudeMax)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 1, "lon", longitudeMin)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 2, "lat", latitudeMax)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 2, "lon", longitudeMax)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 3, "lat", latitudeMin)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 3, "lon", longitudeMax)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 4, "lat", latitudeMin)); + mapWriter.write(String.format(Locale.ENGLISH, cLine, 4, "lon", longitudeMin)); + + mapWriter.flush(); + mapWriter.close(); + } catch (IOException e) { + throw new MapCreationException("Error writing cal file: " + e.getMessage(), map, e); + } finally { + Utilities.closeStream(fout); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/TileStoreDownload.java b/src/main/java/mobac/program/atlascreators/TileStoreDownload.java new file mode 100644 index 0000000..522ec88 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/TileStoreDownload.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import mobac.exceptions.MapCreationException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.interfaces.MapSource; + +@AtlasCreatorName(value = "Tile store download only", type = "TILESTORE") +public class TileStoreDownload extends AtlasCreator { + + public TileStoreDownload() { + } + + @Override + public boolean testMapSource(MapSource mapSource) { + return true; + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + } + +} diff --git a/src/main/java/mobac/program/atlascreators/TomTomRaster.java b/src/main/java/mobac/program/atlascreators/TomTomRaster.java new file mode 100644 index 0000000..b81b2a2 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/TomTomRaster.java @@ -0,0 +1,246 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.text.NumberFormat; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; + +/** + * + * http://sourceforge.net/p/mobac/feature-requests/225/ + * http://create.tomtom.com/manuals/create-your-own-content/index.html?map_overlays.htm + * + * @author r_x + */ +@AtlasCreatorName(value = "TomTom Raster (image + SAT)") +public class TomTomRaster extends AtlasCreator { + + protected File layerDir; + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + } + + @Override + protected void testAtlas() throws AtlasTestException { + Runtime r = Runtime.getRuntime(); + long heapMaxSize = r.maxMemory(); + int maxMapSize = (int) (Math.sqrt(heapMaxSize / 3d) * 0.8); // reduce maximum by 20% + maxMapSize = (maxMapSize / 100) * 100; // round by 100; + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + int w = map.getMaxTileCoordinate().x - map.getMinTileCoordinate().x; + int h = map.getMaxTileCoordinate().y - map.getMinTileCoordinate().y; + if (w > maxMapSize || h > maxMapSize) + throw new AtlasTestException("Map size too large for memory (is: " + Math.max(w, h) + " max: " + + maxMapSize + ")", map); + } + } + + } + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + super.initLayerCreation(layer); + layerDir = new File(atlasDir, layer.getName()); + Utilities.mkDirs(layerDir); + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + try { + createImage(); + } catch (InterruptedException e) { + throw e; + } catch (MapCreationException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + protected void createImage() throws InterruptedException, MapCreationException { + + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + ImageIO.setUseCache(false); + + int mapWidth = (xMax - xMin + 1) * tileSize; + int mapHeight = (yMax - yMin + 1) * tileSize; + + int maxImageSize = getMaxImageSize(); + int imageWidth = Math.min(maxImageSize, mapWidth); + int imageHeight = Math.min(maxImageSize, mapHeight); + + int len = Math.max(mapWidth, mapHeight); + double scaleFactor = 1.0; + boolean scaleImage = (len > maxImageSize); + if (scaleImage) { + scaleFactor = (double) getMaxImageSize() / (double) len; + if (mapWidth != mapHeight) { + // Map is not rectangle -> adapt height or width + if (mapWidth > mapHeight) + imageHeight = (int) (scaleFactor * mapHeight); + else + imageWidth = (int) (scaleFactor * mapWidth); + } + } + if (imageHeight < 0 || imageWidth < 0) + throw new MapCreationException("Invalid map size: (width/height: " + imageWidth + "/" + imageHeight + ")", + map); + long imageSize = 3l * ((long) imageWidth) * ((long) imageHeight); + if (imageSize > Integer.MAX_VALUE) + throw new MapCreationException("Map image too large: (width/height: " + imageWidth + "/" + imageHeight + + ") - reduce the map size and try again", map); + BufferedImage tileImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D graphics = tileImage.createGraphics(); + try { + if (scaleImage) { + graphics.setTransform(AffineTransform.getScaleInstance(scaleFactor, scaleFactor)); + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + int lineY = 0; + for (int y = yMin; y <= yMax; y++) { + int lineX = 0; + for (int x = xMin; x <= xMax; x++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + BufferedImage tile = ImageIO.read(new ByteArrayInputStream(sourceTileData)); + graphics.drawImage(tile, lineX, lineY, Color.WHITE, null); + } + } catch (IOException e) { + log.error("", e); + } + lineX += tileSize; + } + lineY += tileSize; + } + } finally { + graphics.dispose(); + } + writeTileImage(tileImage); + } + + protected void writeTileImage(BufferedImage tileImage) throws MapCreationException { + TileImageDataWriter writer; + if (parameters != null) { + writer = parameters.getFormat().getDataWriter(); + } else + writer = new TileImageJpegDataWriter(0.9); + writer.initialize(); + try { + int initialBufferSize = tileImage.getWidth() * tileImage.getHeight() / 4; + ByteArrayOutputStream buf = new ByteArrayOutputStream(initialBufferSize); + writer.processImage(tileImage, buf); + String imageFileName = map.getName() + "." + writer.getType(); + File imageFile = new File(layerDir, imageFileName); + FileOutputStream fout = new FileOutputStream(imageFile); + try { + fout.write(buf.toByteArray()); + fout.flush(); + } finally { + fout.close(); + } + writeSatFile(imageFileName, tileImage.getWidth(), tileImage.getHeight()); + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + /** + * SAT file content + * + *
+	 * Line 1 - filename of the image file.
+	 * Line 2 - GPS coordinate of the top left corner of the image (longitude).
+	 * Line 3 - GPS coordinate of the top left corner of the image (latitude).
+	 * Line 4 - GPS coordinate of the bottom right corner of the image (longitude).
+	 * Line 5 - GPS coordinate of the bottom right corner of the image (latitude).
+	 * Line 6 - Minimum zoom level for image to be visible (min = 0).
+	 * Line 7 - Maximum zoom level for image to be visible (max = 65,535).
+	 * Line 8 - Width of image file in pixels.
+	 * Line 9 - Height of image file in pixels.
+	 * 
+ */ + protected void writeSatFile(String imageFileName, int width, int height) throws IOException { + int startX = xMin * tileSize; + int startY = yMin * tileSize; + + MapSpace mapSpace = mapSource.getMapSpace(); + NumberFormat df = Utilities.FORMAT_6_DEC_ENG; + + String longitudeMin = df.format(mapSpace.cXToLon(startX, zoom)); + String longitudeMax = df.format(mapSpace.cXToLon(startX + width, zoom)); + String latitudeMin = df.format(mapSpace.cYToLat(startY + height, zoom)); + String latitudeMax = df.format(mapSpace.cYToLat(startY, zoom)); + + StringWriter sw = new StringWriter(); + sw.write(imageFileName + "\r\n"); + sw.write(longitudeMin + "\r\n"); + sw.write(latitudeMax + "\r\n"); + sw.write(longitudeMax + "\r\n"); + sw.write(latitudeMin + "\r\n"); + sw.write("0\r\n"); + sw.write("65535\r\n"); + sw.write(Integer.toString(width) + "\r\n"); + sw.write(Integer.toString(height)); + + int i = imageFileName.lastIndexOf('.'); + String satFileName = imageFileName.substring(0, i) + ".sat"; + FileOutputStream fout = new FileOutputStream(new File(layerDir,satFileName)); + OutputStreamWriter writer = new OutputStreamWriter(fout, Charsets.ISO_8859_1); + writer.append(sw.toString()); + writer.flush(); + writer.close(); + } + + protected int getMaxImageSize() { + return 2048; + } +} diff --git a/src/main/java/mobac/program/atlascreators/TrekBuddy.java b/src/main/java/mobac/program/atlascreators/TrekBuddy.java new file mode 100644 index 0000000..a5f625d --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/TrekBuddy.java @@ -0,0 +1,334 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Locale; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.impl.MapTileBuilder; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.program.atlascreators.tileprovider.CacheTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.model.TileImageParameters.Name; +import mobac.utilities.Utilities; +import mobac.utilities.geo.GeoUtils; + +@AtlasCreatorName(value = "TrekBuddy untared atlas", type = "UntaredAtlas") +@SupportedParameters(names = { Name.format, Name.height, Name.width }) +public class TrekBuddy extends AtlasCreator { + + protected static final String FILENAME_PATTERN = "t_%d_%d.%s"; + + protected File layerDir = null; + protected File mapDir = null; + protected MapTileWriter mapTileWriter; + + @Override + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + // TODO supports Mercator ellipsoid? + } + + public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, InterruptedException, + AtlasTestException { + super.startAtlasCreation(atlas, customAtlasDir); + } + + public void finishAtlasCreation() { + createAtlasTbaFile("cr"); + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + LayerInterface layer = map.getLayer(); + layerDir = new File(atlasDir, layer.getName()); + mapDir = new File(layerDir, map.getName()); + } + + protected void writeMapFile() throws IOException { + File mapFile = new File(mapDir, map.getName() + ".map"); + FileOutputStream mapFileStream = null; + try { + mapFileStream = new FileOutputStream(mapFile); + writeMapFile(mapFileStream); + } finally { + Utilities.closeStream(mapFileStream); + } + } + + protected void writeMapFile(OutputStream stream) throws IOException { + writeMapFile("t_." + mapSource.getTileImageType().getFileExt(), stream); + } + + protected void writeMapFile(String imageFileName, OutputStream stream) throws IOException { + log.trace("Writing map file"); + OutputStreamWriter mapWriter = new OutputStreamWriter(stream, TEXT_FILE_CHARSET); + + MapSpace mapSpace = mapSource.getMapSpace(); + + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize - 1, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize - 1, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + + mapWriter.write(prepareMapString(imageFileName, longitudeMin, longitudeMax, latitudeMin, latitudeMax, width, + height)); + mapWriter.flush(); + } + + public void createMap() throws MapCreationException, InterruptedException { + try { + Utilities.mkDirs(mapDir); + + // write the .map file containing the calibration points + writeMapFile(); + + // This means there should not be any resizing of the tiles. + mapTileWriter = createMapTileWriter(); + + // Select the tile creator instance based on whether tile image + // parameters has been set or not + if (parameters != null) + createCustomTiles(); + else + createTiles(); + + mapTileWriter.finalizeMap(); + } catch (MapCreationException e) { + throw e; + } catch (InterruptedException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + protected MapTileWriter createMapTileWriter() throws IOException { + return new FileTileWriter(); + } + + /** + * New experimental custom tile size algorithm implementation. + * + * It creates each custom sized tile separately. Therefore each original tile (256x256) will be loaded and painted + * multiple times. Therefore this implementation needs much more CPU power as each original tile is loaded at least + * once and each generated tile has to be saved. + * + * @throws MapCreationException + */ + protected void createCustomTiles() throws InterruptedException, MapCreationException { + log.debug("Starting map creation using custom parameters: " + parameters); + + CacheTileProvider ctp = new CacheTileProvider(mapDlTileProvider); + try { + mapDlTileProvider = ctp; + + MapTileBuilder mapTileBuilder = new MapTileBuilder(this, mapTileWriter, true); + atlasProgress.initMapCreation(mapTileBuilder.getCustomTileCount()); + mapTileBuilder.createTiles(); + } finally { + ctp.cleanup(); + } + } + + protected void createTiles() throws InterruptedException, MapCreationException { + int tilex = 0; + int tiley = 0; + + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + + ImageIO.setUseCache(false); + byte[] emptyTileData = Utilities.createEmptyTileData(mapSource); + String tileType = mapSource.getTileImageType().getFileExt(); + for (int x = xMin; x <= xMax; x++) { + tiley = 0; + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + mapTileWriter.writeTile(tilex, tiley, tileType, sourceTileData); + } else { + log.trace(String.format("Tile x=%d y=%d not found in tile archive - creating default", tilex, + tiley)); + mapTileWriter.writeTile(tilex, tiley, tileType, emptyTileData); + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + tiley++; + } + tilex++; + } + } + + private class FileTileWriter implements MapTileWriter { + + File setFolder; + Writer setFileWriter; + + int tileHeight = 256; + int tileWidth = 256; + + public FileTileWriter() throws IOException { + super(); + setFolder = new File(mapDir, "set"); + Utilities.mkDir(setFolder); + log.debug("Writing tiles to set folder: " + setFolder); + File setFile = new File(mapDir, map.getName() + ".set"); + if (parameters != null) { + tileHeight = parameters.getHeight(); + tileWidth = parameters.getWidth(); + } + try { + setFileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(setFile), + TEXT_FILE_CHARSET)); + } catch (IOException e) { + log.error("", e); + } + } + + public void writeTile(int tilex, int tiley, String imageFormat, byte[] tileData) throws IOException { + String tileFileName = String.format(FILENAME_PATTERN, (tilex * tileWidth), (tiley * tileHeight), + imageFormat); + + File f = new File(setFolder, tileFileName); + FileOutputStream out = new FileOutputStream(f); + setFileWriter.write(tileFileName + "\r\n"); + try { + out.write(tileData); + } finally { + Utilities.closeStream(out); + } + } + + public void finalizeMap() { + try { + setFileWriter.flush(); + } catch (IOException e) { + log.error("", e); + } + Utilities.closeWriter(setFileWriter); + } + } + + protected String prepareMapString(String fileName, double longitudeMin, double longitudeMax, double latitudeMin, + double latitudeMax, int width, int height) { + + StringBuffer sbMap = new StringBuffer(); + + sbMap.append("OziExplorer Map Data File Version 2.2\r\n"); + sbMap.append(fileName + "\r\n"); + sbMap.append(fileName + "\r\n"); + sbMap.append("1 ,Map Code,\r\n"); + sbMap.append("WGS 84,WGS 84, 0.0000, 0.0000,WGS 84\r\n"); + sbMap.append("Reserved 1\r\n"); + sbMap.append("Reserved 2\r\n"); + sbMap.append("Magnetic Variation,,,E\r\n"); + sbMap.append("Map Projection,Mercator,PolyCal,No," + "AutoCalOnly,No,BSBUseWPX,No\r\n"); + + String latMax = GeoUtils.getDegMinFormat(latitudeMax, true); + String latMin = GeoUtils.getDegMinFormat(latitudeMin, true); + String lonMax = GeoUtils.getDegMinFormat(longitudeMax, false); + String lonMin = GeoUtils.getDegMinFormat(longitudeMin, false); + + String pointLine = "Point%02d,xy, %4s, %4s,in, deg, %1s, %1s, grid, , , ,N\r\n"; + + sbMap.append(String.format(pointLine, 1, 0, 0, latMax, lonMin)); + sbMap.append(String.format(pointLine, 2, width - 1, 0, latMax, lonMax)); + sbMap.append(String.format(pointLine, 3, width - 1, height - 1, latMin, lonMax)); + sbMap.append(String.format(pointLine, 4, 0, height - 1, latMin, lonMin)); + + for (int i = 5; i <= 30; i++) { + String s = String.format(pointLine, i, "", "", "", ""); + sbMap.append(s); + } + sbMap.append("Projection Setup,,,,,,,,,,\r\n"); + sbMap.append("Map Feature = MF ; Map Comment = MC These follow if they exist\r\n"); + sbMap.append("Track File = TF These follow if they exist\r\n"); + sbMap.append("Moving Map Parameters = MM? These follow if they exist\r\n"); + + sbMap.append("MM0,Yes\r\n"); + sbMap.append("MMPNUM,4\r\n"); + + String mmpxLine = "MMPXY, %d, %5d, %5d\r\n"; + + sbMap.append(String.format(mmpxLine, 1, 0, 0)); + sbMap.append(String.format(mmpxLine, 2, width - 1, 0)); + sbMap.append(String.format(mmpxLine, 3, width - 1, height - 1)); + sbMap.append(String.format(mmpxLine, 4, 0, height - 1)); + + String mpllLine = "MMPLL, %d, %2.6f, %2.6f\r\n"; + + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 1, longitudeMin, latitudeMax)); + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 2, longitudeMax, latitudeMax)); + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 3, longitudeMax, latitudeMin)); + sbMap.append(String.format(Locale.ENGLISH, mpllLine, 4, longitudeMin, latitudeMin)); + + sbMap.append("MOP,Map Open Position,0,0\r\n"); + + // The simple variant for calculating mm1b + // http://www.trekbuddy.net/forum/viewtopic.php?t=3755&postdays=0&postorder=asc&start=286 + double mm1b = (longitudeMax - longitudeMin) * 111319; + mm1b *= Math.cos(Math.toRadians((latitudeMax + latitudeMin) / 2.0)) / width; + + sbMap.append(String.format(Locale.ENGLISH, "MM1B, %2.6f\r\n", mm1b)); + + sbMap.append("IWH,Map Image Width/Height, " + width + ", " + height + "\r\n"); + + return sbMap.toString(); + } + + public void createAtlasTbaFile(String name) { + File crtba = new File(atlasDir.getAbsolutePath(), name + ".tba"); + try { + FileWriter fw = new FileWriter(crtba); + fw.write("Atlas 1.0\r\n"); + fw.close(); + } catch (IOException e) { + log.error("", e); + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/TrekBuddyTared.java b/src/main/java/mobac/program/atlascreators/TrekBuddyTared.java new file mode 100644 index 0000000..1ecb1f4 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/TrekBuddyTared.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.io.FileUtils; + +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.impl.MapTileWriter; +import mobac.utilities.Utilities; +import mobac.utilities.tar.TarArchive; +import mobac.utilities.tar.TarTmiArchive; + +@AtlasCreatorName(value = "TrekBuddy tared atlas", type = "TaredAtlas") +public class TrekBuddyTared extends TrekBuddy { + + @Override + public void finishAtlasCreation() { + createAtlasTarArchive("cr"); + } + + private void createAtlasTarArchive(String name) { + log.trace("Creating cr.tar for atlas in dir \"" + atlasDir.getPath() + "\""); + + File[] atlasLayerDirs = Utilities.listSubDirectories(atlasDir); + List atlasMapDirs = new LinkedList(); + for (File dir : atlasLayerDirs) + Utilities.addSubDirectories(atlasMapDirs, dir, 0); + + TarArchive ta = null; + File crFile = new File(atlasDir, name + ".tar"); + try { + ta = new TarArchive(crFile, atlasDir); + + ta.writeFileFromData(name + ".tba", "Atlas 1.0\r\n".getBytes()); + + for (File mapDir : atlasMapDirs) { + ta.writeFile(mapDir); + File mapFile = new File(mapDir, mapDir.getName() + ".map"); + ta.writeFile(mapFile); + try { + mapFile.delete(); + } catch (Exception e) { + } + } + ta.writeEndofArchive(); + } catch (IOException e) { + log.error("Failed writing tar file \"" + crFile.getPath() + "\"", e); + } finally { + if (ta != null) + ta.close(); + } + } + + @Override + protected MapTileWriter createMapTileWriter() throws IOException { + return new TarTileWriter(); + } + + private class TarTileWriter implements MapTileWriter { + + TarArchive ta = null; + int tileHeight = 256; + int tileWidth = 256; + + public TarTileWriter() { + super(); + if (parameters != null) { + tileHeight = parameters.getHeight(); + tileWidth = parameters.getWidth(); + } + File mapTarFile = new File(mapDir, map.getName() + ".tar"); + log.debug("Writing tiles to tared map: " + mapTarFile); + try { + ta = new TarTmiArchive(mapTarFile, null); + ByteArrayOutputStream buf = new ByteArrayOutputStream(8192); + writeMapFile(buf); + ta.writeFileFromData(map.getName() + ".map", buf.toByteArray()); + } catch (IOException e) { + log.error("", e); + } + } + + public void writeTile(int tilex, int tiley, String imageFormat, byte[] tileData) throws IOException { + String tileFileName = String.format(FILENAME_PATTERN, (tilex * tileWidth), (tiley * tileHeight), + imageFormat); + + ta.writeFileFromData("set/" + tileFileName, tileData); + } + + public void finalizeMap() { + try { + ta.writeEndofArchive(); + } catch (IOException e) { + log.error("", e); + } + ta.close(); + } + + } + +} diff --git a/src/main/java/mobac/program/atlascreators/TwoNavRMAP.java b/src/main/java/mobac/program/atlascreators/TwoNavRMAP.java new file mode 100644 index 0000000..437d9f8 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/TwoNavRMAP.java @@ -0,0 +1,547 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* + * add to mobac.program.model.AtlasOutputFormat.java + * + * import mobac.program.atlascreators.TwoNavRmap; + * TwoNavRMAP("TwoNav (RMAP)", TwoNavRmap.class), // + * + */ +package mobac.program.atlascreators; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +import javax.imageio.ImageIO; + +import mobac.exceptions.AtlasTestException; +import mobac.exceptions.MapCreationException; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSpace.ProjectionCategory; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageFormat; +import mobac.program.model.TileImageParameters.Name; +import mobac.program.model.TileImageType; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; +import mobac.utilities.Utilities; + +import org.apache.log4j.Level; + +/** + * + * Creates one RMAP file per layer. + * + * @author Luka Logar + * @author r_x + * + */ +@AtlasCreatorName(value = "TwoNav (RMAP)") +@SupportedParameters(names = { Name.format }) +public class TwoNavRMAP extends AtlasCreator { + + private RmapFile rmapFile = null; + + private class ZoomLevel { + + private int index = 0; + private long offset = 0; + private int width = 0; + private int height = 0; + private int xTiles = 0; + private int yTiles = 0; + private long jpegOffsets[][] = null; + private int zoom = 0; + private boolean dl = false; + + private void writeHeader() throws IOException { + if (offset == 0) { + offset = rmapFile.getFilePointer(); + } else { + rmapFile.seek(offset); + } + log.trace(String.format("Writing ZoomLevel %d (%dx%d pixels, %dx%d tiles) header at offset %d", index, + width, height, xTiles, yTiles, offset)); + rmapFile.writeIntI(width); + rmapFile.writeIntI(-height); + rmapFile.writeIntI(xTiles); + rmapFile.writeIntI(yTiles); + if (jpegOffsets == null) { + jpegOffsets = new long[xTiles][yTiles]; + } + for (int y = 0; y < yTiles; y++) { + for (int x = 0; x < xTiles; x++) { + rmapFile.writeLongI(jpegOffsets[x][y]); + } + } + } + + private BufferedImage loadJpegAtOffset(long offset) throws IOException { + if (offset == 0) { + throw new IOException("offset == 0"); + } + rmapFile.seek(offset); + int TagId = rmapFile.readIntI(); + if (TagId != 7) { + throw new IOException("TagId != 7"); + } + int TagLen = rmapFile.readIntI(); + byte[] jpegImageBuf = new byte[TagLen]; + rmapFile.readFully(jpegImageBuf); + ByteArrayInputStream input = new ByteArrayInputStream(jpegImageBuf); + return ImageIO.read(input); + } + + private byte[] getTileData(TileImageDataWriter writer, ZoomLevel source, int x, int y) throws IOException { + log.trace(String.format("Shrinking jpegs (%d,%d,%d - %d,%d,%d)", source.index, x, y, source.index, + (x + 1 < source.xTiles) ? x + 1 : x, (y + 1 < source.yTiles) ? y + 1 : y)); + BufferedImage bi11 = loadJpegAtOffset(source.jpegOffsets[x][y]); + BufferedImage bi21 = (x + 1 < source.xTiles) ? loadJpegAtOffset(source.jpegOffsets[x + 1][y]) : null; + BufferedImage bi12 = (y + 1 < source.yTiles) ? loadJpegAtOffset(source.jpegOffsets[x][y + 1]) : null; + BufferedImage bi22 = (x + 1 < source.xTiles) && (y + 1 < source.yTiles) ? loadJpegAtOffset(source.jpegOffsets[x + 1][y + 1]) + : null; + int biWidth = bi11.getWidth() + (bi21 != null ? bi21.getWidth() : 0); + int biHeight = bi11.getHeight() + (bi12 != null ? bi12.getHeight() : 0); + BufferedImage bi = new BufferedImage(biWidth, biHeight, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = bi.createGraphics(); + g.drawImage(bi11, 0, 0, null); + if (bi21 != null) { + g.drawImage(bi21, bi11.getWidth(), 0, null); + } + if (bi12 != null) { + g.drawImage(bi12, 0, bi11.getHeight(), null); + } + if (bi22 != null) { + g.drawImage(bi22, bi11.getWidth(), bi11.getHeight(), null); + } + AffineTransformOp op = new AffineTransformOp(new AffineTransform(0.5, 0, 0, 0.5, 0, 0), + AffineTransformOp.TYPE_BILINEAR); + BufferedImage biOut = new BufferedImage(biWidth / 2, biHeight / 2, BufferedImage.TYPE_3BYTE_BGR); + op.filter(bi, biOut); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(biOut.getWidth() * biOut.getHeight() * 4); + writer.processImage(biOut, buffer); + g.dispose(); + return buffer.toByteArray(); + } + + private void shrinkFrom(ZoomLevel source) { + try { + TileImageDataWriter writer = new TileImageJpegDataWriter(0.9); + writer.initialize(); + writeHeader(); + atlasProgress.initMapCreation(xTiles * yTiles); + for (int x = 0; x < xTiles; x++) { + for (int y = 0; y < yTiles; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + jpegOffsets[x][y] = rmapFile.getFilePointer(); + byte[] tileData = getTileData(writer, source, 2 * x, 2 * y); + rmapFile.seek(jpegOffsets[x][y]); + log.trace(String.format("Writing shrunken jpeg (%d,%d,%d) at offset %d", index, x, y, + jpegOffsets[x][y])); + rmapFile.writeIntI(7); + rmapFile.writeIntI(tileData.length); + rmapFile.write(tileData); + tileData = null; + } + } + } catch (Exception e) { + log.error("Failed generating ZoomLevel " + index + ": " + e.getMessage()); + } + } + } + + private class RmapFile extends RandomAccessFile { + + private String name = ""; + private int width = 0; + private int height = 0; + private int tileWidth = 0; + private int tileHeight = 0; + private double longitudeMin = 0; + private double longitudeMax = 0; + private double latitudeMin = 0; + private double latitudeMax = 0; + private long mapDataOffset = 0; + private ZoomLevel zoomLevels[] = null; + + private RmapFile(File file) throws FileNotFoundException { + super(file, "rw"); + this.name = file.getName(); + } + + private int readIntI() throws IOException { + int ch1 = this.read(); + int ch2 = this.read(); + int ch3 = this.read(); + int ch4 = this.read(); + if ((ch1 | ch2 | ch3 | ch4) < 0) { + throw new IOException(); + } + return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0)); + } + + private void writeIntI(int i) throws IOException { + write((i >>> 0) & 0xFF); + write((i >>> 8) & 0xFF); + write((i >>> 16) & 0xFF); + write((i >>> 24) & 0xFF); + } + + private void writeLongI(long l) throws IOException { + write((int) (l >>> 0) & 0xFF); + write((int) (l >>> 8) & 0xFF); + write((int) (l >>> 16) & 0xFF); + write((int) (l >>> 24) & 0xFF); + write((int) (l >>> 32) & 0xFF); + write((int) (l >>> 40) & 0xFF); + write((int) (l >>> 48) & 0xFF); + write((int) (l >>> 56) & 0xFF); + } + + private void writeHeader() throws IOException { + log.trace("Writing rmap header"); + if (zoomLevels == null) { + throw new IOException("zoomLevels == null"); + } + seek(0); + write("CompeGPSRasterImage".getBytes()); + writeIntI(10); + writeIntI(7); + writeIntI(0); + writeIntI(width); + writeIntI(-height); + writeIntI(24); + writeIntI(1); + writeIntI(tileWidth); + writeIntI(tileHeight); + writeLongI(mapDataOffset); + writeIntI(0); + writeIntI(zoomLevels.length); + for (int n = 0; n < zoomLevels.length; n++) { + writeLongI(zoomLevels[n].offset); + } + } + + private void writeMapInfo() throws IOException { + if (mapDataOffset == 0) { + mapDataOffset = getFilePointer(); + } else { + seek(mapDataOffset); + } + log.trace("Writing MAP data at offset %d" + mapDataOffset); + StringBuffer sbMap = new StringBuffer(); + sbMap.append("CompeGPS MAP File\r\n"); + sbMap.append("
\r\n"); + sbMap.append("Version=2\r\n"); + sbMap.append("VerCompeGPS=MOBAC\r\n"); + sbMap.append("Projection=2,Mercator,\r\n"); + sbMap.append("Coordinates=1\r\n"); + sbMap.append("Datum=WGS 84\r\n"); + sbMap.append("
\r\n"); + sbMap.append("\r\n"); + sbMap.append("Bitmap=" + name + "\r\n"); + sbMap.append("BitsPerPixel=0\r\n"); + sbMap.append(String.format("BitmapWidth=%d\r\n", width)); + sbMap.append(String.format("BitmapHeight=%d\r\n", height)); + sbMap.append("Type=10\r\n"); + sbMap.append("\r\n"); + sbMap.append("\r\n"); + String pointLine = "P%d=%d,%d,A,%s,%s\r\n"; + DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); + df.applyPattern("#0.00000000"); + sbMap.append(String.format(pointLine, 0, 0, 0, df.format(longitudeMin), df.format(latitudeMax))); + sbMap.append(String.format(pointLine, 1, width - 1, 0, df.format(longitudeMax), df.format(latitudeMax))); + sbMap.append(String.format(pointLine, 2, width - 1, height - 1, df.format(longitudeMax), + df.format(latitudeMin))); + sbMap.append(String.format(pointLine, 3, 0, height - 1, df.format(longitudeMin), df.format(latitudeMin))); + sbMap.append("\r\n"); + sbMap.append("\r\n"); + String polyLine = "M%d=%d,%d\r\n"; + sbMap.append(String.format(polyLine, 0, 0, 0)); + sbMap.append(String.format(polyLine, 1, width, 0)); + sbMap.append(String.format(polyLine, 2, width, height)); + sbMap.append(String.format(polyLine, 3, 0, height)); + sbMap.append("\r\n"); + writeIntI(1); + writeIntI(sbMap.length()); + write(sbMap.toString().getBytes()); + } + } + + // ************************************************************ + + public TwoNavRMAP() { + super(); + log.setLevel(Level.TRACE); + } + + @Override + protected void testAtlas() throws AtlasTestException { + for (LayerInterface layer : atlas) { + MapInterface map0 = layer.getMap(0); + MapSpace mapSpace0 = map0.getMapSource().getMapSpace(); + double longitudeMin = mapSpace0.cXToLon(map0.getMinTileCoordinate().x, map0.getZoom()); + double longitudeMax = mapSpace0.cXToLon(map0.getMaxTileCoordinate().x + 1, map0.getZoom()); + double latitudeMin = mapSpace0.cYToLat(map0.getMaxTileCoordinate().y + 1, map0.getZoom()); + double latitudeMax = mapSpace0.cYToLat(map0.getMinTileCoordinate().y, map0.getZoom()); + for (int n = 1; n < layer.getMapCount(); n++) { + MapInterface mapN = layer.getMap(n); + MapSpace mapSpaceN = mapN.getMapSource().getMapSpace(); + + double longitudeMinN = mapSpaceN.cXToLon(mapN.getMinTileCoordinate().x, mapN.getZoom()); + double longitudeMaxN = mapSpaceN.cXToLon(mapN.getMaxTileCoordinate().x + 1, mapN.getZoom()); + double latitudeMinN = mapSpaceN.cYToLat(mapN.getMaxTileCoordinate().y + 1, mapN.getZoom()); + double latitudeMaxN = mapSpaceN.cYToLat(mapN.getMinTileCoordinate().y, mapN.getZoom()); + if ((longitudeMin != longitudeMinN) || (longitudeMax != longitudeMaxN) || (latitudeMin != latitudeMinN) + || (latitudeMax != latitudeMaxN)) { + throw new AtlasTestException("All maps in one layer have to cover the same area!\n" + + "Use grid zoom on the lowest zoom level to get an acceptable result."); + } + for (int m = 0; m < layer.getMapCount(); m++) { + if ((mapN.getZoom() == layer.getMap(m).getZoom()) && (m != n)) { + throw new AtlasTestException("Several maps with the same zoom level within the same layer " + + "are not supported!"); + } + } + } + } + } + + public boolean testMapSource(MapSource mapSource) { + MapSpace mapSpace = mapSource.getMapSpace(); + return (mapSpace instanceof MercatorPower2MapSpace && ProjectionCategory.SPHERE.equals(mapSpace + .getProjectionCategory())); + } + + @Override + public void initLayerCreation(LayerInterface layer) throws IOException { + if (rmapFile != null) + throw new RuntimeException("Layer mismatch - last layer has not been finished correctly!"); + + super.initLayerCreation(layer); + // Logging.configureConsoleLogging(org.apache.log4j.Level.ALL, new SimpleLayout()); + + rmapFile = new RmapFile(new File(atlasDir, layer.getName() + ".rmap")); + + int DefaultMap = 0; + + rmapFile.width = 0; + rmapFile.height = 0; + for (int n = 0; n < layer.getMapCount(); n++) { + int width = layer.getMap(n).getMaxTileCoordinate().x - layer.getMap(n).getMinTileCoordinate().x + 1; + int height = layer.getMap(n).getMaxTileCoordinate().y - layer.getMap(n).getMinTileCoordinate().y + 1; + if ((width > rmapFile.width) || (height > rmapFile.height)) { + rmapFile.width = width; + rmapFile.height = height; + DefaultMap = n; + } + } + + log.trace("rmap width = " + rmapFile.width); + log.trace("rmap height = " + rmapFile.height); + + rmapFile.tileWidth = layer.getMap(DefaultMap).getTileSize().width; + rmapFile.tileHeight = layer.getMap(DefaultMap).getTileSize().height; + + log.trace("rmap tileWidth = " + rmapFile.tileWidth); + log.trace("rmap tileHeight = " + rmapFile.tileHeight); + + MapSpace mapSpace = layer.getMap(DefaultMap).getMapSource().getMapSpace(); + rmapFile.longitudeMin = mapSpace.cXToLon(layer.getMap(DefaultMap).getMinTileCoordinate().x, + layer.getMap(DefaultMap).getZoom()); + rmapFile.longitudeMax = mapSpace.cXToLon(layer.getMap(DefaultMap).getMaxTileCoordinate().x, + layer.getMap(DefaultMap).getZoom()); + rmapFile.latitudeMin = mapSpace.cYToLat(layer.getMap(DefaultMap).getMaxTileCoordinate().y, + layer.getMap(DefaultMap).getZoom()); + rmapFile.latitudeMax = mapSpace.cYToLat(layer.getMap(DefaultMap).getMinTileCoordinate().y, + layer.getMap(DefaultMap).getZoom()); + + log.trace("rmap longitudeMin = " + rmapFile.longitudeMin); + log.trace("rmap longitudeMax = " + rmapFile.longitudeMax); + log.trace("rmap latitudeMin = " + rmapFile.latitudeMin); + log.trace("rmap latitudeMax = " + rmapFile.latitudeMax); + + double width = rmapFile.width; + double height = rmapFile.height; + int count = 1; + while ((width >= 256.0) || (height >= 256.0)) { + width = Math.ceil(width / 2.0); + height = Math.ceil(height / 2.0); + count++; + } + + log.trace("rmap zoomLevels count = " + count); + + rmapFile.zoomLevels = new ZoomLevel[count]; + width = rmapFile.width; + height = rmapFile.height; + for (int n = 0; n < rmapFile.zoomLevels.length; n++) { + rmapFile.zoomLevels[n] = new ZoomLevel(); + rmapFile.zoomLevels[n].index = n; + rmapFile.zoomLevels[n].width = (int) Math.round(width); + rmapFile.zoomLevels[n].height = (int) Math.round(height); + rmapFile.zoomLevels[n].xTiles = (int) Math.ceil((double) rmapFile.zoomLevels[n].width + / (double) rmapFile.tileWidth); + rmapFile.zoomLevels[n].yTiles = (int) Math.ceil((double) rmapFile.zoomLevels[n].height + / (double) rmapFile.tileHeight); + rmapFile.zoomLevels[n].jpegOffsets = new long[rmapFile.zoomLevels[n].xTiles][rmapFile.zoomLevels[n].yTiles]; + rmapFile.zoomLevels[n].zoom = layer.getMap(DefaultMap).getZoom() - n; + rmapFile.zoomLevels[n].dl = false; + for (int m = 0; m < layer.getMapCount(); m++) { + if ((rmapFile.zoomLevels[n].zoom == layer.getMap(m).getZoom())) { + rmapFile.zoomLevels[n].dl = true; + } + } + width = Math.ceil(width / 2.0); + height = Math.ceil(height / 2.0); + } + + for (int n = 0; n < rmapFile.zoomLevels.length; n++) { + log.trace(String.format("zoomLevels[%d] zoom=%d %dx%d pixels, %dx%d tiles %s", + rmapFile.zoomLevels[n].index, rmapFile.zoomLevels[n].zoom, rmapFile.zoomLevels[n].width, + rmapFile.zoomLevels[n].height, rmapFile.zoomLevels[n].xTiles, rmapFile.zoomLevels[n].yTiles, + rmapFile.zoomLevels[n].dl == false ? "calc" : "dl")); + } + + rmapFile.writeHeader(); + + } + + public void createMap() throws MapCreationException, InterruptedException { + try { + + int index; + for (index = 0; index < rmapFile.zoomLevels.length; index++) { + if (rmapFile.zoomLevels[index].zoom == map.getZoom()) { + break; + } + } + if (index == rmapFile.zoomLevels.length) { + throw new MapCreationException("Map not found in the zoomLevels list", map); + } + try { + rmapFile.zoomLevels[index].writeHeader(); + } catch (IOException e) { + throw new MapCreationException("rmapFile.zoomLevels[Index].writeHeader() failed: " + e.getMessage(), + map, e); + } + + int tilex = 0; + int tiley = 0; + + atlasProgress.initMapCreation((xMax - xMin + 1) * (yMax - yMin + 1)); + + if ((map.getMapSource().getTileImageType() != TileImageType.JPG) || (map.getParameters() != null)) { + // Tiles have to be converted to jpeg format + TileImageFormat imageFormat = TileImageFormat.JPEG90; + if (map.getParameters() != null) + imageFormat = map.getParameters().getFormat(); + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, imageFormat); + } + + ImageIO.setUseCache(false); + byte[] emptyTileData = Utilities.createEmptyTileData(mapSource); + for (int x = xMin; x <= xMax; x++) { + tiley = 0; + for (int y = yMin; y <= yMax; y++) { + checkUserAbort(); + atlasProgress.incMapCreationProgress(); + try { + // Remember offset to tile + rmapFile.zoomLevels[index].jpegOffsets[tilex][tiley] = rmapFile.getFilePointer(); + byte[] sourceTileData = mapDlTileProvider.getTileData(x, y); + if (sourceTileData != null) { + rmapFile.writeIntI(7); + rmapFile.writeIntI(sourceTileData.length); + rmapFile.write(sourceTileData); + } else { + log.trace(String.format("Tile x=%d y=%d not found in tile archive - creating default", + tilex, tiley)); + rmapFile.writeIntI(7); + rmapFile.writeIntI(emptyTileData.length); + rmapFile.write(emptyTileData); + } + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + tiley++; + } + tilex++; + } + + } catch (MapCreationException e) { + throw e; + } catch (Exception e) { + throw new MapCreationException(map, e); + } + } + + @Override + public void abortAtlasCreation() throws IOException { + try { + rmapFile.setLength(0); + } finally { + Utilities.closeFile(rmapFile); + } + super.abortAtlasCreation(); + } + + @Override + public void finishLayerCreation() throws IOException { + try { + for (int n = 0; n < rmapFile.zoomLevels.length; n++) { + if (rmapFile.zoomLevels[n].offset == 0) { + if (n == 0) { + throw new IOException("Missing top level map"); + } + rmapFile.zoomLevels[n].shrinkFrom(rmapFile.zoomLevels[n - 1]); + } + } + rmapFile.writeMapInfo(); + rmapFile.writeHeader(); + for (int n = 0; n < rmapFile.zoomLevels.length; n++) { + rmapFile.zoomLevels[n].writeHeader(); + } + rmapFile.close(); + } catch (IOException e) { + log.error("Failed writing rmap file \"" + rmapFile.name + "\": " + e.getMessage(), e); + abortAtlasCreation(); + throw e; + } + rmapFile = null; + super.finishLayerCreation(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/Ublox.java b/src/main/java/mobac/program/atlascreators/Ublox.java new file mode 100644 index 0000000..62fd351 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Ublox.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Locale; + +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.Utilities; + + +@AtlasCreatorName("Ublox") +public class Ublox extends Ozi { + + @Override + protected void writeMapFile() { + FileOutputStream fout = null; + try { + fout = new FileOutputStream(new File(layerDir, mapName + ".mcf")); + writeMapFile(map.getName(), fout); + } catch (Exception e) { + log.error("", e); + } finally { + Utilities.closeStream(fout); + } + } + + @Override + protected void writeMapFile(String imageFileName, OutputStream stream) throws IOException { + log.trace("Writing mcf file"); + OutputStreamWriter mcfWriter = new OutputStreamWriter(stream, TEXT_FILE_CHARSET); + + MapSpace mapSpace = mapSource.getMapSpace(); + + double longitudeMin = mapSpace.cXToLon(xMin * tileSize, zoom); + double longitudeMax = mapSpace.cXToLon((xMax + 1) * tileSize - 1, zoom); + double latitudeMin = mapSpace.cYToLat((yMax + 1) * tileSize - 1, zoom); + double latitudeMax = mapSpace.cYToLat(yMin * tileSize, zoom); + + int width = (xMax - xMin + 1) * tileSize; + int height = (yMax - yMin + 1) * tileSize; + + String refFmt = "%d = %5d, %5d, %10.6f, %10.6f\r\n"; + + mcfWriter.write("; I N F O\r\n"); + mcfWriter.write("; ------------------------------------------------------------\r\n"); + mcfWriter.write("; File: " + imageFileName + ".mcf\r\n"); + mcfWriter.write("; Source: " + map.getMapSource().getName() + "\r\n"); + mcfWriter.write("\r\n"); + mcfWriter.write("; R E F E R E N C E\r\n"); + mcfWriter.write("; ------------------------------------------------------------\r\n"); + mcfWriter.write("; 3 Points must be defined to calibrate a Map\r\n"); + mcfWriter.write("; Parameters:\r\n"); + mcfWriter.write("; # = index of the point (1 to 3)\r\n"); + mcfWriter.write("; x,y = image coordinates\r\n"); + mcfWriter.write("; lat,lon = world coordinates\r\n"); + mcfWriter.write("; Syntax:\r\n"); + mcfWriter.write("; # = , , , \r\n"); + mcfWriter.write("\r\n"); + + mcfWriter.write("[REFERENCE]\r\n"); + mcfWriter.write(String.format(Locale.ENGLISH, refFmt, 1, 0, 0, longitudeMin, latitudeMax)); + mcfWriter.write(String.format(Locale.ENGLISH, refFmt, 2, width, height, longitudeMax, + latitudeMin)); + mcfWriter.write(String.format(Locale.ENGLISH, refFmt, 3, width, 0, longitudeMax, + latitudeMax)); + mcfWriter.flush(); + } +} diff --git a/src/main/java/mobac/program/atlascreators/Viewranger.java b/src/main/java/mobac/program/atlascreators/Viewranger.java new file mode 100644 index 0000000..3571d53 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/Viewranger.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators; + +import java.io.File; + +import mobac.exceptions.AtlasTestException; +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.annotations.SupportedParameters; +import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider; +import mobac.program.atlascreators.tileprovider.PngTileProvider; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.model.TileImageParameters.Name; +import mobac.program.model.TileImageType; + +/** + * http://sourceforge.net/p/mobac/feature-requests/219/ + */ +@AtlasCreatorName("Viewranger") +@SupportedParameters(names = { Name.format_png }) +public class Viewranger extends OSMTracker { + + public Viewranger() { + super(); + tileFileNamePattern = "%d/%d/%d"; + } + + @Override + protected void testAtlas() throws AtlasTestException { + for (LayerInterface layer : atlas) { + for (MapInterface map : layer) { + if (map.getParameters() == null) + continue; + if (!TileImageType.PNG.equals(map.getParameters().getFormat().getType())) + throw new AtlasTestException("Only PNG output format allowed for Viewranger", map); + if (map.getZoom() > 18) + throw new AtlasTestException("Unsupported zoom level: " + map.getZoom() + + "\nMaximum supported zoom level is 18", map); + if (map.getZoom() < 3) + throw new AtlasTestException("Unsupported zoom level: " + map.getZoom() + + "\nMinimum suupported toom level is 3", map); + } + } + } + + @Override + public void initializeMap(MapInterface map, TileProvider mapTileProvider) { + super.initializeMap(map, mapTileProvider); + mapDir = new File(atlasDir, map.getLayer().getName()); + tileType = ""; + if (parameters == null) + mapDlTileProvider = new PngTileProvider(mapDlTileProvider); + else + mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, parameters.getFormat()); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/MapTileBuilder.java b/src/main/java/mobac/program/atlascreators/impl/MapTileBuilder.java new file mode 100644 index 0000000..fc870e8 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/MapTileBuilder.java @@ -0,0 +1,268 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.exceptions.MapCreationException; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageParameters; +import mobac.utilities.MyMath; + +import org.apache.log4j.Logger; + +public class MapTileBuilder { + + private static final Logger log = Logger.getLogger(MapTileBuilder.class); + + private final AtlasCreator atlasCreator; + private final TileProvider mapDlTileProvider; + private final MapInterface map; + private final MapSource mapSource; + private final TileImageParameters parameters; + private final TileImageDataWriter tileImageDataWriter; + private final int tileSize; + private final int xMin; + private final int xMax; + private final int yMin; + private final int yMax; + + private final boolean useRealTileSize; + + private int realWidth; + private int realHeight; + int mergedWidth; + int mergedHeight; + + final int customTileCount; + final int xStart; + final int yStart; + final int xEnd; + final int yEnd; + + protected final MapTileWriter mapTileWriter; + + /** + * @param atlasCreator + * @param mapTileWriter + * @param useRealTileSize + * affects the tile size at the left and bottom border of the map. If true those tile will + * have the size of the remaining image data available size (they can be smaller than the size + * specified). If false the tile size will be as specified by the map's + * {@link TileImageParameters}. + */ + public MapTileBuilder(AtlasCreator atlasCreator, MapTileWriter mapTileWriter, boolean useRealTileSize) { + this(atlasCreator, atlasCreator.getParameters().getFormat().getDataWriter(), mapTileWriter, useRealTileSize); + } + + /** + * + * @param atlasCreator + * @param tileImageDataWriter + * @param mapTileWriter + * @param useRealTileSize + * affects the tile size at the left and bottom border of the map. If true those tile will + * have the size of the remaining image data available size (they can be smaller than the size + * specified). If false the tile size will be as specified by the map's + * {@link TileImageParameters}. + */ + public MapTileBuilder(AtlasCreator atlasCreator, TileImageDataWriter tileImageDataWriter, + MapTileWriter mapTileWriter, boolean useRealTileSize) { + this.atlasCreator = atlasCreator; + this.tileImageDataWriter = tileImageDataWriter; + this.mapTileWriter = mapTileWriter; + this.mapDlTileProvider = atlasCreator.getMapDlTileProvider(); + this.useRealTileSize = useRealTileSize; + map = atlasCreator.getMap(); + mapSource = map.getMapSource(); + tileSize = mapSource.getMapSpace().getTileSize(); + xMax = atlasCreator.getXMax(); + xMin = atlasCreator.getXMin(); + yMax = atlasCreator.getYMax(); + yMin = atlasCreator.getYMin(); + parameters = atlasCreator.getParameters(); + // left upper point on the map in pixels + // regarding the current zoom level + xStart = xMin * tileSize; + yStart = yMin * tileSize; + + // lower right point on the map in pixels + // regarding the current zoom level + xEnd = xMax * tileSize + (tileSize - 1); + yEnd = yMax * tileSize + (tileSize - 1); + + mergedWidth = xEnd - xStart + 1; + mergedHeight = yEnd - yStart + 1; + + realWidth = parameters.getWidth(); + realHeight = parameters.getHeight(); + if (useRealTileSize) { + // Reduce tile size of overall map height/width + // if it is smaller than one tile + if (realWidth > mergedWidth) + realWidth = mergedWidth; + if (realHeight > mergedHeight) + realHeight = mergedHeight; + } + customTileCount = MyMath.divCeil(mergedWidth, realWidth) * MyMath.divCeil(mergedHeight, realHeight); + } + + public void createTiles() throws MapCreationException, InterruptedException { + + // Absolute positions + int xAbsPos = xStart; + int yAbsPos = yStart; + + log.trace("tile size: " + realWidth + " * " + realHeight); + log.trace("X: from " + xStart + " to " + xEnd); + log.trace("Y: from " + yStart + " to " + yEnd); + + // We don't work with large images, therefore we can disable the (file) + // cache of ImageIO. This will speed up the creation process a bit + ImageIO.setUseCache(false); + ByteArrayOutputStream buf = new ByteArrayOutputStream(32768); + tileImageDataWriter.initialize(); + int currentTileHeight = realHeight; + int currentTileWidth = realWidth; + try { + String tileType = tileImageDataWriter.getType().getFileExt(); + int tiley = 0; + while (yAbsPos < yEnd) { + int tilex = 0; + xAbsPos = xStart; + if (useRealTileSize) + currentTileHeight = Math.min(realHeight, yEnd - yAbsPos + 1); + while (xAbsPos < xEnd) { + if (useRealTileSize) + currentTileWidth = Math.min(realWidth, xEnd - xAbsPos + 1); + atlasCreator.checkUserAbort(); + atlasCreator.getAtlasProgress().incMapCreationProgress(); + BufferedImage tileImage = new BufferedImage(currentTileWidth, currentTileHeight, + BufferedImage.TYPE_3BYTE_BGR); + buf.reset(); + try { + Graphics2D graphics = tileImage.createGraphics(); + prepareTile(graphics); + paintCustomTile(graphics, xAbsPos, yAbsPos); + graphics.dispose(); + tileImageDataWriter.processImage(tileImage, buf); + mapTileWriter.writeTile(tilex, tiley, tileType, buf.toByteArray()); + } catch (IOException e) { + throw new MapCreationException("Error writing tile image: " + e.getMessage(), map, e); + } + + tilex++; + xAbsPos += realWidth; + } + tiley++; + yAbsPos += realHeight; + } + } finally { + tileImageDataWriter.dispose(); + } + } + + protected void prepareTile(Graphics2D graphics) { + graphics.setColor(mapSource.getBackgroundColor()); + graphics.fillRect(0, 0, realWidth, realHeight); + } + + /** + * Paints the graphics of the custom tile specified by the pixel coordinates xAbsPos and + * yAbsPos on the currently selected map & layer. + * + * @param graphics + * @param xAbsPos + * @param yAbsPos + */ + private void paintCustomTile(Graphics2D graphics, int xAbsPos, int yAbsPos) { + int xTile = xAbsPos / tileSize; + int xTileOffset = -(xAbsPos % tileSize); + + for (int x = xTileOffset; x < realWidth; x += tileSize) { + int yTile = yAbsPos / tileSize; + int yTileOffset = -(yAbsPos % tileSize); + for (int y = yTileOffset; y < realHeight; y += tileSize) { + try { + BufferedImage orgTileImage = loadOriginalMapTile(xTile, yTile); + if (orgTileImage != null) { + //int w = orgTileImage.getWidth(); + //int h = orgTileImage.getHeight(); + graphics.drawImage(orgTileImage, xTileOffset, yTileOffset, tileSize, tileSize, null); + } + } catch (Exception e) { + log.error("Error while painting sub-tile", e); + } + yTile++; + yTileOffset += tileSize; + } + xTile++; + xTileOffset += tileSize; + } + } + + public int getCustomTileCount() { + return customTileCount; + } + + /** + * A simple local cache holding the last 10 loaded original tiles. If the custom tile size is smaller than 256x256 + * the efficiency of this cache is very high (~ 75% hit rate). + */ + private CachedTile[] cache = new CachedTile[10]; + private int cachePos = 0; + + private BufferedImage loadOriginalMapTile(int xTile, int yTile) throws Exception { + for (CachedTile ct : cache) { + if (ct == null) + continue; + if (ct.xTile == xTile && ct.yTile == yTile) { + // log.trace("cache hit"); + return ct.image; + } + } + // log.trace("cache miss"); + BufferedImage image = mapDlTileProvider.getTileImage(xTile, yTile); + if (image == null) + return null; + cache[cachePos] = new CachedTile(image, xTile, yTile); + cachePos = (cachePos + 1) % cache.length; + return image; + } + + private static class CachedTile { + BufferedImage image; + int xTile; + int yTile; + + public CachedTile(BufferedImage image, int tile, int tile2) { + super(); + this.image = image; + xTile = tile; + yTile = tile2; + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/MapTileWriter.java b/src/main/java/mobac/program/atlascreators/impl/MapTileWriter.java new file mode 100644 index 0000000..fed9936 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/MapTileWriter.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl; + +import java.io.IOException; + +public interface MapTileWriter { + + /** + * + * @param tilex + * x tile number regarding regarding the currently processed map + * (0..mapWidth / tileWidth)] + * @param tiley + * y tile number regarding regarding the currently processed map + * (0..mapheight / tileHeight)] + * @param tileType + * @param tileData + * @throws IOException + */ + public void writeTile(int tilex, int tiley, String tileType, byte[] tileData) + throws IOException; + + public void finalizeMap() throws IOException; + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/aqm/FlatPackCreator.java b/src/main/java/mobac/program/atlascreators/impl/aqm/FlatPackCreator.java new file mode 100644 index 0000000..2649da8 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/aqm/FlatPackCreator.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl.aqm; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; + +public class FlatPackCreator { + public static final String FLAT_PACK_HEADER = "FLATPACK1"; + public static final int FILE_COPY_BUFFER_LEN = 4096 * 10; + + private String packPath = null; + + private FileOutputStream dataStream = null; + + private ByteArrayOutputStream structBuffer = null; + + private OutputStreamWriter structBufferWriter = null; + + private long currentDataWritedSize = 0; + + private long currentNbFiles = 0; + + public FlatPackCreator(final File packPath) throws FileNotFoundException { + this(packPath.getAbsolutePath()); + } + + public FlatPackCreator(final String packPath) throws FileNotFoundException { + this.packPath = packPath; + + if (packPath == null) + throw new NullPointerException("Pack file path is null."); + + dataStream = new FileOutputStream(packPath + ".tmp"); + structBuffer = new ByteArrayOutputStream(); + structBufferWriter = new OutputStreamWriter(structBuffer, Charsets.ISO_8859_1); + + currentDataWritedSize = 0; + currentNbFiles = 0; + } + + public final void add(final String filePath, final String fileEntryName) throws IOException { + add(new File(filePath), fileEntryName); + } + + public final void add(final File filePath) throws IOException { + add(filePath, filePath.getName()); + } + + public final void add(final File filePath, final String fileEntryName) throws IOException { + // read file content + FileInputStream in = new FileInputStream(filePath); + byte[] buff = new byte[(int) filePath.length()]; + int read = in.read(buff); + in.close(); + + if (filePath.length() != read) + throw new IOException("Error reading '" + filePath + "'."); + + add(buff, fileEntryName); + } + + public final void add(final byte[] buff, final String fileEntryName) throws IOException { + if (dataStream == null) + throw new IOException("Write stream is null."); + + // write file size + String fileSize = Integer.toString(buff.length) + "\0"; + dataStream.write(fileSize.getBytes(Charsets.ISO_8859_1)); + + // write file into pack data + if (buff.length > 0) + dataStream.write(buff); + + // write file into pack structure + structBufferWriter.append(fileEntryName + "\0" + currentDataWritedSize + "\0"); + + // update writed size + currentDataWritedSize += buff.length + fileSize.length(); + currentNbFiles++; + } + + public final void close() throws IOException { + if (dataStream == null) + throw new NullPointerException("Write stream is null."); + + // close data file + dataStream.flush(); + dataStream.close(); + dataStream = null; + + File tmpFile = new File(packPath + ".tmp"); + // open pack file + FileOutputStream packStream = new FileOutputStream(packPath); + try { + String nbFiles = Long.toString(currentNbFiles) + "\0"; + + // write header + packStream.write(FLAT_PACK_HEADER.getBytes(Charsets.ISO_8859_1)); + + // write struct + structBufferWriter.flush(); + structBufferWriter.close(); + int headerSize = structBuffer.size() + nbFiles.length(); + packStream.write(Integer.toString(headerSize).getBytes(Charsets.ISO_8859_1)); + packStream.write('\0'); + + packStream.write(nbFiles.getBytes(Charsets.ISO_8859_1)); + + structBuffer.writeTo(packStream); + + // Free memory + structBufferWriter = null; + structBuffer = null; + + // write data + FileInputStream in = new FileInputStream(tmpFile); + try { + byte[] buffer = new byte[FILE_COPY_BUFFER_LEN]; + + int read; + while ((read = in.read(buffer)) > 0) + packStream.write(buffer, 0, read); + + packStream.flush(); + packStream.close(); + } finally { + Utilities.closeStream(in); + } + } finally { + Utilities.closeStream(packStream); + } + + // delete temp file + if (tmpFile.isFile()) + Utilities.deleteFile(tmpFile); + + // reset state + packPath = null; + structBuffer = null; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/gemf/GEMFFileCreator.java b/src/main/java/mobac/program/atlascreators/impl/gemf/GEMFFileCreator.java new file mode 100644 index 0000000..ab36e49 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/gemf/GEMFFileCreator.java @@ -0,0 +1,391 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * + */ +package mobac.program.atlascreators.impl.gemf; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.TreeSet; + +import org.apache.log4j.Logger; + +/** + * GEMF File creator class. + * + * Reference about GEMF format: https://sites.google.com/site/abudden/android-map-store + * + * @author A. S. Budden + * @author Erik Burrows + * + * This class is a stripped-down version of the GEMFFile.java class at: + * http://code.google.com/p/osmdroid/source/ + * browse/trunk/osmdroid-android/src/main/java/org/osmdroid/util/GEMFFile.java + * + * (Date: Wed, 11th of April 2012) + * + * The original GEMFFile.java from above has been reduced to the functionality of writing a GEMF archive, as + * reading the archive seems not to be necessary. + * + * @author M. Reiter + * + */ +public class GEMFFileCreator { + private static final long FILE_SIZE_LIMIT = 1 * 1024 * 1024 * 1024; // 1GB + private static final int FILE_COPY_BUFFER_SIZE = 1024; + + private static final int VERSION = 4; + private static final int TILE_SIZE = 256; + + private static final int U32_SIZE = 4; + private static final int U64_SIZE = 8; + + /* + * Constructor to create new GEMF file from directory of sources/tiles. + * + * @param pLocation String object representing path to first GEMF archive file. Additional files (if archive size + * exceeds FILE_SIZE_LIMIT will be created with numerical suffixes, eg: test.gemf-1, test.gemf-2. + * + * @param pSourceFolders Each specified folder will be imported into the GEMF archive as a seperate source. The name + * of the folder will be the name of the source in the archive. + */ + public GEMFFileCreator(final String pLocation, final List pSourceFolders, Logger log) throws FileNotFoundException, + IOException { + + /** + *
+		 * 1. For each source folder
+		 *   1. Create array of zoom levels, X rows, Y rows
+		 * 2. Build index data structure index[source][zoom][range]
+		 *   1. For each S-Z-X find list of Ys values
+		 *   2. For each S-Z-X-Ys set, find complete X ranges
+		 *   3. For each S-Z-Xr-Ys set, find complete Y ranges, create Range record
+		 * 3. Write out index
+		 *   1. Header
+		 *   2. Sources
+		 *   3. For each Range
+		 *     1. Write Range record
+		 * 4. For each Range record
+		 *   1. For each Range entry
+		 *     1. If over file size limit, start new data file
+		 *   2. Write tile data
+		 * 
+ */ + + // this.mLocation = pLocation; + + // Create in-memory array of sources, X and Y values. + final LinkedHashMap>>> dirIndex = new LinkedHashMap>>>(); + + for (final File sourceDir : pSourceFolders) { + + final LinkedHashMap>> zList = new LinkedHashMap>>(); + + for (final File zDir : sourceDir.listFiles()) { + // Make sure the directory name is just a number + try { + Integer.parseInt(zDir.getName()); + } catch (final NumberFormatException e) { + continue; + } + + final LinkedHashMap> xList = new LinkedHashMap>(); + + for (final File xDir : zDir.listFiles()) { + + // Make sure the directory name is just a number + try { + Integer.parseInt(xDir.getName()); + } catch (final NumberFormatException e) { + continue; + } + + final LinkedHashMap yList = new LinkedHashMap(); + for (final File yFile : xDir.listFiles()) { + + try { + Integer.parseInt(yFile.getName().substring(0, yFile.getName().indexOf('.'))); + } catch (final NumberFormatException e) { + continue; + } + + yList.put(Integer.parseInt(yFile.getName().substring(0, yFile.getName().indexOf('.'))), yFile); + } + + xList.put(new Integer(xDir.getName()), yList); + } + + zList.put(Integer.parseInt(zDir.getName()), xList); + } + + dirIndex.put(sourceDir.getName(), zList); + } + + // Create a source index list + final LinkedHashMap sourceIndex = new LinkedHashMap(); + final LinkedHashMap indexSource = new LinkedHashMap(); + int si = 0; + for (final String source : dirIndex.keySet()) { + sourceIndex.put(source, new Integer(si)); + indexSource.put(new Integer(si), source); + ++si; + } + + // Create the range objects + final List ranges = new ArrayList(); + + for (final String source : dirIndex.keySet()) { + for (final Integer zoom : dirIndex.get(source).keySet()) { + + // Get non-contiguous Y sets for each Z/X + final LinkedHashMap, List> ySets = new LinkedHashMap, List>(); + + for (final Integer x : new TreeSet(dirIndex.get(source).get(zoom).keySet())) { + + final List ySet = new ArrayList(); + for (final Integer y : dirIndex.get(source).get(zoom).get(x).keySet()) { + ySet.add(y); + } + + if (ySet.size() == 0) { + continue; + } + + Collections.sort(ySet); + + if (!ySets.containsKey(ySet)) { + ySets.put(ySet, new ArrayList()); + } + + ySets.get(ySet).add(x); + } + + // For each Y set find contiguous X sets + final LinkedHashMap, List> xSets = new LinkedHashMap, List>(); + + for (final List ySet : ySets.keySet()) { + + final TreeSet xList = new TreeSet(ySets.get(ySet)); + + List xSet = new ArrayList(); + for (int i = xList.first(); i < xList.last() + 1; ++i) { + if (xList.contains(new Integer(i))) { + xSet.add(new Integer(i)); + } else { + if (xSet.size() > 0) { + xSets.put(ySet, xSet); + xSet = new ArrayList(); + } + } + } + + if (xSet.size() > 0) { + xSets.put(ySet, xSet); + } + } + + // For each contiguous X set, find contiguous Y sets and create GEMFRange object + for (final List xSet : xSets.keySet()) { + + final TreeSet yList = new TreeSet(xSet); + final TreeSet xList = new TreeSet(ySets.get(xSet)); + + GEMFRange range = new GEMFRange(); + range.zoom = zoom; + range.sourceIndex = sourceIndex.get(source); + range.xMin = xList.first(); + range.xMax = xList.last(); + + for (int i = yList.first(); i < yList.last() + 1; ++i) { + if (yList.contains(new Integer(i))) { + if (range.yMin == null) { + range.yMin = i; + } + range.yMax = i; + } else { + + if (range.yMin != null) { + ranges.add(range); + + range = new GEMFRange(); + range.zoom = zoom; + range.sourceIndex = sourceIndex.get(source); + range.xMin = xList.first(); + range.xMax = xList.last(); + } + } + } + + if (range.yMin != null) { + ranges.add(range); + } + } + } + } + + // Calculate size of header for computation of data offsets + int source_list_size = 0; + for (final String source : sourceIndex.keySet()) { + source_list_size += (U32_SIZE + U32_SIZE + source.length()); + } + + long offset = U32_SIZE + // GEMF Version + U32_SIZE + // Tile size + U32_SIZE + // Number of sources + source_list_size + ranges.size() * ((U32_SIZE * 6) + U64_SIZE) + U32_SIZE; // Number of ranges + + // Calculate offset for each range in the data set + for (final GEMFRange range : ranges) { + range.offset = offset; + + for (int x = range.xMin; x < range.xMax + 1; ++x) { + for (int y = range.yMin; y < range.yMax + 1; ++y) { + offset += (U32_SIZE + U64_SIZE); + } + } + } + + final long headerSize = offset; + + RandomAccessFile gemfFile = new RandomAccessFile(pLocation, "rw"); + + // Write version header + gemfFile.writeInt(VERSION); + + // Write file size header + gemfFile.writeInt(TILE_SIZE); + + // Write number of sources + gemfFile.writeInt(sourceIndex.size()); + + // Write source list + for (final String source : sourceIndex.keySet()) { + gemfFile.writeInt(sourceIndex.get(source)); + gemfFile.writeInt(source.length()); + gemfFile.write(source.getBytes()); + } + + // Write number of ranges + gemfFile.writeInt(ranges.size()); + + // Write range objects + for (final GEMFRange range : ranges) { + gemfFile.writeInt(range.zoom); + gemfFile.writeInt(range.xMin); + gemfFile.writeInt(range.xMax); + gemfFile.writeInt(range.yMin); + gemfFile.writeInt(range.yMax); + gemfFile.writeInt(range.sourceIndex); + gemfFile.writeLong(range.offset); + } + + // Write file offset list + for (final GEMFRange range : ranges) { + for (int x = range.xMin; x < range.xMax + 1; ++x) { + for (int y = range.yMin; y < range.yMax + 1; ++y) { + gemfFile.writeLong(offset); + + long fileSize = 0; + try { + fileSize = dirIndex.get(indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y).length(); + } catch (NullPointerException e) { + //dont' do anything here. Error will be logged later. + } + gemfFile.writeInt((int) fileSize); + offset += fileSize; + } + } + } + + // + // Write tiles + // + + final byte[] buf = new byte[FILE_COPY_BUFFER_SIZE]; + + long currentOffset = headerSize; + int fileIndex = 0; + + for (final GEMFRange range : ranges) { + for (int x = range.xMin; x < range.xMax + 1; ++x) { + for (int y = range.yMin; y < range.yMax + 1; ++y) { + + long fileSize = 0; + try { + fileSize = dirIndex.get(indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y).length(); + } catch (NullPointerException e) { + //don't do anything here. Error will be logged later. + } + + if (currentOffset + fileSize > FILE_SIZE_LIMIT) { + gemfFile.close(); + ++fileIndex; + gemfFile = new RandomAccessFile(pLocation + "-" + fileIndex, "rw"); + currentOffset = 0; + } else { + currentOffset += fileSize; + } + + try { + final FileInputStream tile = new FileInputStream(dirIndex.get(indexSource.get(range.sourceIndex)).get(range.zoom).get(x).get(y)); + + int read = tile.read(buf, 0, FILE_COPY_BUFFER_SIZE); + while (read != -1) { + gemfFile.write(buf, 0, read); + read = tile.read(buf, 0, FILE_COPY_BUFFER_SIZE); + } + + tile.close(); + } catch (Exception e) { + log.warn("Please check that all required Tiles have been downloaded correctly. I am missing tile for x=" + x + ", y=" + y + ", z=" + range.zoom); + } + } + } + } + + gemfFile.close(); + + // Complete construction of GEMFFile object + // openFiles(); + // readHeader(); + } + + // Class to represent a range of stored tiles within the archive. + private class GEMFRange { + Integer zoom; + Integer xMin; + Integer xMax; + Integer yMin; + Integer yMax; + Integer sourceIndex; + Long offset; + + @Override + public String toString() { + return String.format("GEMF Range: source=%d, zoom=%d, x=%d-%d, y=%d-%d, offset=0x%08X", sourceIndex, zoom, + xMin, xMax, yMin, yMax, offset); + } + }; +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/BoundingRect.java b/src/main/java/mobac/program/atlascreators/impl/rmp/BoundingRect.java new file mode 100644 index 0000000..97eda5d --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/BoundingRect.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp; + +/** + * Rectangle made from latitude/longitude coordinates. Negative latitude is to + * the north and negative longitude is to the west. + * + * @author Andreas Sander + * + */ +public class BoundingRect { + private double north; + private double south; + private double west; + private double east; + + /** + * Constructor + */ + public BoundingRect(double north, double south, double west, double east) { + this.north = north; + this.south = south; + this.west = west; + this.east = east; + } + + public double getNorth() { + return north; + } + + public void setNorth(double north) { + this.north = north; + } + + public double getSouth() { + return south; + } + + public void setSouth(double south) { + this.south = south; + } + + public double getWest() { + return west; + } + + public void setWest(double west) { + this.west = west; + } + + public double getEast() { + return east; + } + + public void setEast(double east) { + this.east = east; + } + + @Override + public String toString() { + return String.format("BoundingRect [N=%2.4f, S==%2.4f, W=%2.4f, E=%2.4f]", north, south, + west, east); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/ChecksumOutputStream.java b/src/main/java/mobac/program/atlascreators/impl/rmp/ChecksumOutputStream.java new file mode 100644 index 0000000..0a1704e --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/ChecksumOutputStream.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ------------------------------------------------------------------------ + + CheckSumStream.java + + Project: Testing + + --------------------------------------------------------------------------*/ + +/* --- + created: 15.08.2008 a.sander + + $History:$ + --- */ + +package mobac.program.atlascreators.impl.rmp; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * OutputStream that calculates a 2Byte XOR checksum over the stream written + * + */ +public class ChecksumOutputStream extends OutputStream { + private OutputStream nextStream; + int checksum; + boolean evenByte; + + /** + * Constructor + * + * @param next_stream + * stream to write data to + */ + public ChecksumOutputStream(OutputStream next_stream) { + nextStream = next_stream; + checksum = 0; + evenByte = true; + } + + /** + * Resets the checksum to 0 + */ + public void clearChecksum() { + checksum = 0; + evenByte = true; + } + + /** + * Returns the current checksum + */ + public int getChecksum() { + return checksum; + } + + /** + * Sets the current checksum + */ + public void setChecksum(int checksum) { + this.checksum = checksum; + } + + /** + * Writes the current checksum (2 bytes) to the output stream + */ + public void writeChecksum() throws IOException { + nextStream.write((checksum >> 8) & 0xFF); + nextStream.write(checksum & 0xFF); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + int value; + int o = off; + + for (int i = 0; i < len; i++) { + if (evenByte) + value = (((int) buf[o++]) & 0xFF) << 8; + else + value = (((int) buf[o++]) & 0xFF); + + checksum ^= value; + evenByte = !evenByte; + } + + /* --- Send to next stream --- */ + nextStream.write(buf, off, len); + } + + @Override + public void write(byte[] buf) throws IOException { + write(buf, 0, buf.length); + } + + @Override + public void write(int val) throws IOException { + int value; + + if (evenByte) + value = (val & 0xFF) << 8; + else + value = (val & 0xFF); + + checksum ^= value; + evenByte = !evenByte; + + /* --- Send to next stream --- */ + nextStream.write(val); + } + + @Override + public void close() throws IOException { + nextStream.close(); + } + + @Override + public void flush() throws IOException { + nextStream.flush(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/MobacTile.java b/src/main/java/mobac/program/atlascreators/impl/rmp/MobacTile.java new file mode 100644 index 0000000..3113ab4 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/MobacTile.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl.rmp; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.io.IOException; + +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapSpace; + +import org.apache.log4j.Logger; + + +public class MobacTile { + private static final Logger log = Logger.getLogger(MobacTile.class); + + private final TileProvider tileProvider; + private final int tilex; + private final int tiley; + private BufferedImage image; + + private BoundingRect boundingRect; + + public MobacTile(TileProvider tileProvider, MapSpace mapSpace, int tilex, int tiley, int zoom) { + this.tileProvider = tileProvider; + this.tilex = tilex; + this.tiley = tiley; + image = null; + + int tileSize = mapSpace.getTileSize(); + int x = tilex * tileSize; + int y = tiley * tileSize; + double north = mapSpace.cYToLat(y, zoom); + double south = mapSpace.cYToLat(y + tileSize - 1, zoom); + double west = mapSpace.cXToLon(x, zoom); + double east = mapSpace.cXToLon(x + tileSize - 1, zoom); + + // north and south have to be negated - this really strange! + boundingRect = new BoundingRect(-north, -south, west, east); + } + + /** + * Returns the image of the tile. Creates on if necessary + */ + public BufferedImage getImage() { + + /* --- Load image if none is present --- */ + if (image == null) + image = loadImage(); + + return image; + } + + private BufferedImage loadImage() { + try { + image = tileProvider.getTileImage(tilex, tiley); + } catch (IOException e) { + log.error("", e); + image = createBlack(256, 256); + } + return image; + } + + /** + * create a black Tile + * + * @return image of black square + */ + private BufferedImage createBlack(int width, int height) { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics graph = img.getGraphics(); + graph.setColor(Color.BLACK); + graph.fillRect(0, 0, width, height); + return img; + } + + public void drawSubImage(BoundingRect dest_area, BufferedImage dest_image) { + BufferedImage src_image; + WritableRaster src_graph, dst_graph; + BoundingRect src_area; + int maxx, maxy; + double src_c_x, src_c_y; + int pix_x, pix_y; + BufferedImage imageBuffer; + Graphics graphics; + int[] pixel = new int[3]; + + /* --- Get the coordination rectangle of the source image --- */ + src_area = boundingRect; + + /* --- Get Graphics context --- */ + src_image = getImage(); + + if (src_image == null) + return; + + /* --- Convert it to RGB color space --- */ + imageBuffer = new BufferedImage(src_image.getWidth(), src_image.getHeight(), + BufferedImage.TYPE_INT_RGB); + graphics = imageBuffer.createGraphics(); + try { + graphics.drawImage(src_image, 0, 0, null); + } finally { + graphics.dispose(); + } + src_graph = imageBuffer.getRaster(); + dst_graph = dest_image.getRaster(); + + /* + * --- Iterate over all pixels of the destination image. Unfortunately + * we need this technique because source and dest do not have exactly + * the same zoom level, so the source image has to be compressed or + * expanded to match the destination image --- + */ + maxx = dest_image.getWidth(); + maxy = dest_image.getHeight(); + for (int y = 0; y < maxy; y++) { + + /* --- Calculate the y-coordinate of the current line --- */ + src_c_y = dest_area.getNorth() + (dest_area.getSouth() - dest_area.getNorth()) * y + / maxy; + + /* --- Calculate the pixel line of the source image --- */ + pix_y = (int) ((src_c_y - src_area.getNorth()) * 256 + / (src_area.getSouth() - src_area.getNorth()) + 0.5); + + /* --- Ignore line that are out of the source area --- */ + if (pix_y < 0 || pix_y > 255) + continue; + + // log.trace("scale factor y: " + (pix_y / (double) y)); + + for (int x = 0; x < maxx; x++) { + /* --- Calculate the x-coordinate of the current row --- */ + src_c_x = dest_area.getWest() + (dest_area.getEast() - dest_area.getWest()) * x + / maxx; + + /* --- Calculate the pixel row of the source image --- */ + pix_x = (int) ((src_c_x - src_area.getWest()) * 256 + / (src_area.getEast() - src_area.getWest()) + 0.5); + + /* --- Ignore the row if it is outside the source area --- */ + if (pix_x < 0 || pix_x > 255) + continue; + + /* --- Transfer the pixel --- */ + src_graph.getPixel(pix_x, pix_y, pixel); + dst_graph.setPixel(x, y, pixel); + } + } + } + + public int getImageHeight() { + /* --- A tile is always 256 pixels high --- */ + return 256; + } + + public int getImageWidth() { + /* --- A tile is always 256 pixels wide --- */ + return 256; + } + + public BufferedImage getSubImage(BoundingRect area, int width, int height) { + BufferedImage result = createBlack(width, height); + drawSubImage(area, result); + return result; + } + + @Override + public String toString() { + return String.format("MobacTile x/y [%d/%d] = %s", tilex, tiley, boundingRect); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/MultiImage.java b/src/main/java/mobac/program/atlascreators/impl/rmp/MultiImage.java new file mode 100644 index 0000000..ec5ec33 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/MultiImage.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl.rmp; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +import mobac.exceptions.MapCreationException; +import mobac.program.atlascreators.tileprovider.TileProvider; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.utilities.collections.SoftHashMap; + +import org.apache.log4j.Logger; + +/** + * CalibratedImage that gets its data from a set of other CalibratedImage2 + * + */ +public class MultiImage { + + private static final Logger log = Logger.getLogger(MultiImage.class); + + private final MapInterface map; + private final MapSource mapSource; + private final int zoom; + private final TileProvider tileProvider; + private SoftHashMap cache; + + public MultiImage(MapSource mapSource, TileProvider tileProvider, MapInterface map) { + this.mapSource = mapSource; + this.tileProvider = tileProvider; + this.zoom = map.getZoom(); + this.map = map; + cache = new SoftHashMap(400); + } + + public BufferedImage getSubImage(BoundingRect area, int width, int height) throws MapCreationException { + if (log.isTraceEnabled()) + log.trace(String.format("getSubImage %d %d %s", width, height, area)); + + MapSpace mapSpace = mapSource.getMapSpace(); + int tilesize = mapSpace.getTileSize(); + + int xMax = mapSource.getMapSpace().cLonToX(area.getEast(), zoom) / tilesize; + int xMin = mapSource.getMapSpace().cLonToX(area.getWest(), zoom) / tilesize; + int yMax = mapSource.getMapSpace().cLatToY(-area.getSouth(), zoom) / tilesize; + int yMin = mapSource.getMapSpace().cLatToY(-area.getNorth(), zoom) / tilesize; + + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + Graphics2D graph = result.createGraphics(); + try { + graph.setColor(Color.WHITE); + graph.fillRect(0, 0, width, height); + + for (int x = xMin; x <= xMax; x++) { + for (int y = yMin; y <= yMax; y++) { + TileKey key = new TileKey(x, y); + MobacTile image = cache.get(key); + if (image == null) { + image = new MobacTile(tileProvider, mapSpace, x, y, zoom); + cache.put(key, image); + } + image.drawSubImage(area, result); + } + } + } catch (Throwable t) { + throw new MapCreationException(map, t); + } finally { + graph.dispose(); + } + return result; + } + + protected static class TileKey { + int x; + int y; + + public TileKey(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + x; + result = prime * result + y; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TileKey other = (TileKey) obj; + if (x != other.x) + return false; + if (y != other.y) + return false; + return true; + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/RmpLayer.java b/src/main/java/mobac/program/atlascreators/impl/rmp/RmpLayer.java new file mode 100644 index 0000000..b8019c9 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/RmpLayer.java @@ -0,0 +1,302 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; + +import mobac.gui.AtlasProgress; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.atlascreators.impl.rmp.interfaces.RmpFileEntry; + +import org.apache.log4j.Logger; + + +/** + * Class for building a TLM file from image and writing the file to a stream + * + */ +public class RmpLayer { + private static final Logger log = Logger.getLogger(RmpLayer.class); + + private List tiles; + private TLMEntry tlmFile = null; + + private final AtlasCreator atlasCreator; + + /** + * Constructor + */ + public RmpLayer(AtlasCreator atlasCreator) { + tiles = new LinkedList(); + this.atlasCreator = atlasCreator; + } + + /** + * Return the content of the A00 file as byte array + */ + public RmpFileEntry getA00File(String image_name) { + return new A00Entry(image_name); + } + + /** + * Return the content of the TLM file as byte array + * + * @throws IOException + */ + public TLMEntry getTLMFile(String image_name) { + tlmFile.setImageName(image_name); + return tlmFile; + } + + public void addPreparedImage(Tiledata tileData) throws IOException { + tiles.add(tileData); + } + + /** + * distribute the tiles over containers of max 256 tiles + */ + private TileContainer buildTileTree() { + TileContainer[] container; + TileContainer indexContainer = null; + int containerCount; + + /* + * --- Calculate the number of tiles and tiles per container. 99 would + * be possible but we limit ourselves to 80 - That's enough --- + */ + int count = tiles.size(); + containerCount = count / 80; + if (count % 80 != 0) + containerCount++; + + int tilesPerContainer = count / containerCount; + + /* --- Create containers --- */ + container = new TileContainer[containerCount]; + for (int i = 0; i < containerCount; i++) + container[i] = new TileContainer(); + + /* + * --- We need an index container if there is more than one container. + * Container 0 is the previous of the index container --- + */ + if (containerCount > 1) + indexContainer = new TileContainer(container[0]); + + /* --- Place the tiles into the container --- */ + int tileCount = 0; + int totalTileCount = 0; + int containerNumber = 0; + for (Tiledata tiledata : tiles) { + /* + * --- Starting with the second container, the first element is + * moved to the index container --- + */ + if (tileCount == 0 && containerNumber != 0) + indexContainer.addTile(tiledata, container[containerNumber]); + else + container[containerNumber].addTile(tiledata, null); + + /* --- Switch to next container if we reach end of container --- */ + tileCount++; + if (tileCount == tilesPerContainer) { + containerNumber++; + tileCount = 0; + + /* + * --- Recalculate the number of tiles per container because of + * rounding issues + */ + if (containerCount != containerNumber) + tilesPerContainer = (count - (totalTileCount + 1)) + / (containerCount - containerNumber); + } + totalTileCount++; + } + + /* + * --- If we have multiple containers, then the index container is the + * result, otherwise the single container. + */ + if (indexContainer == null) + return container[0]; + else + return indexContainer; + } + + /** + * Create the TLM file from the TileContainer infos + * + * @throws IOException + */ + public void buildTLMFile(double tile_width, double tile_height, double left, double right, + double top, double bottom) throws IOException { + tlmFile = new TLMEntry(tile_width, tile_height, left, right, top, bottom); + tlmFile.updateContent(); + } + + public class TLMEntry implements RmpFileEntry { + + private byte[] data = null; + String imageName; + final double tile_width; + final double tile_height; + final double left; + final double right; + final double top; + final double bottom; + + public TLMEntry(double tile_width, double tile_height, double left, double right, + double top, double bottom) { + super(); + this.tile_width = tile_width; + this.tile_height = tile_height; + this.left = left; + this.right = right; + this.top = top; + this.bottom = bottom; + } + + public void updateContent() throws IOException { + // calculate offset of each tile in A00 file + int totaloffset = 4; + for (Tiledata tile : tiles) { + tile.totalOffset = totaloffset; + totaloffset += tile.getTileDataSize() + 4; + } + + /* --- Build the tile container --- */ + TileContainer container = buildTileTree(); + log.debug("Number of output tiles: " + container.getTileCount()); + + /* --- Create Output file --- */ + ByteArrayOutputStream bos = new ByteArrayOutputStream(16384); + + /* --- header --- */ + RmpTools.writeValue(bos, 1, 4); // Start of block + RmpTools.writeValue(bos, container.getTileCount(), 4); // Number of + // tiles in files + RmpTools.writeValue(bos, 256, 2); // Hor. size of tile in pixel + RmpTools.writeValue(bos, 256, 2); // Vert. size of tile in pixel + RmpTools.writeValue(bos, 1, 4); // Start of block + RmpTools.writeDouble(bos, tile_height); // Height of tile in degree + RmpTools.writeDouble(bos, tile_width); // Tile width in degree + RmpTools.writeDouble(bos, left); // Frame + RmpTools.writeDouble(bos, top); // Frame + RmpTools.writeDouble(bos, right); // Frame + RmpTools.writeDouble(bos, bottom); // Frame + + RmpTools.writeValue(bos, 0, 88); // Filler + + RmpTools.writeValue(bos, 256, 2); // Tile size ???? + RmpTools.writeValue(bos, 0, 2); // Filler + + int size = 256 + 1940 + 3 * 1992; + size += container.getContainerCount() * 1992; + if (container.getContainerCount() != 1) + size += 1992; + RmpTools.writeValue(bos, size, 4); // File size + + RmpTools.writeValue(bos, 0, 96); // Filler + + RmpTools.writeValue(bos, 1, 4); // Start of block + RmpTools.writeValue(bos, 99, 4); // Number of tiles in block + int firstBlockOffset = 0x0f5c + ((container.getContainerCount() == 1) ? 0 : 1992); + RmpTools.writeValue(bos, firstBlockOffset, 4); // offset for first + // block + + RmpTools.writeValue(bos, 0, 3920); // Filler + + /* --- Write the Tiledata --- */ + container.writeTree(bos); + + /* --- Add two empty blocks --- */ + RmpTools.writeValue(bos, 0, 1992 * 2); + data = bos.toByteArray(); + } + + public void setImageName(String imageName) { + this.imageName = imageName; + } + + public String getFileExtension() { + return "tlm"; + } + + public String getFileName() { + return imageName; + } + + public void writeFileContent(OutputStream out) throws IOException { + out.write(data); + } + + } + + protected class A00Entry implements RmpFileEntry { + + protected String name; + + public A00Entry(String name) { + super(); + this.name = name; + } + + public String getFileExtension() { + return "a00"; + } + + public String getFileName() { + return name; + } + + public void writeFileContent(OutputStream os) throws IOException, InterruptedException { + BufferedOutputStream bos = new BufferedOutputStream(os, 32768); + /* --- Number of tiles --- */ + RmpTools.writeValue(bos, tiles.size(), 4); + + AtlasProgress atlasProgress = atlasCreator.getAtlasProgress(); + /* --- The tiles --- */ + int x = 0; + int xMax = tiles.size(); + for (Tiledata tile : tiles) { + tile.writeTileData(bos); + atlasCreator.checkUserAbort(); + + atlasProgress.setMapCreationProgress((1000 * x++ / xMax)); + } + bos.flush(); + } + + @Override + public String toString() { + return "A00Entry"; + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/RmpTools.java b/src/main/java/mobac/program/atlascreators/impl/rmp/RmpTools.java new file mode 100644 index 0000000..bcf2f9d --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/RmpTools.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class RmpTools { + + /** + * Copies the given value into the stream as binary value, with least + * significant byte first. + * + * @param stream + * stream to write to + * @param value + * value to write + * @param length + * number of bytes to write to stream + * @throws IOException + */ + public static void writeValue(OutputStream stream, int value, int length) throws IOException { + int i; + + for (i = 0; i < length; i++) { + stream.write(value & 0xFF); + value >>= 8; + } + } + + public static void writeValue(OutputStream stream, long value, int length) throws IOException { + int i; + + for (i = 0; i < length; i++) { + stream.write((int)(value & 0xFF)); + value >>= 8; + } + } + + /** + * Writes the given string into the stream. The string is written with a + * fixed length. If the length is longer than the string, then 00 bytes are + * written + * + * @param stream + * stream to write to + * @param str + * strign to write + * @param length + * number of bytes to write + * @throws IOException + */ + public static void writeFixedString(OutputStream stream, String str, int length) + throws IOException { + int i; + int value; + + for (i = 0; i < length; i++) { + if (i < str.length()) + value = str.charAt(i); + else + value = 0; + + stream.write(value); + } + } + + /** + * Write a double value into a byte array + * + * @param os + * stream to write to + * @param value + * value to write + */ + public static void writeDouble(OutputStream os, double value) throws IOException { + ByteArrayOutputStream bo; + DataOutputStream dos; + byte[] b; + byte help; + + /* --- Convert the value into a byte array --- */ + bo = new ByteArrayOutputStream(); + dos = new DataOutputStream(bo); + + /* --- Convert the value into a 8 byte double --- */ + dos.writeDouble(value); + dos.close(); + b = bo.toByteArray(); + + /* --- Change byte order --- */ + for (int i = 0; i < 4; i++) { + help = b[i]; + b[i] = b[7 - i]; + b[7 - i] = help; + } + + /* --- Write result into output stream --- */ + os.write(b); + } + + /** + * Build an image name from a filename. The image name is the name of the + * file without path and extension . The length of the name is limited to 8 + * chars. We use only 6 chars, so we can use 99 images + */ + public static String buildImageName(String name) { + int index; + + /* --- Remove the extension --- */ + index = name.indexOf('.'); + if (index != -1) + name = name.substring(0, index); + + /* --- Limit the filename to 8 chars --- */ + if (name.length() > 8) + name = name.substring(0, 8); + + return name.toLowerCase().trim(); + } + + /** + * Builds a tile name from a basename and an index. + */ + public static String buildTileName(String basename, int index) { + String indexstr; + + /* --- Convert the index number to a string --- */ + indexstr = String.valueOf(index); + + /* + * --- cut the basename so that basename+index is not longer than 8 + * chars --- + */ + if (indexstr.length() + basename.length() > 8) + basename = basename.substring(0, 8 - indexstr.length()); + + return basename.trim() + indexstr; + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/RmpWriter.java b/src/main/java/mobac/program/atlascreators/impl/rmp/RmpWriter.java new file mode 100644 index 0000000..d0a4240 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/RmpWriter.java @@ -0,0 +1,253 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; + +import mobac.program.atlascreators.impl.rmp.interfaces.RmpFileEntry; +import mobac.program.atlascreators.impl.rmp.rmpfile.RmpIni; +import mobac.utilities.Utilities; +import mobac.utilities.stream.CountingOutputStream; +import mobac.utilities.stream.RandomAccessFileOutputStream; + +import org.apache.commons.io.output.NullOutputStream; +import org.apache.log4j.Logger; + +/** + * Class that writes files in RMP archive format + * + */ +public class RmpWriter { + + /** + * Max file size: 2147483647 bytes = 2047,99 MiB + */ + public static final long MAX_FILE_SIZE = 0xffffffffl; + + private static final Logger log = Logger.getLogger(RmpWriter.class); + + private final ArrayList entries = new ArrayList(); + private final File rmpFile; + private final RandomAccessFile rmpOutputFile; + private int projectedEntryCount; + + private ChecksumOutputStream entryOut; + + /** + * @param imageName + * @param layerCount + * projected number of layers that will be written to this rmp file + * @param rmpFile + * @throws IOException + * @throws InterruptedException + */ + public RmpWriter(String imageName, int layerCount, File rmpFile) throws IOException, InterruptedException { + this.rmpFile = rmpFile; + // We only use one A00 entry per map/layer - therefore we can + // pre-calculate the number of entries: + // RmpIni + (TLM & A00) per layer + Bmp2Bit + Bmp4bit + this.projectedEntryCount = (3 + (2 * layerCount)); + if (rmpFile.exists()) + Utilities.deleteFile(rmpFile); + log.debug("Writing data to " + rmpFile.getAbsolutePath()); + rmpOutputFile = new RandomAccessFile(rmpFile, "rw"); + // Calculate offset to the directory end + int directoryEndOffset = projectedEntryCount * 24 + 10; + rmpOutputFile.seek(directoryEndOffset); + entryOut = new ChecksumOutputStream(new RandomAccessFileOutputStream(rmpOutputFile)); + /* --- Write the directory-end marker --- */ + RmpTools.writeFixedString(entryOut, "MAGELLAN", 30); + + RmpIni rmpIni = new RmpIni(imageName, layerCount); + + /* --- Create packer and fill it with content --- */ + writeFileEntry(rmpIni); + } + + public void writeFileEntry(RmpFileEntry entry) throws IOException, InterruptedException { + EntryInfo info = new EntryInfo(); + info.name = entry.getFileName(); + info.extendsion = entry.getFileExtension(); + info.offset = rmpOutputFile.getFilePointer(); + entry.writeFileContent(entryOut); + info.length = rmpOutputFile.getFilePointer() - info.offset; + if ((info.length % 2) != 0) + entryOut.write(0); + entries.add(info); + if (rmpOutputFile.getFilePointer() > MAX_FILE_SIZE) + throwRmpTooLarge(); + log.debug("Written data of entry " + entry + " bytes=" + info.length); + } + + public void prepareFileEntry(RmpFileEntry entry) throws IOException, InterruptedException { + EntryInfo info = new EntryInfo(); + info.name = entry.getFileName(); + info.extendsion = entry.getFileExtension(); + long pos = rmpOutputFile.getFilePointer(); + info.offset = pos; + CountingOutputStream cout = new CountingOutputStream(new NullOutputStream()); + entry.writeFileContent(cout); + info.length = cout.getBytesWritten(); + long newPos = pos + info.length; + if ((info.length % 2) != 0) + newPos++; + if (newPos > MAX_FILE_SIZE) + throwRmpTooLarge(); + rmpOutputFile.seek(newPos); + entries.add(info); + log.debug("Prepared data of entry " + entry + " bytes=" + info.length); + } + + public void writePreparedFileEntry(RmpFileEntry entry) throws IOException, InterruptedException { + long pos = rmpOutputFile.getFilePointer(); + EntryInfo info = new EntryInfo(); + info.name = entry.getFileName(); + info.extendsion = entry.getFileExtension(); + int index = entries.indexOf(info); + if (index < 0) + throw new RuntimeException("Index for entry not found"); + info = entries.get(index); + + rmpOutputFile.seek(info.offset); + entry.writeFileContent(entryOut); + if (rmpOutputFile.getFilePointer() > MAX_FILE_SIZE) + throwRmpTooLarge(); + long newLength = rmpOutputFile.getFilePointer() - info.offset; + if (newLength != info.length) + throw new RuntimeException("Length of entry has changed!"); + if ((newLength % 2) != 0) + entryOut.write(0); + + // restore old file position + rmpOutputFile.seek(pos); + } + + private void throwRmpTooLarge() throws IOException { + throw new IOException("RMP file size exeeds 2GiB! The RMP file format does not support that."); + } + + /** + * Writes the directory of the archive into the rmp file + * + * @throws IOException + * Error accessing disk + */ + public void writeDirectory() throws IOException { + if (projectedEntryCount != entries.size()) + throw new RuntimeException("Entry count does not correspond " + + "to the projected layer count: \nProjected: " + projectedEntryCount + "\nPresent:" + + entries.size()); + + // Finalize the list of written entries + RmpTools.writeFixedString(entryOut, "MAGELLAN", 8); + entryOut.writeChecksum(); + + log.debug("Finished writing entries, updating directory"); + + /* --- Create file --- */ + rmpOutputFile.seek(0); + OutputStream out = new RandomAccessFileOutputStream(rmpOutputFile); + ChecksumOutputStream cout = new ChecksumOutputStream(out); + + /* --- Write header with number of files --- */ + RmpTools.writeValue(cout, entries.size(), 4); + RmpTools.writeValue(cout, entries.size(), 4); + + /* --- Write the directory --- */ + log.debug("Writing directory: " + entries.size() + " entries"); + for (EntryInfo entryInfo : entries) { + + log.trace("Entry: " + entryInfo); + /* --- Write directory entry --- */ + RmpTools.writeFixedString(cout, entryInfo.name, 9); + RmpTools.writeFixedString(cout, entryInfo.extendsion, 7); + RmpTools.writeValue(cout, entryInfo.offset, 4); + RmpTools.writeValue(cout, entryInfo.length, 4); + } + + /* --- Write the header checksum (2 bytes) --- */ + cout.writeChecksum(); + + } + + public void close() { + try { + rmpOutputFile.close(); + } catch (Exception e) { + log.error("", e); + } + } + + public void delete() throws IOException { + close(); + Utilities.deleteFile(rmpFile); + } + + private static class EntryInfo { + String name; + String extendsion; + long offset; + long length; + + @Override + public String toString() { + return "\"" + name + "." + extendsion + "\" offset=" + offset + " length=" + length; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((extendsion == null) ? 0 : extendsion.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + EntryInfo other = (EntryInfo) obj; + if (extendsion == null) { + if (other.extendsion != null) + return false; + } else if (!extendsion.equals(other.extendsion)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/TileContainer.java b/src/main/java/mobac/program/atlascreators/impl/rmp/TileContainer.java new file mode 100644 index 0000000..59f6a4e --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/TileContainer.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +/** + * Container for tiles and link to subtrees + * + */ +public class TileContainer { + + private final TileContainer previous; + private final ArrayList tiles; + private final ArrayList followUps; + + public TileContainer() { + this(null); + } + + public TileContainer(TileContainer previous) { + this.previous = previous; + tiles = new ArrayList(100); + followUps = new ArrayList(); + } + + /** + * Set tile for given position. next is only valid if a previous value is + * set + */ + public void addTile(Tiledata tile, TileContainer next) { + tiles.add(tile); + + if (previous != null) + followUps.add(next); + } + + /** + * Returns the number of tiles in this container including all sub + * containers + */ + public int getTileCount() { + int count = tiles.size(); + + if (previous != null) + count += previous.getTileCount(); + + for (TileContainer next : followUps) + count += next.getTileCount(); + + return count; + } + + /** + * returns the number of containers in the tree + */ + public int getContainerCount() { + return 1 + followUps.size(); + } + + /** + * Write the whole tree into the stream + */ + public void writeTree(OutputStream os) throws IOException { + /* --- if this container has subtrees --- */ + if (previous != null) { + /* --- Write previous --- */ + previous.writeTree(os); + + /* --- Write ourselves --- */ + writeContainer(os); + + /* --- And all subtrees --- */ + for (TileContainer tc : followUps) + tc.writeTree(os); + } else { + /* --- Just write the tile itself, if it does not have subtrees --- */ + writeContainer(os); + } + } + + /** + * Write the content of the container + */ + public void writeContainer(OutputStream os) throws IOException { + + /* --- Number of tiles in subtree --- */ + RmpTools.writeValue(os, getTileCount(), 4); + + /* --- Number of tiles in node --- */ + RmpTools.writeValue(os, tiles.size(), 2); + + /* --- Last node flag --- */ + RmpTools.writeValue(os, previous == null ? 1 : 0, 2); + + /* --- Info about 99 tiles --- */ + for (int i = 0; i < 99; i++) { + int x = 0; + int y = 0; + int offset = 0; + + if (i < tiles.size()) { + x = tiles.get(i).posx; + y = tiles.get(i).posy; + offset = tiles.get(i).totalOffset; + } + + RmpTools.writeValue(os, x, 4); + RmpTools.writeValue(os, y, 4); + RmpTools.writeValue(os, 0, 4); + RmpTools.writeValue(os, offset, 4); + } + + /* --- Offset to previous --- */ + if (previous == null) + RmpTools.writeValue(os, 0, 4); + else + RmpTools.writeValue(os, 0x0f5c, 4); + + /* --- Offset to following --- */ + for (int i = 0; i < 99; i++) { + if (i < followUps.size()) + RmpTools.writeValue(os, 0x0f5c + (i + 2) * 1992, 4); + else + RmpTools.writeValue(os, 0, 4); + } + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/Tiledata.java b/src/main/java/mobac/program/atlascreators/impl/rmp/Tiledata.java new file mode 100644 index 0000000..de5e862 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/Tiledata.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import mobac.exceptions.MapCreationException; +import mobac.program.interfaces.TileImageDataWriter; + +/** + * Content of a single tile + * + */ +public class Tiledata { + + private final TileImageDataWriter writer; + + public int posx; + public int posy; + public int totalOffset; + public MultiImage si; + + public BoundingRect rect; + + private int dataSize = 0; + + public Tiledata(TileImageDataWriter writer) { + this.writer = writer; + } + + public int getTileDataSize() { + return dataSize; + } + + public void writeTileData(OutputStream out) throws IOException { + try { + BufferedImage image = si.getSubImage(rect, 256, 256); + ByteArrayOutputStream bout = new ByteArrayOutputStream(16384); + writer.processImage(image, bout); + byte[] data = bout.toByteArray(); + dataSize = data.length; + // Utilities.saveBytes(String.format("D:/jpg/mobac-%04d-%04d.jpg", posx, posy), data); + RmpTools.writeValue(out, dataSize, 4); + out.write(data); + } catch (MapCreationException e) { + throw new IOException(e.getCause()); + } + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/interfaces/RmpFileEntry.java b/src/main/java/mobac/program/atlascreators/impl/rmp/interfaces/RmpFileEntry.java new file mode 100644 index 0000000..9c41ea0 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/interfaces/RmpFileEntry.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp.interfaces; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Interface for all files that are stored in a RMP file + * + */ +public interface RmpFileEntry { + /** + * Returns the content of the file as byte array + * + * @throws InterruptedException + */ + public void writeFileContent(OutputStream os) throws IOException, InterruptedException; + + /** + * Returns the name of the file without extension + */ + public String getFileName(); + + /** + * Returns the extension of the file + */ + public String getFileExtension(); + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp2bit.java b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp2bit.java new file mode 100644 index 0000000..8fc6c31 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp2bit.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl.rmp.rmpfile; + +public class Bmp2bit extends GeneralRmpFileEntry { + private static final byte[] content = { 73, 99, 111, 110, 32, 102, 105, 108, 101, 32, 118, 101, + 114, 115, 105, 111, 110, 32, 49, 46, 48, 46, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 0, 1, 0, 0, 60, 0, 0, 0, -16, -1, -1, 127, -40, 0, 0, 0, -15, -1, -1, 127, 116, 1, 0, + 0, 0, 1, 0, 0, -1, -1, -1, -1, 88, 0, 0, 0, -104, 0, 0, 0, 0, 0, 0, 0, 16, 0, 16, 0, 4, + 2, 0, 0, 0, 0, 0, 0, 63, -1, -1, -4, 48, 0, 0, 12, 48, 0, 0, 12, 48, 0, 0, 12, 48, 0, + 0, 12, 48, 0, 0, 12, 48, 0, 0, 12, 48, 0, 0, 12, 48, 0, 0, 12, 48, 0, 0, 12, 48, 0, 0, + 12, 48, 0, 0, 12, 48, 0, 0, 12, 63, -1, -1, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 3, -1, -1, -64, 3, -1, -1, -64, 3, -1, -1, -64, 3, -1, -1, -64, 3, -1, -1, + -64, 3, -1, -1, -64, 3, -1, -1, -64, 3, -1, -1, -64, 3, -1, -1, -64, 3, -1, -1, -64, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -1, -1, 127, -1, -1, -1, -1, -12, 0, 0, 0, 52, 1, + 0, 0, 0, 0, 0, 0, 1, 0, 64, 0, 4, 8, 0, 0, 81, -3, -40, -70, -124, 24, -99, -19, 46, + -36, -3, -21, -128, 98, 96, -75, 82, -99, 78, 108, -79, 18, -9, -36, -20, -88, -41, 84, + -99, 88, 98, 40, 74, 93, 118, -31, -73, -62, -98, -43, -41, 12, -75, 60, 69, 54, -54, + -40, 51, -73, -18, 36, 83, 86, -8, 80, -2, 105, -61, -122, 37, 114, 7, -7, 0, -1, -1, + -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, + -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, + -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -15, -1, -1, 127, -1, + -1, -1, -1, -112, 1, 0, 0, -88, 1, 0, 0, 0, 0, 0, 0, 1, 0, 24, 0, 4, 8, 0, 0, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 48, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, + -1, -1, 0, -1, -1 }; + + public Bmp2bit() { + super(content, "bmp2bit", "ics"); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + " " + getFileName() + "." + getFileExtension(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp4bit.java b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp4bit.java new file mode 100644 index 0000000..476c2b7 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/Bmp4bit.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl.rmp.rmpfile; + +public class Bmp4bit extends GeneralRmpFileEntry { + private static final byte[] content = { 73, 99, 111, 110, 32, 102, 105, 108, 101, 32, 118, 101, + 114, 115, 105, 111, 110, 32, 49, 46, 48, 46, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 0, 1, 0, 0, 60, 0, 0, 0, -16, -1, -1, 127, 88, 1, 0, 0, -15, -1, -1, 127, -12, 1, 0, 0, + 0, 1, 0, 0, -1, -1, -1, -1, 88, 0, 0, 0, -40, 0, 0, 0, 0, 0, 0, 0, 16, 0, 16, 0, 4, 4, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 51, 51, 51, 51, 51, 51, 48, 3, 0, 0, 0, 0, 0, 0, 48, + 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, + 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, + 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, 0, 0, 0, 48, 3, 0, 0, 0, + 0, 0, 0, 48, 3, 51, 51, 51, 51, 51, 51, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, -1, -1, -1, -1, -16, 0, 0, + 15, -1, -1, -1, -1, -16, 0, 0, 15, -1, -1, -1, -1, -16, 0, 0, 15, -1, -1, -1, -1, -16, + 0, 0, 15, -1, -1, -1, -1, -16, 0, 0, 15, -1, -1, -1, -1, -16, 0, 0, 15, -1, -1, -1, -1, + -16, 0, 0, 15, -1, -1, -1, -1, -16, 0, 0, 15, -1, -1, -1, -1, -16, 0, 0, 15, -1, -1, + -1, -1, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + -16, -1, -1, 127, -1, -1, -1, -1, 116, 1, 0, 0, -76, 1, 0, 0, 0, 0, 0, 0, 1, 0, 64, 0, + 4, 8, 0, 0, 81, -3, -40, -70, -124, 24, -99, -19, 46, -36, -3, -21, -128, 98, 96, -75, + 82, -99, 78, 108, -79, 18, -9, -36, -20, -88, -41, 84, -99, 88, 98, 40, 74, 93, 118, + -31, -73, -62, -98, -43, -41, 12, -75, 60, 69, 54, -54, -40, 51, -73, -18, 36, 83, 86, + -8, 80, -2, 105, -61, -122, 37, 114, 7, -7, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, + -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, + -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, + -1, 0, -1, -1, -1, -1, -1, -1, 0, -15, -1, -1, 127, -1, -1, -1, -1, 16, 2, 0, 0, 40, 2, + 0, 0, 0, 0, 0, 0, 1, 0, 24, 0, 4, 8, 0, 0, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 97, + 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 0, -1, -1, -1, -1, -1, + -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 0, -1, -1 }; + + public Bmp4bit() { + super(content, "bmp4bit", "ics"); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + " " + getFileName() + "." + getFileExtension(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/GeneralRmpFileEntry.java b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/GeneralRmpFileEntry.java new file mode 100644 index 0000000..21bd3b6 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/GeneralRmpFileEntry.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp.rmpfile; + +import java.io.IOException; +import java.io.OutputStream; + +import mobac.program.atlascreators.impl.rmp.interfaces.RmpFileEntry; + + +/** + * General class for storing the content of a rmp file + * + */ +public class GeneralRmpFileEntry implements RmpFileEntry { + protected final byte[] content; + protected final String filename; + protected final String extension; + + public GeneralRmpFileEntry(byte[] content, String filename, String extension) { + this.content = content; + this.filename = filename; + this.extension = extension; + } + + public void writeFileContent(OutputStream os) throws IOException { + os.write(content); + } + + public String getFileExtension() { + return extension; + } + + public String getFileName() { + return filename; + } + + @Override + public String toString() { + return "GeneralRmpFileEntry \"" + filename + "." + extension + "\" content-len=" + + content.length; + } +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/RmpIni.java b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/RmpIni.java new file mode 100644 index 0000000..c2f1a70 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rmp/rmpfile/RmpIni.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/* ********************************************* + * Copyright: Andreas Sander + * + * + * ********************************************* */ + +package mobac.program.atlascreators.impl.rmp.rmpfile; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import mobac.program.atlascreators.impl.rmp.RmpTools; + +import org.apache.log4j.Logger; + + +/** + * Instance of the rmp.ini file + * + * @author Andreas + * + */ +public class RmpIni extends GeneralRmpFileEntry { + + private static final Logger log = Logger.getLogger(RmpIni.class); + + public RmpIni(String layername, int count) { + super(generateContent(layername, count), "rmp", "ini"); + } + + private static byte[] generateContent(String layername, int count) { + ByteArrayOutputStream bos; + PrintStream ps; + int i; + + bos = new ByteArrayOutputStream(); + ps = new PrintStream(bos); + + /* --- Content of rmp.ini is a simple INI file --- */ + ps.print("[T_Layers]\r\n"); + + for (i = 0; i < count; i++) { + String layerName = RmpTools.buildTileName(layername, i); + log.trace("layer name: " + layerName); + ps.print(i + "=" + layerName + "\r\n"); + } + ps.flush(); + return bos.toByteArray(); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + " size=" + content.length; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/impl/rungps/RunGPSAtlasFile.java b/src/main/java/mobac/program/atlascreators/impl/rungps/RunGPSAtlasFile.java new file mode 100644 index 0000000..934ceb5 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/impl/rungps/RunGPSAtlasFile.java @@ -0,0 +1,434 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.impl.rungps; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.List; + +import mobac.utilities.Charsets; +import mobac.utilities.Utilities; + +/** + * + * @author Tom Henne, eSymetric + */ +public class RunGPSAtlasFile { + + public final static String SUFFIX = ".ratlas"; + RandomAccessFile raf; + RunGPSAtlasHeader rah = new RunGPSAtlasHeader(); + File cacheFile; + OutputStream cacheOutStream = null; + long positionInCacheFile = 0L; + int offset = 0; + int indexLength = 0; + + public class RAINode { + + RAINode() { + key = -1; + value = -1L; + next = -1; + child = -1; + index = -1; + } + + int key; + long value; + int next; + int child; + int index; + + void readFrom(int index) throws IOException { + this.index = index; + raf.seek(index); + key = raf.readInt(); + value = raf.readLong(); + next = raf.readInt(); + child = raf.readInt(); + } + + void writeNew() throws IOException { + raf.seek(indexLength); // go to end + raf.writeInt(key); + raf.writeLong(value); + raf.writeInt(next); + raf.writeInt(child); + this.index = indexLength; + indexLength += 20; + } + + void write() throws IOException { + raf.seek(index + 4); + raf.writeLong(value); + raf.writeInt(next); + raf.writeInt(child); + } + + String listAll() throws IOException { + return listAll(""); + } + + String listAll(String path) throws IOException { + List children = getChildren(); + StringBuilder sb = new StringBuilder(); + if (key != -1) { // not for root + path += "" + key + "/"; + if (value != -1L) { + sb.append(path).append('=').append(value).append("\n"); + + } + } + for (RAINode childNode : children) { + sb.append(childNode.listAll(path)); + + } + return sb.toString(); + } + + List getChildren() throws IOException { + // currentIndexPos is now at fathers node index pos or -1 + + List children = new ArrayList(); + + int indexPos = child; + + for (;;) { + if (indexPos == -1) { + break; + } + RAINode n = new RAINode(); + n.readFrom(indexPos); + children.add(n); + indexPos = n.next; + } + + return children; + } + + RAINode getChildNode(int key, boolean createNew) throws IOException { + // currentIndexPos is now at fathers node index pos or -1 + + // check if father has a child + + if (this.child == -1) { + RAINode newNode = new RAINode(); + newNode.key = key; + newNode.writeNew(); + + this.child = newNode.index; + this.write(); + return newNode; + } + + // find node on this level + + RAINode n = new RAINode(); + int currentIndexPos = this.child; + for (;;) { + if (currentIndexPos == -1) { + break; + } + n.readFrom(currentIndexPos); + if (n.key == key) { + return n; + } + currentIndexPos = n.next; + } + + if (!createNew) { + return null; + } + + RAINode newNode = new RAINode(); + newNode.key = key; + newNode.writeNew(); + + n.next = newNode.index; + n.write(); + return newNode; + } + + public int getSize() { + return indexLength; + } + + public RandomAccessFile getRandomAccessFile() { + return raf; + } + + RAINode getChildNode(List hierarchy) throws IOException { + if (hierarchy.isEmpty()) { + return this; + + } + int key_ = hierarchy.get(0); + hierarchy.remove(0); + + RAINode childNode = getChildNode(key_, false); + if (childNode == null) { + return null; + + } else { + return childNode.getChildNode(hierarchy); + + } + } + } + + public boolean readSuccess() { + return rah.readSuccess(); + } + + public class RunGPSAtlasHeader { + + public final static long FILETYPE_KEY = 8723487877262773L; + public final static int HEADER_SIZE = 128; // bytes + public final static int FILE_VERSION = 3; + int indexSize; // bytes + + public void writeHeader() throws IOException { + raf.seek(0); + raf.writeLong(FILETYPE_KEY); + raf.writeInt(FILE_VERSION); + raf.writeInt(indexSize); + } + + public boolean readHeader() throws IOException { + if (raf.readLong() != FILETYPE_KEY) { + return false; + + } + if (raf.readInt() != FILE_VERSION) { + return false; + + } + indexSize = raf.readInt(); + + return true; + } + + public boolean readSuccess() { + return indexSize > 0; + } + } + + public RunGPSAtlasFile(String filePath, boolean write) throws IOException { + + this.offset = RunGPSAtlasHeader.HEADER_SIZE; + + if (write) { + cacheFile = new File(filePath + ".cac"); + cacheOutStream = new BufferedOutputStream(new FileOutputStream(cacheFile), 8216); + + raf = new RandomAccessFile(filePath, "rw"); + raf.setLength(0); // delete existing content + + indexLength = offset; + RAINode root = new RAINode(); + root.writeNew(); + } else { + raf = new RandomAccessFile(filePath, "r"); + rah.readHeader(); + } + } + + public RAINode getRootNode() throws IOException { + RAINode root = new RAINode(); + root.readFrom(offset); + return root; + } + + public String listAll() throws IOException { + return getRootNode().listAll(); + } + + public void close() throws IOException { + if (cacheOutStream != null) { + cacheOutStream.close(); + } + if (raf != null) { + raf.close(); + } + } + + public void finishArchive() throws IOException { + cacheOutStream.close(); + cacheOutStream = null; + + FileInputStream fis = new FileInputStream(cacheFile); + + rah.indexSize = indexLength - RunGPSAtlasHeader.HEADER_SIZE; + rah.writeHeader(); + + byte[] buf = new byte[4096]; + + raf.seek(indexLength); + for (;;) { + int l = fis.read(buf); + if (l <= 0) { + break; + + } + raf.write(buf, 0, l); + } + + fis.close(); + + Utilities.deleteFile(cacheFile); + close(); + } + + public void addData(List hierarchy, byte[] data) throws IOException { + addData(hierarchy, data, data.length); + } + + public void addData(List hierarchy, byte[] data, int length) throws IOException { + cacheOutStream.write(data, 0, length); + + List posHierarchy = new ArrayList(); + posHierarchy.addAll(hierarchy); + posHierarchy.add(1); // position + setValue(posHierarchy, positionInCacheFile); + + List lenHierarchy = new ArrayList(); + lenHierarchy.addAll(hierarchy); + lenHierarchy.add(2); // size + setValue(lenHierarchy, length); + + positionInCacheFile += length; + + } + + public static List makeHierarchyFromPath(String path) { + // e.g. /10/3487/8345.png + + // sure that replace() is correct? may be replaceAll() would be better? + path = path.replace(File.separatorChar, '/'); + + int p = path.indexOf('.'); + if (p > 0) { + path = path.substring(0, p); // remove suffix + } + if (path.startsWith("/")) { + path = path.substring(1); + } + + try { + List hierarchy = new ArrayList(); + for (;;) { + p = path.indexOf('/'); + if (p > 0) { + hierarchy.add(Integer.parseInt(path.substring(0, p))); + path = path.substring(p + 1); + } else { + hierarchy.add(Integer.parseInt(path)); + break; + } + } + + return hierarchy; + } catch (NumberFormatException e) { + return null; + } + + } + + public byte[] getData(String path) throws IOException { + return getData(makeHierarchyFromPath(path)); + } + + public byte[] getData(List keyHierarchy) throws IOException { + ArrayList posHierarchy = new ArrayList(); + posHierarchy.addAll(keyHierarchy); + posHierarchy.add(1); // position + long indexPosition = getValue(posHierarchy); + if (indexPosition == -1) { + return null; + } + ArrayList sizeHierarchy = new ArrayList(); + sizeHierarchy.addAll(keyHierarchy); + sizeHierarchy.add(2); // size + int size = (int) getValue(sizeHierarchy); + if (size == -1) { + return null; + } + + byte[] buf = new byte[size]; + raf.seek(RunGPSAtlasHeader.HEADER_SIZE + rah.indexSize + indexPosition); + raf.readFully(buf); + return buf; + } + + public void setValue(List keyHierarchy, long value) throws IOException { + setValueImpl(getRootNode(), keyHierarchy, value); + } + + public void setValue(String path, long value) throws IOException { + setValue(makeHierarchyFromPath(path), value); + } + + private void setValueImpl(RAINode currentNode, List keyHierarchy, long value) throws IOException { + int key = keyHierarchy.get(0); + keyHierarchy.remove(0); + RAINode n = currentNode.getChildNode(key, true); + if (keyHierarchy.isEmpty()) { + n.value = value; + n.write(); + } else { + setValueImpl(n, keyHierarchy, value); + } + } + + public long getValue(List keyHierarchy) throws IOException { + RAINode n = getRootNode().getChildNode(keyHierarchy); + return n == null ? -1L : n.value; + } + + public long getValue(String path) throws IOException { + return getValue(makeHierarchyFromPath(path)); + } + + public void setString(List keyHierarchy, String value) throws IOException { + byte[] data = value.getBytes(Charsets.UTF_8); + addData(keyHierarchy, data); + } + + public void setString(String path, String value) throws IOException { + setString(makeHierarchyFromPath(path), value); + } + + public String getString(List keyHierarchy) throws IOException { + byte[] data = getData(keyHierarchy); + if (data != null) { + return new String(data, Charsets.UTF_8); + } + return null; + } + + public String getString(String path) throws IOException { + return getString(makeHierarchyFromPath(path)); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/CacheTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/CacheTileProvider.java new file mode 100644 index 0000000..0879264 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/CacheTileProvider.java @@ -0,0 +1,256 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.Hashtable; +import java.util.concurrent.LinkedBlockingQueue; + +import mobac.program.interfaces.MapSource; + +import org.apache.log4j.Logger; + +/** + * A tile cache with speculative loading on a separate thread. Usually this decreases map generation time on multi-core + * systems. + */ +public class CacheTileProvider implements TileProvider { + + private Logger log = Logger.getLogger(CacheTileProvider.class); + + /** + * Counter for identifying the different threads + */ + private static int PRELOADER_THREAD_NUM = 1; + + private Hashtable cache; + + private PreLoadThread preLoader = new PreLoadThread(); + + protected final TileProvider tileProvider; + + public CacheTileProvider(TileProvider tileProvider) { + this.tileProvider = tileProvider; + cache = new Hashtable(500); + preLoader.start(); + } + + public boolean preferTileImageUsage() { + return true; + } + + public BufferedImage getTileImage(int x, int y) throws IOException { + SRCachedTile cachedTile = cache.get(new CacheKey(x, y)); + BufferedImage image = null; + if (cachedTile != null) { + CachedTile tile = cachedTile.get(); + if (tile != null) { + if (tile.loaded) + log.trace(String.format("Cache hit: x=%d y=%d", x, y)); + image = tile.getImage(); + if (!tile.nextLoadJobCreated) { + // log.debug(String.format("Preload job added : x=%d y=%d l=%d", + // x + 1, y, layer)); + preloadTile(new CachedTile(new CacheKey(x + 1, y))); + tile.nextLoadJobCreated = true; + } + } + } + if (image == null) { + log.trace(String.format("Cache miss: x=%d y=%d", x, y)); + // log.debug(String.format("Preload job added : x=%d y=%d l=%d", x + + // 1, y, layer)); + preloadTile(new CachedTile(new CacheKey(x + 1, y))); + image = internalGetTileImage(x, y); + } + return image; + } + + protected BufferedImage internalGetTileImage(int x, int y) throws IOException { + synchronized (tileProvider) { + return tileProvider.getTileImage(x, y); + } + } + + public byte[] getTileData(int layer, int x, int y) throws IOException { + throw new RuntimeException("Not implemented"); + } + + public byte[] getTileData(int x, int y) throws IOException { + throw new RuntimeException("Not implemented"); + } + + public MapSource getMapSource() { + return tileProvider.getMapSource(); + } + + private void preloadTile(CachedTile tile) { + if (preLoader.queue.remainingCapacity() < 1) { + // Preloader thread is too slow + log.trace("Preloading rejected: " + tile.key); + return; + } + if (cache.get(tile.key) != null) + return; + try { + preLoader.queue.add(tile); + cache.put(tile.key, new SRCachedTile(tile)); + } catch (IllegalStateException e) { + // Queue is "full" + log.trace("Preloading rejected: " + tile.key); + } + } + + public void cleanup() { + try { + cache.clear(); + if (preLoader != null) { + preLoader.interrupt(); + preLoader = null; + } + } catch (Throwable t) { + log.error("", t); + } + } + + @Override + protected void finalize() throws Throwable { + cleanup(); + super.finalize(); + } + + private static class SRCachedTile extends SoftReference { + + public SRCachedTile(CachedTile referent) { + super(referent); + } + + } + + private class PreLoadThread extends Thread { + + private LinkedBlockingQueue queue = null; + + public PreLoadThread() { + super("ImagePreLoadThread" + (PRELOADER_THREAD_NUM++)); + log.debug("Image pre-loader thread started"); + // pre-loading more than 20 tiles doesn't make much sense + queue = new LinkedBlockingQueue(20); + } + + @Override + public void run() { + CachedTile tile; + try { + while (true) { + tile = queue.take(); + if (tile != null && !tile.loaded) { + // log.trace("Loading image async: " + tile); + tile.loadImage(); + } + } + } catch (InterruptedException e) { + log.debug("Image pre-loader thread terminated"); + } + } + + } + + private static class CacheKey { + int x; + int y; + + public CacheKey(int x, int y) { + super(); + this.x = x; + this.y = y; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + x; + result = prime * result + y; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CacheKey other = (CacheKey) obj; + if (x != other.x) + return false; + if (y != other.y) + return false; + return true; + } + + @Override + public String toString() { + return "CacheKey [x=" + x + ", y=" + y + "]"; + } + + } + + private class CachedTile { + + CacheKey key; + private BufferedImage image; + private IOException loadException = null; + boolean loaded = false; + boolean nextLoadJobCreated = false; + + public CachedTile(CacheKey key) { + super(); + this.key = key; + image = null; + } + + public synchronized void loadImage() { + try { + image = internalGetTileImage(key.x, key.y); + } catch (IOException e) { + loadException = e; + } catch (Exception e) { + loadException = new IOException(e); + } + loaded = true; + } + + public synchronized BufferedImage getImage() throws IOException { + if (!loaded) + loadImage(); + if (loadException != null) + throw loadException; + return image; + } + + @Override + public String toString() { + return "CachedTile [key=" + key + ", loaded=" + loaded + ", nextLoadJobCreated=" + nextLoadJobCreated + "]"; + } + + } +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/ConvertedRawTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/ConvertedRawTileProvider.java new file mode 100644 index 0000000..8c97366 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/ConvertedRawTileProvider.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageFormat; + +/** + * Loads a tile from the underlying {@link TileProvider}, loads the tile to memory, converts it to the desired + * {@link TileImageFormat} and returns the binary representation of the image in the specified format. + */ +public class ConvertedRawTileProvider extends FilterTileProvider { + + private TileImageDataWriter writer; + + public ConvertedRawTileProvider(TileProvider tileProvider, TileImageFormat tileImageFormat) { + super(tileProvider); + writer = tileImageFormat.getDataWriter(); + writer.initialize(); + ImageIO.setUseCache(false); + } + + public byte[] getTileData(int x, int y) throws IOException { + BufferedImage image = getTileImage(x, y); + if (image == null) + return null; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(32000); + writer.processImage(image, buffer); + return buffer.toByteArray(); + } + + public boolean preferTileImageUsage() { + return true; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/DownloadedTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/DownloadedTileProvider.java new file mode 100644 index 0000000..2394d8c --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/DownloadedTileProvider.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageType; +import mobac.utilities.tar.TarIndex; + +import org.apache.log4j.Logger; + +public class DownloadedTileProvider implements TileProvider { + + private static final Logger log = Logger.getLogger(DownloadedTileProvider.class); + + public static final String TILE_FILENAME_PATTERN = "x%dy%d"; + + protected final TarIndex tarIndex; + protected final MapInterface map; + protected final TileImageType mapTileType; + + public DownloadedTileProvider(TarIndex tarIndex, MapInterface map) { + this.tarIndex = tarIndex; + this.map = map; + this.mapTileType = map.getMapSource().getTileImageType(); + } + + public byte[] getTileData(int x, int y) throws IOException { + log.trace("Reading tile x=" + x + " y=" + y); + return tarIndex.getEntryContent(String.format(TILE_FILENAME_PATTERN, x, y)); + } + + public BufferedImage getTileImage(int x, int y) throws IOException { + byte[] unconvertedTileData = getTileData(x, y); + if (unconvertedTileData == null) + return null; + return ImageIO.read(new ByteArrayInputStream(unconvertedTileData)); + } + + public boolean preferTileImageUsage() { + return false; + } + + public MapSource getMapSource() { + return map.getMapSource(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/FilterTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/FilterTileProvider.java new file mode 100644 index 0000000..120d4f0 --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/FilterTileProvider.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.program.interfaces.MapSource; + +import org.apache.log4j.Logger; + +/** + * Base implementation of an {@link TileProvider} that changes somehow the images, e.g. combines two layers to one or + * paints something onto a tile image. + */ +public abstract class FilterTileProvider implements TileProvider { + + protected final Logger log; + + protected final TileProvider tileProvider; + + public FilterTileProvider(TileProvider tileProvider) { + log = Logger.getLogger(this.getClass()); + this.tileProvider = tileProvider; + } + + public BufferedImage getTileImage(int x, int y) throws IOException { + return tileProvider.getTileImage(x, y); + } + + public byte[] getTileData(int x, int y) throws IOException { + return tileProvider.getTileData(x, y); + } + + public MapSource getMapSource() { + return tileProvider.getMapSource(); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/FilteredMapSourceProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/FilteredMapSourceProvider.java new file mode 100644 index 0000000..9babcfd --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/FilteredMapSourceProvider.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSource.LoadMethod; +import mobac.program.interfaces.TileFilter; + +/** + * Based on a given {@link TileFilter} implementation an {@link FilteredMapSourceProvider} instance ignores certain + * requests via {@link #getTileData(int, int)} and {@link #getTileImage(int, int)} and returns null + * instead. + * + * This functionality is required especially for polygonal maps where certain tiles which are located outside of the + * polygon should be ignored. + */ +public class FilteredMapSourceProvider extends MapSourceProvider { + + protected final TileFilter tileFilter; + + public FilteredMapSourceProvider(MapInterface map, LoadMethod loadMethod) { + this(map.getMapSource(), map.getZoom(), loadMethod, map.getTileFilter()); + } + + public FilteredMapSourceProvider(MapSource mapSource, int zoom, LoadMethod loadMethod, TileFilter tileFilter) { + super(mapSource, zoom, loadMethod); + this.tileFilter = tileFilter; + } + + @Override + public byte[] getTileData(int x, int y) throws IOException { + if (!tileFilter.testTile(x, y, zoom, mapSource)) + return null; + return super.getTileData(x, y); + } + + @Override + public BufferedImage getTileImage(int x, int y) throws IOException { + if (!tileFilter.testTile(x, y, zoom, mapSource)) + return null; + return super.getTileImage(x, y); + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/GpxPainterTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/GpxPainterTileProvider.java new file mode 100644 index 0000000..9b5d83c --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/GpxPainterTileProvider.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.data.gpx.gpx11.Gpx; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.TileImageFormat; + +/** + * + * Incomplete! + * + * TODO: Fully implement this class so that the content (points, tracks, ...) can be painted on each tile. If the + * implementation is complete the {@link GpxPainterTileProvider} can be chained into the tile provider chain after the + * {@link DownloadedTileProvider} (see AtlasThread line ~348). + * + * Problem: texts and lines that span multiple tiles. + * + */ +public class GpxPainterTileProvider extends ConvertedRawTileProvider { + + private final MapSpace mapSpace; + + public GpxPainterTileProvider(MapSourceProvider tileProvider, TileImageFormat tileImageFormat, Gpx gpx) { + super(tileProvider, tileImageFormat); + int zoom = tileProvider.getZoom(); + MapSource mapSource = tileProvider.getMapSource(); + mapSpace = mapSource.getMapSpace(); + + // TODO Prepare GPX points + } + + @Override + public BufferedImage getTileImage(int x, int y) throws IOException { + BufferedImage image = super.getTileImage(x, y); + + // TODO Perform GPX painting + + return image; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/MapSourceProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/MapSourceProvider.java new file mode 100644 index 0000000..0bdbf8c --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/MapSourceProvider.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.exceptions.TileException; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSource.LoadMethod; + +/** + * A {@link TileProvider} implementation that retrieves all tiles directly from the {@link MapSource}. + */ +public class MapSourceProvider implements TileProvider { + + protected final MapSource mapSource; + protected final int zoom; + protected final LoadMethod loadMethod; + + /** + * + * @param mapSource + * @param zoom + * @param loadMethod + * defines if the tile should be taken from tile cache or from it's original source (web server, + * generated...). + */ + public MapSourceProvider(MapSource mapSource, int zoom, LoadMethod loadMethod) { + super(); + this.mapSource = mapSource; + this.zoom = zoom; + this.loadMethod = loadMethod; + } + + public byte[] getTileData(int x, int y) throws IOException { + try { + return mapSource.getTileData(zoom, x, y, loadMethod); + } catch (TileException e) { + throw new IOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public BufferedImage getTileImage(int x, int y) throws IOException { + try { + return mapSource.getTileImage(zoom, x, y, loadMethod); + } catch (TileException e) { + throw new IOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public boolean preferTileImageUsage() { + return false; + } + + public MapSource getMapSource() { + return mapSource; + } + + public int getZoom() { + return zoom; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/PngTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/PngTileProvider.java new file mode 100644 index 0000000..797975e --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/PngTileProvider.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageType; +import mobac.program.tiledatawriter.TileImagePngDataWriter; +import mobac.utilities.Utilities; + +/** + * A tile provider for atlas formats that only allow PNG images. Each image processed is checked + */ +public class PngTileProvider extends FilterTileProvider { + + final TileImageDataWriter writer; + + public PngTileProvider(TileProvider tileProvider) { + super(tileProvider); + writer = new TileImagePngDataWriter(); + writer.initialize(); + } + + @Override + public byte[] getTileData(int x, int y) throws IOException { + if (!tileProvider.preferTileImageUsage()) { + byte[] data = super.getTileData(x, y); + if (Utilities.getImageType(data) == TileImageType.PNG) + return data; + } + ByteArrayOutputStream buffer = new ByteArrayOutputStream(32000); + BufferedImage image = getTileImage(x, y); + if (image == null) + return null; + writer.processImage(image, buffer); + return buffer.toByteArray(); + } + + public boolean preferTileImageUsage() { + return true; + } + +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/TileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/TileProvider.java new file mode 100644 index 0000000..ce979da --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/TileProvider.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.program.interfaces.MapSource; + +public interface TileProvider { + + public byte[] getTileData(int x, int y) throws IOException; + + public BufferedImage getTileImage(int x, int y) throws IOException; + + public MapSource getMapSource(); + + /** + * Indicates if subsequent filter in the filter-chain should prefer the {@link #getTileImage(int, int)} or + * {@link #getTileData(int, int)} method. + * + * @return + */ + public boolean preferTileImageUsage(); +} diff --git a/src/main/java/mobac/program/atlascreators/tileprovider/TileStoreTileProvider.java b/src/main/java/mobac/program/atlascreators/tileprovider/TileStoreTileProvider.java new file mode 100644 index 0000000..c8676ea --- /dev/null +++ b/src/main/java/mobac/program/atlascreators/tileprovider/TileStoreTileProvider.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.atlascreators.tileprovider; + +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSource.LoadMethod; + +/** + * A {@link TileProvider} implementation that retrieves all tiles from the tile store (if the mapSource + * supports that). + */ +public class TileStoreTileProvider extends MapSourceProvider { + + public TileStoreTileProvider(MapSource mapSource, int zoom) { + super(mapSource, zoom, LoadMethod.CACHE); + } + +} diff --git a/src/main/java/mobac/program/commandline/CommandLineEmpty.java b/src/main/java/mobac/program/commandline/CommandLineEmpty.java new file mode 100644 index 0000000..39a614c --- /dev/null +++ b/src/main/java/mobac/program/commandline/CommandLineEmpty.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.commandline; + +import mobac.program.interfaces.CommandLineAction; + +public class CommandLineEmpty implements CommandLineAction { + + @Override + public void runBeforeMainGUI() { + } + + @Override + public void runMainGUI() { + } + + @Override + public boolean showSplashScreen() { + return true; + } + + @Override + public boolean showMainGUI() { + return true; + } + +} diff --git a/src/main/java/mobac/program/commandline/CreateAtlas.java b/src/main/java/mobac/program/commandline/CreateAtlas.java new file mode 100644 index 0000000..798f6e7 --- /dev/null +++ b/src/main/java/mobac/program/commandline/CreateAtlas.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.commandline; + +import java.io.File; + +import javax.xml.bind.JAXBException; + +import mobac.program.AtlasThread; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.CommandLineAction; +import mobac.program.model.Profile; +import mobac.utilities.GUIExceptionHandler; + +public class CreateAtlas implements CommandLineAction { + + private final String profileName; + private final File outputDir; + + public CreateAtlas(String profileName) { + this(profileName, null); + } + + public CreateAtlas(String profileName, String outputDirectory) { + super(); + this.profileName = profileName; + if (outputDirectory != null) { + File dir = new File(outputDirectory); + if (dir.isDirectory() || dir.exists()) { + System.err.println("Error: Atlas output directory \"" + outputDirectory + "\" already exists."); + System.exit(1); + } + outputDir = dir; + } else + outputDir = null; + } + + @Override + public void runBeforeMainGUI() { + try { + Profile p = new Profile(profileName); + if (!p.exists()) { + System.err.println("Profile \"" + profileName + "\" could not be loaded:"); + System.err.println("File \"" + p.getFile().getAbsolutePath() + "\" does not exist."); + System.exit(1); + } + AtlasInterface atlas = null; + try { + atlas = p.load(); + } catch (JAXBException e) { + System.err.println("Error loading profile \"" + profileName + "\"."); + e.printStackTrace(); + System.exit(1); + } + AtlasThread atlasThread = new AtlasThread(atlas); + if (outputDir != null) + atlasThread.setCustomAtlasDir(outputDir); + atlasThread.setQuitMobacAfterAtlasCreation(true); + atlasThread.start(); + } catch (Exception e) { + GUIExceptionHandler.processException(e); + } + } + + @Override + public void runMainGUI() { + } + + @Override + public boolean showSplashScreen() { + return false; + } + + @Override + public boolean showMainGUI() { + return false; + } + +} diff --git a/src/main/java/mobac/program/download/DownloadJob.java b/src/main/java/mobac/program/download/DownloadJob.java new file mode 100644 index 0000000..24872ab --- /dev/null +++ b/src/main/java/mobac/program/download/DownloadJob.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.download; + +import java.net.ConnectException; +import java.net.SocketTimeoutException; + +import mobac.exceptions.DownloadFailedException; +import mobac.exceptions.StopAllDownloadsException; +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.program.JobDispatcher; +import mobac.program.JobDispatcher.Job; +import mobac.program.atlascreators.tileprovider.DownloadedTileProvider; +import mobac.program.interfaces.DownloadJobListener; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSource.LoadMethod; +import mobac.utilities.tar.TarIndexedArchive; + +import org.apache.log4j.Logger; + +public class DownloadJob implements Job { + + static Logger log = Logger.getLogger(DownloadJob.class); + + int errorCounter = 0; + + final MapSource mapSource; + final int xValue; + final int yValue; + final int zoomValue; + final TarIndexedArchive tileArchive; + final DownloadJobListener listener; + + public DownloadJob(MapSource mapSource, int xValue, int yValue, int zoomValue, TarIndexedArchive tileArchive, + DownloadJobListener listener) { + this.mapSource = mapSource; + this.xValue = xValue; + this.yValue = yValue; + this.zoomValue = zoomValue; + this.tileArchive = tileArchive; + this.listener = listener; + } + + public void run(JobDispatcher dispatcher) throws Exception { + try { + // Thread.sleep(1500); + listener.jobStarted(); + byte[] tileData = mapSource.getTileData(zoomValue, xValue, yValue, LoadMethod.DEFAULT); + String tileFileName = String.format(DownloadedTileProvider.TILE_FILENAME_PATTERN, xValue, yValue); + if (tileArchive != null) { + synchronized (tileArchive) { + tileArchive.writeFileFromData(tileFileName, tileData); + } + } + listener.jobFinishedSuccessfully(tileData.length); + } catch (UnrecoverableDownloadException e) { + listener.jobFinishedWithError(false); + log.error("Download of tile z" + zoomValue + "_x" + xValue + "_y" + yValue + + " failed with an unrecoverable error: " + e.getCause()); + } catch (InterruptedException e) { + throw e; + } catch (StopAllDownloadsException e) { + throw e; + } catch (SocketTimeoutException e) { + processError(dispatcher, e); + } catch (ConnectException e) { + processError(dispatcher, e); + } catch (DownloadFailedException e) { + processError(dispatcher, e); + } catch (Exception e) { + processError(dispatcher, e); + throw e; + } + } + + private void processError(JobDispatcher dispatcher, Exception e) { + errorCounter++; + // Reschedule job to try it later again + if (errorCounter <= listener.getMaxDownloadRetries()) { + listener.jobFinishedWithError(true); + log.warn("Download of tile z" + zoomValue + "_x" + xValue + "_y" + yValue + " failed: \"" + e.getMessage() + + "\" (tries: " + errorCounter + ") - rescheduling download job"); + dispatcher.addErrorJob(this); + } else { + listener.jobFinishedWithError(false); + log.error("Download of tile z" + zoomValue + "_x" + xValue + "_y" + yValue + " failed again: \"" + + e.getMessage() + "\". Retry limit reached, " + "job will not be rescheduled (no further try)"); + } + } + + @Override + public String toString() { + return "DownloadJob x=" + xValue + " y=" + yValue + " z=" + zoomValue; + } + +} diff --git a/src/main/java/mobac/program/download/DownloadJobProducerThread.java b/src/main/java/mobac/program/download/DownloadJobProducerThread.java new file mode 100644 index 0000000..cda6f3a --- /dev/null +++ b/src/main/java/mobac/program/download/DownloadJobProducerThread.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.download; + +import java.util.Enumeration; + +import mobac.program.AtlasThread; +import mobac.program.JobDispatcher; +import mobac.program.JobDispatcher.Job; +import mobac.program.interfaces.DownloadableElement; +import mobac.utilities.tar.TarIndexedArchive; + +import org.apache.log4j.Logger; + + +/** + * Creates the jobs for downloading tiles. If the job queue is full it will + * block on {@link JobDispatcher#addJob(Job)} + */ +public class DownloadJobProducerThread extends Thread { + + private Logger log = Logger.getLogger(DownloadJobProducerThread.class); + + final JobDispatcher downloadJobDispatcher; + + final Enumeration jobEnumerator; + + public DownloadJobProducerThread(AtlasThread atlasThread, JobDispatcher downloadJobDispatcher, + TarIndexedArchive tileArchive, DownloadableElement de) { + this.downloadJobDispatcher = downloadJobDispatcher; + jobEnumerator = de.getDownloadJobs(tileArchive, atlasThread); + start(); + } + + @Override + public void run() { + try { + while (jobEnumerator.hasMoreElements()) { + Job job = jobEnumerator.nextElement(); + downloadJobDispatcher.addJob(job); + log.trace("Job added: " + job); + } + log.debug("All download jobs has been generated"); + } catch (InterruptedException e) { + downloadJobDispatcher.cancelOutstandingJobs(); + log.error("Download job generation interrupted"); + } + } + + public void cancel() { + try { + interrupt(); + } catch (Exception e) { + } + } + +} diff --git a/src/main/java/mobac/program/download/TileDownLoader.java b/src/main/java/mobac/program/download/TileDownLoader.java new file mode 100644 index 0000000..a00978f --- /dev/null +++ b/src/main/java/mobac/program/download/TileDownLoader.java @@ -0,0 +1,406 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.download; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; + +import mobac.exceptions.DownloadFailedException; +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSourceListener; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.Settings; +import mobac.program.model.TileImageType; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreEntry; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ThrottledInputStream; + +import org.apache.log4j.Logger; + +public class TileDownLoader { + + public static String ACCEPT = "text/html, image/png, image/jpeg, image/gif, */*;q=0.1"; + + static { + Object defaultReadTimeout = System.getProperty("sun.net.client.defaultReadTimeout"); + if (defaultReadTimeout == null) + System.setProperty("sun.net.client.defaultReadTimeout", "15000"); + System.setProperty("http.maxConnections", "20"); + } + + private static Logger log = Logger.getLogger(TileDownLoader.class); + + private static Settings settings = Settings.getInstance(); + + public static byte[] getImage(int x, int y, int zoom, HttpMapSource mapSource) throws IOException, + InterruptedException, UnrecoverableDownloadException { + + MapSpace mapSpace = mapSource.getMapSpace(); + int maxTileIndex = mapSpace.getMaxPixels(zoom) / mapSpace.getTileSize(); + if (x > maxTileIndex) + throw new RuntimeException("Invalid tile index x=" + x + " for zoom " + zoom); + if (y > maxTileIndex) + throw new RuntimeException("Invalid tile index y=" + y + " for zoom " + zoom); + + TileStore ts = TileStore.getInstance(); + + // Thread.sleep(2000); + + // Test code for creating random download failures + // if (Math.random()>0.7) throw new + // IOException("intentionally download error"); + + Settings s = Settings.getInstance(); + + TileStoreEntry tile = null; + if (s.tileStoreEnabled) { + + // Copy the file from the persistent tilestore instead of + // downloading it from internet. + tile = ts.getTile(x, y, zoom, mapSource); + boolean expired = isTileExpired(tile); + if (tile != null) { + if (expired) { + log.trace("Expired: " + mapSource.getName() + " " + tile); + } else { + log.trace("Tile of map source " + mapSource.getName() + " used from tilestore"); + byte[] data = tile.getData(); + notifyCachedTileUsed(data.length); + return data; + } + } + } + byte[] data = null; + if (tile == null) { + data = downloadTileAndUpdateStore(x, y, zoom, mapSource); + notifyTileDownloaded(data.length); + } else { + byte[] updatedData = updateStoredTile(tile, mapSource); + if (updatedData != null) { + data = updatedData; + notifyTileDownloaded(data.length); + } else { + data = tile.getData(); + notifyCachedTileUsed(data.length); + } + } + return data; + } + + private static void notifyTileDownloaded(int size) { + if (Thread.currentThread() instanceof MapSourceListener) { + ((MapSourceListener) Thread.currentThread()).tileDownloaded(size); + } + + } + + private static void notifyCachedTileUsed(int size) { + if (Thread.currentThread() instanceof MapSourceListener) { + ((MapSourceListener) Thread.currentThread()).tileLoadedFromCache(size); + } + } + + /** + * Download the tile from the web server and updates the tile store if the tile could be successfully retrieved. + * + * @param x + * @param y + * @param zoom + * @param mapSource + * @return + * @throws UnrecoverableDownloadException + * @throws IOException + * @throws InterruptedException + */ + public static byte[] downloadTileAndUpdateStore(int x, int y, int zoom, HttpMapSource mapSource) + throws UnrecoverableDownloadException, IOException, InterruptedException { + return downloadTileAndUpdateStore(x, y, zoom, mapSource, Settings.getInstance().tileStoreEnabled); + } + + public static byte[] downloadTile(int x, int y, int zoom, HttpMapSource mapSource) + throws UnrecoverableDownloadException, IOException, InterruptedException { + return downloadTileAndUpdateStore(x, y, zoom, mapSource, false); + } + + public static byte[] downloadTileAndUpdateStore(int x, int y, int zoom, HttpMapSource mapSource, + boolean useTileStore) throws UnrecoverableDownloadException, IOException, InterruptedException { + + if (zoom < 0) + throw new UnrecoverableDownloadException("Negative zoom!"); + HttpURLConnection conn = mapSource.getTileUrlConnection(zoom, x, y); + if (conn == null) + throw new UnrecoverableDownloadException("Tile x=" + x + " y=" + y + " zoom=" + zoom + + " is not a valid tile in map source " + mapSource); + + log.trace("Downloading " + conn.getURL()); + + prepareConnection(conn); + conn.connect(); + + int code = conn.getResponseCode(); + byte[] data = loadBodyDataInBuffer(conn); + + if (code != HttpURLConnection.HTTP_OK) + throw new DownloadFailedException(conn, code); + + checkContentType(conn, data); + checkContentLength(conn, data); + + String eTag = conn.getHeaderField("ETag"); + long timeLastModified = conn.getLastModified(); + long timeExpires = conn.getExpiration(); + + Utilities.checkForInterruption(); + TileImageType imageType = Utilities.getImageType(data); + if (imageType == null) + throw new UnrecoverableDownloadException("The returned image is of unknown format"); + if (useTileStore) { + TileStore.getInstance().putTileData(data, x, y, zoom, mapSource, timeLastModified, timeExpires, eTag); + } + Utilities.checkForInterruption(); + return data; + } + + public static byte[] updateStoredTile(TileStoreEntry tile, HttpMapSource mapSource) + throws UnrecoverableDownloadException, IOException, InterruptedException { + final int x = tile.getX(); + final int y = tile.getY(); + final int zoom = tile.getZoom(); + final HttpMapSource.TileUpdate tileUpdate = mapSource.getTileUpdate(); + + switch (tileUpdate) { + case ETag: { + boolean unchanged = hasTileETag(tile, mapSource); + if (unchanged) { + if (log.isTraceEnabled()) + log.trace("Data unchanged on server (eTag): " + mapSource + " " + tile); + return null; + } + break; + } + case LastModified: { + boolean isNewer = isTileNewer(tile, mapSource); + if (!isNewer) { + if (log.isTraceEnabled()) + log.trace("Data unchanged on server (LastModified): " + mapSource + " " + tile); + return null; + } + break; + } + } + HttpURLConnection conn = mapSource.getTileUrlConnection(zoom, x, y); + if (conn == null) + throw new UnrecoverableDownloadException("Tile x=" + x + " y=" + y + " zoom=" + zoom + + " is not a valid tile in map source " + mapSource); + + if (log.isTraceEnabled()) + log.trace(String.format("Checking %s %s", mapSource.getName(), tile)); + + prepareConnection(conn); + + boolean conditionalRequest = false; + + switch (tileUpdate) { + case IfNoneMatch: { + if (tile.geteTag() != null) { + conn.setRequestProperty("If-None-Match", tile.geteTag()); + conditionalRequest = true; + } + break; + } + case IfModifiedSince: { + if (tile.getTimeLastModified() > 0) { + conn.setIfModifiedSince(tile.getTimeLastModified()); + conditionalRequest = true; + } + break; + } + } + + conn.connect(); + + Settings s = Settings.getInstance(); + + int code = conn.getResponseCode(); + + if (conditionalRequest && code == HttpURLConnection.HTTP_NOT_MODIFIED) { + // Data unchanged on server + if (s.tileStoreEnabled) { + tile.update(conn.getExpiration()); + TileStore.getInstance().putTile(tile, mapSource); + } + if (log.isTraceEnabled()) + log.trace("Data unchanged on server: " + mapSource + " " + tile); + return null; + } + byte[] data = loadBodyDataInBuffer(conn); + + if (code != HttpURLConnection.HTTP_OK) + throw new DownloadFailedException(conn, code); + + checkContentType(conn, data); + checkContentLength(conn, data); + + String eTag = conn.getHeaderField("ETag"); + long timeLastModified = conn.getLastModified(); + long timeExpires = conn.getExpiration(); + + Utilities.checkForInterruption(); + TileImageType imageType = Utilities.getImageType(data); + if (imageType == null) + throw new UnrecoverableDownloadException("The returned image is of unknown format"); + if (s.tileStoreEnabled) { + TileStore.getInstance().putTileData(data, x, y, zoom, mapSource, timeLastModified, timeExpires, eTag); + } + Utilities.checkForInterruption(); + return data; + } + + public static boolean isTileExpired(TileStoreEntry tileStoreEntry) { + if (tileStoreEntry == null) + return true; + long expiredTime = tileStoreEntry.getTimeExpires(); + if (expiredTime >= 0) { + // server had set an expiration time + long maxExpirationTime = settings.tileMaxExpirationTime + tileStoreEntry.getTimeDownloaded(); + long minExpirationTime = settings.tileMinExpirationTime + tileStoreEntry.getTimeDownloaded(); + expiredTime = Math.max(minExpirationTime, Math.min(maxExpirationTime, expiredTime)); + } else { + // no expiration time set by server - use the default one + expiredTime = tileStoreEntry.getTimeDownloaded() + settings.tileDefaultExpirationTime; + } + return (expiredTime < System.currentTimeMillis()); + } + + /** + * Reads all available data from the input stream of conn and returns it as byte array. If no input + * data is available the method returns null. + * + * @param conn + * @return + * @throws IOException + */ + protected static byte[] loadBodyDataInBuffer(HttpURLConnection conn) throws IOException { + InputStream input = conn.getInputStream(); + byte[] data = null; + try { + if (Thread.currentThread() instanceof MapSourceListener) { + // We only throttle atlas downloads, not downloads for the preview map + long bandwidthLimit = Settings.getInstance().getBandwidthLimit(); + if (bandwidthLimit > 0) { + input = new ThrottledInputStream(input); + } + } + data = Utilities.getInputBytes(input); + } catch (IOException e) { + InputStream errorIn = conn.getErrorStream(); + try { + byte[] errData = Utilities.getInputBytes(errorIn); + log.trace("Retrieved " + errData.length + " error bytes for a HTTP " + conn.getResponseCode()); + } catch (Exception ee) { + log.debug("Error retrieving error stream content: " + e); + } finally { + Utilities.closeStream(errorIn); + } + throw e; + } finally { + Utilities.closeStream(input); + } + log.trace("Retrieved " + data.length + " bytes for a HTTP " + conn.getResponseCode()); + if (data.length == 0) + return null; + return data; + } + + /** + * Performs a HEAD request for retrieving the LastModified header value. + */ + protected static boolean isTileNewer(TileStoreEntry tile, HttpMapSource mapSource) throws IOException { + long oldLastModified = tile.getTimeLastModified(); + if (oldLastModified <= 0) { + log.warn("Tile age comparison not possible: " + "tile in tilestore does not contain lastModified attribute"); + return true; + } + HttpURLConnection conn = mapSource.getTileUrlConnection(tile.getZoom(), tile.getX(), tile.getY()); + conn.setRequestMethod("HEAD"); + conn.setRequestProperty("Accept", ACCEPT); + long newLastModified = conn.getLastModified(); + if (newLastModified == 0) + return true; + return (newLastModified > oldLastModified); + } + + protected static boolean hasTileETag(TileStoreEntry tile, HttpMapSource mapSource) throws IOException { + String eTag = tile.geteTag(); + if (eTag == null || eTag.length() == 0) { + log.warn("ETag check not possible: " + "tile in tilestore does not contain ETag attribute"); + return true; + } + HttpURLConnection conn = mapSource.getTileUrlConnection(tile.getZoom(), tile.getX(), tile.getY()); + conn.setRequestMethod("HEAD"); + conn.setRequestProperty("Accept", ACCEPT); + String onlineETag = conn.getHeaderField("ETag"); + if (onlineETag == null || onlineETag.length() == 0) + return true; + return (onlineETag.equals(eTag)); + } + + protected static void prepareConnection(HttpURLConnection conn) throws ProtocolException { + conn.setRequestMethod("GET"); + + Settings s = Settings.getInstance(); + conn.setConnectTimeout(1000 * s.httpConnectionTimeout); + conn.setReadTimeout(1000 * s.httpReadTimeout); + if (conn.getRequestProperty("User-agent") == null) + conn.setRequestProperty("User-agent", s.getUserAgent()); + conn.setRequestProperty("Accept", ACCEPT); + } + + protected static void checkContentType(HttpURLConnection conn, byte[] data) throws UnrecoverableDownloadException { + String contentType = conn.getContentType(); + if (contentType != null) { + contentType = contentType.toLowerCase(); + if (!contentType.startsWith("image/")) { + if (log.isTraceEnabled() && contentType.startsWith("text/")) { + log.trace("Content (" + contentType + "): " + new String(data)); + } + throw new UnrecoverableDownloadException("Content type of the loaded image is unknown: " + contentType, + UnrecoverableDownloadException.ERROR_CODE_CONTENT_TYPE); + } + } + } + + /** + * Check if the retrieved data length is equal to the header value Content-Length + * + * @param conn + * @param data + * @throws UnrecoverableDownloadException + */ + protected static void checkContentLength(HttpURLConnection conn, byte[] data) throws UnrecoverableDownloadException { + int len = conn.getContentLength(); + if (len < 0) + return; + if (data.length != len) + throw new UnrecoverableDownloadException("Content length is not as declared by the server: retrived=" + + data.length + " bytes expected-content-length=" + len + " bytes"); + } +} diff --git a/src/main/java/mobac/program/download/UserAgent.java b/src/main/java/mobac/program/download/UserAgent.java new file mode 100644 index 0000000..af036bf --- /dev/null +++ b/src/main/java/mobac/program/download/UserAgent.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.download; + +public class UserAgent { + + public static final String IE7_XP = "Mozilla/4.0 (compatible; MSIE 7.0; " + + "Windows NT 5.1; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1))"; + + public static final String IE6_XP = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"; + + public static final String IE9_WIN7 = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"; + + public static final String FF2_XP = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.8.1.17) Gecko/20080829"; + + public static final String FF3_XP = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9.2) Gecko/20100115 Firefox/3.6"; + + public static final String FF3_WIN7 = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15"; + + public static final String OPERA11_WIN7 = "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.7.62 Version/11.01"; + + private String name; + private String userAgent; + + protected UserAgent(String name, String userAgent) { + super(); + this.name = name; + this.userAgent = userAgent; + } + + public String getName() { + return name; + } + + public String getUserAgent() { + return userAgent; + } + + @Override + public String toString() { + return name; + } + +} diff --git a/src/main/java/mobac/program/download/jobenumerators/DownloadJobEnumerator.java b/src/main/java/mobac/program/download/jobenumerators/DownloadJobEnumerator.java new file mode 100644 index 0000000..cf7a70c --- /dev/null +++ b/src/main/java/mobac/program/download/jobenumerators/DownloadJobEnumerator.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.download.jobenumerators; + +import java.awt.Point; +import java.util.Enumeration; + +import mobac.program.JobDispatcher.Job; +import mobac.program.download.DownloadJob; +import mobac.program.interfaces.DownloadJobListener; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.TileFilter; +import mobac.program.model.Map; +import mobac.utilities.tar.TarIndexedArchive; + +/** + * Enumerates / creates the download jobs for a regular rectangle single layer map. + */ +public class DownloadJobEnumerator implements Enumeration { + + final protected TileFilter tileFilter; + final protected DownloadJobListener listener; + final protected int xMin; + final protected int xMax; + final protected int yMax; + final protected int zoom; + final protected MapSource mapSource; + final protected TarIndexedArchive tileArchive; + + protected int x, y; + protected Job nextJob; + + /** + * This enumerator is the unfolded version for two encapsulated loops: + * + *
+	 * for (int y = yMin; y <= yMax; y++) {
+	 * 	for (int x = xMin; x <= xMax; x++) {
+	 * 		DownloadJob job = new DownloadJob(downloadDestinationDir, tileSource, x, y, zoom, AtlasThread.this);
+	 * 	}
+	 * }
+	 * 
+ * + * @param map + * @param tileArchive + * @param listener + */ + public DownloadJobEnumerator(Map map, MapSource mapSource, TarIndexedArchive tileArchive, + DownloadJobListener listener) { + this.tileFilter = map.getTileFilter(); + this.listener = listener; + Point minCoord = map.getMinTileCoordinate(); + Point maxCoord = map.getMaxTileCoordinate(); + int tileSize = map.getMapSource().getMapSpace().getTileSize(); + this.xMin = minCoord.x / tileSize; + this.xMax = maxCoord.x / tileSize; + int yMin = minCoord.y / tileSize; + this.yMax = maxCoord.y / tileSize; + this.zoom = map.getZoom(); + this.tileArchive = tileArchive; + this.mapSource = mapSource; + y = yMin; + x = xMin; + + nextJob = new DownloadJob(mapSource, x, y, zoom, tileArchive, listener); + if (!tileFilter.testTile(x, y, zoom, mapSource)) + nextElement(); + } + + public boolean hasMoreElements() { + return (nextJob != null); + } + + public Job nextElement() { + Job job = nextJob; + boolean filter = false; + do { + x++; + if (x > xMax) { + y++; + x = xMin; + if (y > yMax) { + nextJob = null; + return job; + } + } + filter = tileFilter.testTile(x, y, zoom, mapSource); + } while (!filter); + nextJob = new DownloadJob(mapSource, x, y, zoom, tileArchive, listener); + return job; + } +} diff --git a/src/main/java/mobac/program/interfaces/AtlasInterface.java b/src/main/java/mobac/program/interfaces/AtlasInterface.java new file mode 100644 index 0000000..18776f5 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/AtlasInterface.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import mobac.program.model.AtlasOutputFormat; + +public interface AtlasInterface extends AtlasObject, Iterable { + + /** + * @return Number of layers in this atlas + */ + public int getLayerCount(); + + /** + * + * @param index + * 0 - ({@link #getLayerCount()}-1) + * @return + */ + public LayerInterface getLayer(int index); + + public void addLayer(LayerInterface l); + + public void deleteLayer(LayerInterface l); + + public void setOutputFormat(AtlasOutputFormat atlasOutputFormat); + + public AtlasOutputFormat getOutputFormat(); + + public long calculateTilesToDownload(); + + public int getVersion(); + + public AtlasInterface deepClone(); +} diff --git a/src/main/java/mobac/program/interfaces/AtlasObject.java b/src/main/java/mobac/program/interfaces/AtlasObject.java new file mode 100644 index 0000000..ec6acd6 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/AtlasObject.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import mobac.exceptions.InvalidNameException; + +/** + * Marker interface that indicates that the implementing class/instance is an + * atlas or is part of an atlas (layer or map) + */ +public interface AtlasObject { + + public String getName(); + + public void setName(String newName) throws InvalidNameException; + + /** + * Called after loading the complete atlas from a profile. + * + * @return any problems found? true=yes + */ + public boolean checkData(); + + public double getMinLat(); + public double getMaxLat(); + public double getMinLon(); + public double getMaxLon(); +} diff --git a/src/main/java/mobac/program/interfaces/CapabilityDeletable.java b/src/main/java/mobac/program/interfaces/CapabilityDeletable.java new file mode 100644 index 0000000..8480376 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/CapabilityDeletable.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import mobac.gui.atlastree.JAtlasTree; +import mobac.program.model.AtlasTreeModel; + +/** + * Identifies nodes in {@link JAtlasTree} / {@link AtlasTreeModel} that can be + * deleted (including sub-nodes). Nodes implementing this interface will show a + * "delete" entry in it's context menu. + */ +public interface CapabilityDeletable { + + public void delete(); +} diff --git a/src/main/java/mobac/program/interfaces/CommandLineAction.java b/src/main/java/mobac/program/interfaces/CommandLineAction.java new file mode 100644 index 0000000..61db376 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/CommandLineAction.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface CommandLineAction { + + public boolean showSplashScreen(); + + public boolean showMainGUI(); + + public void runBeforeMainGUI(); + + public void runMainGUI(); +} diff --git a/src/main/java/mobac/program/interfaces/DownloadJobListener.java b/src/main/java/mobac/program/interfaces/DownloadJobListener.java new file mode 100644 index 0000000..7120b71 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/DownloadJobListener.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface DownloadJobListener { + + public int getMaxDownloadRetries(); + + public void jobStarted(); + + public void jobFinishedSuccessfully(int bytesDownloaded); + + public void jobFinishedWithError(boolean retry); + +} diff --git a/src/main/java/mobac/program/interfaces/DownloadableElement.java b/src/main/java/mobac/program/interfaces/DownloadableElement.java new file mode 100644 index 0000000..dd0430a --- /dev/null +++ b/src/main/java/mobac/program/interfaces/DownloadableElement.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import java.util.Enumeration; + +import mobac.program.JobDispatcher.Job; +import mobac.utilities.tar.TarIndexedArchive; + + +/** + * Classes that implement this interface identify themselves as responsible for + * specifying what tiles should be downloaded. + * + * In general this interface should be implemented in combination with + * {@link MapInterface}, {@link LayerInterface} or {@link AtlasInterface}. + * + */ +public interface DownloadableElement { + + /** + * + * @param tileArchive + * @param listener + * @return An enumeration that returns {@link Job} objects. Each job should + * download one map tile from the providing web server (or from the + * tile cache). + */ + public Enumeration getDownloadJobs(TarIndexedArchive tileArchive, + DownloadJobListener listener); + +} diff --git a/src/main/java/mobac/program/interfaces/ExceptionExtendedInfo.java b/src/main/java/mobac/program/interfaces/ExceptionExtendedInfo.java new file mode 100644 index 0000000..1291454 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/ExceptionExtendedInfo.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface ExceptionExtendedInfo { + + public String getExtendedInfo(); +} diff --git a/src/main/java/mobac/program/interfaces/FileBasedMapSource.java b/src/main/java/mobac/program/interfaces/FileBasedMapSource.java new file mode 100644 index 0000000..40dd846 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/FileBasedMapSource.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +/** + * Marker interface for file based map sources (map sources that use tiles present in the file system and therefore do + * not stress online tile servers. + */ +public interface FileBasedMapSource extends InitializableMapSource { + + public void reinitialize(); + +} diff --git a/src/main/java/mobac/program/interfaces/HttpMapSource.java b/src/main/java/mobac/program/interfaces/HttpMapSource.java new file mode 100644 index 0000000..79a7e2f --- /dev/null +++ b/src/main/java/mobac/program/interfaces/HttpMapSource.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import java.io.IOException; +import java.net.HttpURLConnection; + +public interface HttpMapSource extends MapSource { + /** + * Specifies the different mechanisms for detecting updated tiles respectively only download newer tiles than those + * stored locally. + * + *
    + *
  • {@link #IfNoneMatch} Server provides ETag header entry for all tiles and supports conditional download + * via If-None-Match header entry.
  • + *
  • {@link #ETag} Server provides ETag header entry for all tiles but does not support conditional + * download via If-None-Match header entry.
  • + *
  • {@link #IfModifiedSince} Server provides Last-Modified header entry for all tiles and supports + * conditional download via If-Modified-Since header entry.
  • + *
  • {@link #LastModified} Server provides Last-Modified header entry for all tiles but does not support + * conditional download via If-Modified-Since header entry.
  • + *
  • {@link #None} The server does not support any of the listed mechanisms.
  • + *
+ * + */ + public enum TileUpdate { + IfNoneMatch, ETag, IfModifiedSince, LastModified, None + } + + /** + * @return The supported tile update mechanism + * @see TileUpdate + */ + public TileUpdate getTileUpdate(); + + /** + * Constructs the tile url connection. If necessary the url connection can be prepared with cookies or other http + * specific headers which are required by the http server. + * + * @param zoom + * @param tilex + * tile number on x-axis for the specified zoom level + * @param tiley + * tile number on y-axis for the specified zoom level + * @return the initialized urlConnection for downloading the specified tile image + */ + public HttpURLConnection getTileUrlConnection(int zoom, int tilex, int tiley) throws IOException; + +} diff --git a/src/main/java/mobac/program/interfaces/InitializableMapSource.java b/src/main/java/mobac/program/interfaces/InitializableMapSource.java new file mode 100644 index 0000000..b21d86d --- /dev/null +++ b/src/main/java/mobac/program/interfaces/InitializableMapSource.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface InitializableMapSource extends MapSource { + + public void initialize(); + +} diff --git a/src/main/java/mobac/program/interfaces/LayerInterface.java b/src/main/java/mobac/program/interfaces/LayerInterface.java new file mode 100644 index 0000000..e1f58d0 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/LayerInterface.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface LayerInterface extends AtlasObject, Iterable, CapabilityDeletable { + + public void addMap(MapInterface map); + + public int getMapCount(); + + public MapInterface getMap(int index); + + public AtlasInterface getAtlas(); + + public long calculateTilesToDownload(); + + public LayerInterface deepClone(AtlasInterface atlas); + +} diff --git a/src/main/java/mobac/program/interfaces/MapInterface.java b/src/main/java/mobac/program/interfaces/MapInterface.java new file mode 100644 index 0000000..7ff0590 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/MapInterface.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import java.awt.Dimension; +import java.awt.Point; + +import mobac.program.model.TileImageParameters; + + + +public interface MapInterface extends AtlasObject, CapabilityDeletable { + + public Point getMinTileCoordinate(); + + public Point getMaxTileCoordinate(); + + public int getZoom(); + + public MapSource getMapSource(); + + public Dimension getTileSize(); + + public LayerInterface getLayer(); + + public void setLayer(LayerInterface layer); + + public TileImageParameters getParameters(); + + public void setParameters(TileImageParameters p); + + public long calculateTilesToDownload(); + + public String getInfoText(); + + public TileFilter getTileFilter(); + + public MapInterface deepClone(LayerInterface newLayer); + +} diff --git a/src/main/java/mobac/program/interfaces/MapSource.java b/src/main/java/mobac/program/interfaces/MapSource.java new file mode 100644 index 0000000..d92dfb6 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/MapSource.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.exceptions.TileException; +import mobac.exceptions.UnrecoverableDownloadException; +import mobac.gui.mapview.JMapViewer; +import mobac.program.jaxb.MapSourceAdapter; +import mobac.program.model.MapSourceLoaderInfo; +import mobac.program.model.TileImageType; + +//License: GPL. Copyright 2008 by Jan Peter Stotz + +/** + * + * @author Jan Peter Stotz + */ +@XmlJavaTypeAdapter(MapSourceAdapter.class) +public interface MapSource { + + public enum LoadMethod { + DEFAULT, CACHE, SOURCE + }; + + /** + * Specifies the maximum zoom value. The number of zoom levels is [0.. {@link #getMaxZoom()}]. + * + * @return maximum zoom value that has to be smaller or equal to {@link JMapViewer#MAX_ZOOM} + */ + public int getMaxZoom(); + + /** + * Specifies the minimum zoom value. This value is usually 0. Only for maps that cover a certain region up to a + * limited zoom level this method should return a value different than 0. + * + * @return minimum zoom value - usually 0 + */ + public int getMinZoom(); + + /** + * A tile layer name has to be unique and has to consist only of characters valid for filenames. + * + * @return Name of the tile layer + */ + public String getName(); + + /** + * + * @param zoom + * @param x + * @param y + * @param loadMethod + * TODO + * @return + * @throws IOException + * @throws InterruptedException + * @throws UnrecoverableDownloadException + */ + public byte[] getTileData(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException; + + /** + * + * @param zoom + * @param x + * @param y + * @param loadMethod + * @return + * @throws IOException + * @throws UnrecoverableDownloadException + * @throws InterruptedException + */ + public BufferedImage getTileImage(int zoom, int x, int y, LoadMethod loadMethod) throws IOException, TileException, + InterruptedException; + + /** + * Specifies the tile image type. For tiles rendered by Mapnik or Osmarenderer this is usually + * {@link TileImageType#PNG}. + * + * @return file extension of the tile image type + */ + public TileImageType getTileImageType(); + + public MapSpace getMapSpace(); + + public Color getBackgroundColor(); + + @XmlTransient + public MapSourceLoaderInfo getLoaderInfo(); + + public void setLoaderInfo(MapSourceLoaderInfo loaderInfo); + +} diff --git a/src/main/java/mobac/program/interfaces/MapSourceListener.java b/src/main/java/mobac/program/interfaces/MapSourceListener.java new file mode 100644 index 0000000..07edf17 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/MapSourceListener.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface MapSourceListener { + + public void tileDownloaded(int size); + + public void tileLoadedFromCache(int size); +} diff --git a/src/main/java/mobac/program/interfaces/MapSourceTextAttribution.java b/src/main/java/mobac/program/interfaces/MapSourceTextAttribution.java new file mode 100644 index 0000000..9a10cc4 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/MapSourceTextAttribution.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface MapSourceTextAttribution { + + public String getAttributionText(); + + /** + * @return The URL to open when the user clicks the attribution image. + */ + public String getAttributionLinkURL(); +} diff --git a/src/main/java/mobac/program/interfaces/MapSpace.java b/src/main/java/mobac/program/interfaces/MapSpace.java new file mode 100644 index 0000000..a74139e --- /dev/null +++ b/src/main/java/mobac/program/interfaces/MapSpace.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import java.awt.Point; + +import mobac.mapsources.mapspace.MercatorPower2MapSpace; + +/** + * Preparation for supporting map resolutions other than those uses by + * Google/OpenstreetMap. + * + * {@link MercatorPower2MapSpace} is the only implementation that is currently + * supported by Mobile Atlas Creator. + *

+ * DO NOT TRY TO IMPLEMENT YOUR OWN. IT WILL NOT WORK! + *

+ */ +public interface MapSpace { + + public enum ProjectionCategory { + SPHERE, ELLIPSOID, GEOID + }; + + public ProjectionCategory getProjectionCategory(); + + public int getMaxPixels(int zoom); + + /** + * @return size (height and width) of each tile in pixel + */ + public int getTileSize(); + + /** + * Converts the horizontal pixel coordinate from map space to longitude. + * + * @param lon + * @param zoom + * @return + */ + public int cLonToX(double lon, int zoom); + + /** + * Converts the vertical pixel coordinate from map space to latitude. + * + * @param lat + * @param zoom + * @return + */ + public int cLatToY(double lat, int zoom); + + /** + * Converts longitude to the horizontal pixel coordinate from map space. + * + * @param x + * @param zoom + * @return + */ + public double cXToLon(int x, int zoom); + + /** + * Converts latitude to the vertical pixel coordinate from map space. + * + * @param y + * @param zoom + * @return + */ + public double cYToLat(int y, int zoom); + + /** + * "Walks" westerly a certain distance on a latitude and returns the + * "mileage" in map space pixels. The distance is specified as angular + * distance, therefore this method works with all length unit systems (e.g. + * metric, imperial, ...). + * + * @param startX + * x-coordinate of start point + * @param y + * y-coordinate specifying the latitude to "walk" on + * @param zoom + * @param angularDist + * angular distance: distance / earth radius (e.g. 6367.5km or + * 3956.6miles) + * @return "mileage" in number of pixels + */ + public int moveOnLatitude(int startX, int y, int zoom, double angularDist); + + /** + * Calculates the distance between two points that are laying on the same + * latitude / y-coordinate. + * + * @param zoom + * @param y + * y-coordinate specifying the latitude + * @param xDist + * distance in pixels on the x-axis + * @return angular distance angular distance: distance / earth radius (e.g. + * 6367.5km or 3956.6miles) + */ + public double horizontalDistance(int zoom, int y, int xDist); + + public Point changeZoom(Point pixelCoordinate, int oldZoom, int newZoom); + + public int xChangeZoom(int x, int oldZoom, int newZoom); + + public int yChangeZoom(int y, int oldZoom, int newZoom); + +} diff --git a/src/main/java/mobac/program/interfaces/RequiresSQLite.java b/src/main/java/mobac/program/interfaces/RequiresSQLite.java new file mode 100644 index 0000000..d07b624 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/RequiresSQLite.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +/** + * Marker-Interface which has to implemented by all atlas creator classes which require the SQLite libraries. + */ +public interface RequiresSQLite { + + /** + * Accumulate tiles in batch process until 20MB of heap are remaining + */ + public static final long HEAP_MIN = 20 * 1024 * 1024; +} diff --git a/src/main/java/mobac/program/interfaces/TileFilter.java b/src/main/java/mobac/program/interfaces/TileFilter.java new file mode 100644 index 0000000..152a4d4 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/TileFilter.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +/** + * Basic interface that allows to filter tiles based on their position and zoom level in the map. + */ +public interface TileFilter { + + /** + * Tests if the tile specified by the parameters should be included or excluded + * + * @param x + * @param y + * @param zoom + * @param mapSource + * @return
    + *
  • true: tile did pass the filter and should be included
  • + *
  • false:tile did not pass the filter and should be excluded
  • + *
+ */ + public boolean testTile(int x, int y, int zoom, MapSource mapSource); +} diff --git a/src/main/java/mobac/program/interfaces/TileImageDataWriter.java b/src/main/java/mobac/program/interfaces/TileImageDataWriter.java new file mode 100644 index 0000000..d3abc1a --- /dev/null +++ b/src/main/java/mobac/program/interfaces/TileImageDataWriter.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import mobac.program.model.TileImageType; + +public interface TileImageDataWriter { + + public void initialize(); + /** + * Processes the image according to the implementation of this + * interfaces and saves the image data in a binary representation such as + * PNG, JPG, ...into the given OutputStream. + * + * @param image + * @param out + * {@link OutputStream} to write binary image data to (usually + * this is a {@link FileOutputStream} or a + * {@link ByteArrayOutputStream} + * @throws IOException + */ + public void processImage(BufferedImage image, OutputStream out) throws IOException; + + public void dispose(); + + public TileImageType getType(); + +} diff --git a/src/main/java/mobac/program/interfaces/ToolTipProvider.java b/src/main/java/mobac/program/interfaces/ToolTipProvider.java new file mode 100644 index 0000000..d2aea16 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/ToolTipProvider.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +public interface ToolTipProvider { + + public String getToolTip(); +} diff --git a/src/main/java/mobac/program/interfaces/WrappedMapSource.java b/src/main/java/mobac/program/interfaces/WrappedMapSource.java new file mode 100644 index 0000000..f29ab98 --- /dev/null +++ b/src/main/java/mobac/program/interfaces/WrappedMapSource.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.interfaces; + +import mobac.exceptions.MapSourceCreateException; + +public interface WrappedMapSource { + + public MapSource getMapSource() throws MapSourceCreateException; +} diff --git a/src/main/java/mobac/program/jaxb/AtlasOutputFormatAdapter.java b/src/main/java/mobac/program/jaxb/AtlasOutputFormatAdapter.java new file mode 100644 index 0000000..9ddf643 --- /dev/null +++ b/src/main/java/mobac/program/jaxb/AtlasOutputFormatAdapter.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import mobac.program.model.AtlasOutputFormat; + +public class AtlasOutputFormatAdapter extends XmlAdapter { + + @Override + public String marshal(AtlasOutputFormat v) throws Exception { + return v.getTypeName(); + } + + @Override + public AtlasOutputFormat unmarshal(String v) throws Exception { + return AtlasOutputFormat.getFormatByName(v); + } + + +} diff --git a/src/main/java/mobac/program/jaxb/BigDecimalAdapter.java b/src/main/java/mobac/program/jaxb/BigDecimalAdapter.java new file mode 100644 index 0000000..9cfd64d --- /dev/null +++ b/src/main/java/mobac/program/jaxb/BigDecimalAdapter.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Locale; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +public class BigDecimalAdapter extends XmlAdapter { + + NumberFormat df = new DecimalFormat("0.00000",DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + + @Override + public BigDecimal unmarshal(String v) throws Exception { + return new BigDecimal(v); + } + + @Override + public String marshal(BigDecimal v) throws Exception { + return df.format(v); + } +} diff --git a/src/main/java/mobac/program/jaxb/ColorAdapter.java b/src/main/java/mobac/program/jaxb/ColorAdapter.java new file mode 100644 index 0000000..2625d14 --- /dev/null +++ b/src/main/java/mobac/program/jaxb/ColorAdapter.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.awt.Color; + +import javax.xml.bind.UnmarshalException; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +public class ColorAdapter extends XmlAdapter { + + @Override + public String marshal(Color color) throws Exception { + if (color.getAlpha() == 255) + return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); + else + return String.format("#%02x%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue(), + color.getAlpha()); + } + + @Override + public Color unmarshal(String value) throws Exception { + return parseColor(value); + } + + public static Color parseColor(String value) throws UnmarshalException { + value = value.trim(); + int length = value.length(); + if (!value.startsWith("#")) + throw new UnmarshalException("Invalid format: does not start with #"); + if (length != 7 && length != 9) + throw new UnmarshalException("Invalid format: wrong length"); + int r = Integer.parseInt(value.substring(1, 3), 16); + int g = Integer.parseInt(value.substring(3, 5), 16); + int b = Integer.parseInt(value.substring(5, 7), 16); + if (length == 7) + return new Color(r, g, b); + int a = Integer.parseInt(value.substring(7, 9), 16); + return new Color(r, g, b, a); + } +} diff --git a/src/main/java/mobac/program/jaxb/DimensionAdapter.java b/src/main/java/mobac/program/jaxb/DimensionAdapter.java new file mode 100644 index 0000000..df3ce9b --- /dev/null +++ b/src/main/java/mobac/program/jaxb/DimensionAdapter.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.awt.Dimension; + +import javax.xml.bind.UnmarshalException; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +/** + * Required {@link XmlAdapter} implementation for serializing a + * {@link Dimension} as the default one creates a {@link StackOverflowError} + */ +public class DimensionAdapter extends XmlAdapter { + + @Override + public String marshal(Dimension dimension) throws Exception { + return dimension.width + "/" + dimension.height; + } + + @Override + public Dimension unmarshal(String value) throws Exception { + int i = value.indexOf('/'); + if (i < 0) + throw new UnmarshalException("Invalid format"); + int width = Integer.parseInt(value.substring(0, i)); + int height = Integer.parseInt(value.substring(i + 1)); + return new Dimension(width, height); + } +} diff --git a/src/main/java/mobac/program/jaxb/FontAdapter.java b/src/main/java/mobac/program/jaxb/FontAdapter.java new file mode 100644 index 0000000..a1f5f03 --- /dev/null +++ b/src/main/java/mobac/program/jaxb/FontAdapter.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.awt.Font; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import mobac.gui.dialogs.FontChooser; + +/** + * Required {@link XmlAdapter} implementation for serializing {@link Font} + */ +public class FontAdapter extends XmlAdapter { + + @Override + public Font unmarshal(String value) throws Exception { + return Font.decode(value); + } + + @Override + public String marshal(Font font) throws Exception { + return FontChooser.encodeFont(font); + } +} diff --git a/src/main/java/mobac/program/jaxb/MapSourceAdapter.java b/src/main/java/mobac/program/jaxb/MapSourceAdapter.java new file mode 100644 index 0000000..e3437e2 --- /dev/null +++ b/src/main/java/mobac/program/jaxb/MapSourceAdapter.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; + + +public class MapSourceAdapter extends XmlAdapter { + + @Override + public String marshal(MapSource mapSource) throws Exception { + return mapSource.getName(); + } + + @Override + public MapSource unmarshal(String name) throws Exception { + return MapSourcesManager.getInstance().getSourceByName(name); + } + +} diff --git a/src/main/java/mobac/program/jaxb/PaperSizeAdapter.java b/src/main/java/mobac/program/jaxb/PaperSizeAdapter.java new file mode 100644 index 0000000..990abaf --- /dev/null +++ b/src/main/java/mobac/program/jaxb/PaperSizeAdapter.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import mobac.program.model.PaperSize; +import mobac.program.model.PaperSize.Format; + +/** + * Required {@link XmlAdapter} implementation for serializing {@link PaperSize} + */ +public class PaperSizeAdapter extends XmlAdapter { + + private static final String LANDSCAPE = "_LANDSCAPE", SELECTION = "SELECTION", X = "x"; + + @Override + public PaperSize unmarshal(String value) throws Exception { + if (value.equals(SELECTION)) { + return null; + } + if (value.contains(X)) { + String split[] = value.split(X); + double width = Double.parseDouble(split[0]); + double height = Double.parseDouble(split[1]); + return new PaperSize(width, height); + } + boolean landscape = false; + if (value.contains(LANDSCAPE)) { + value = value.substring(0, value.indexOf(LANDSCAPE)); + landscape = true; + } + Format format = Format.valueOf(value); + return new PaperSize(format, landscape); + } + + @Override + public String marshal(PaperSize v) throws Exception { + if (v == null) { + return SELECTION; + } + if (v.format != null) { + return v.format.name() + (v.landscape ? LANDSCAPE : ""); + } else { + return v.width + X + v.height; + } + } +} diff --git a/src/main/java/mobac/program/jaxb/PointAdapter.java b/src/main/java/mobac/program/jaxb/PointAdapter.java new file mode 100644 index 0000000..33a402c --- /dev/null +++ b/src/main/java/mobac/program/jaxb/PointAdapter.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.awt.Point; + +import javax.xml.bind.UnmarshalException; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +/** + * Required {@link XmlAdapter} implementation for serializing a {@link Point} as + * the default one creates a {@link StackOverflowError} + * + */ +public class PointAdapter extends XmlAdapter { + + @Override + public String marshal(Point point) throws Exception { + return point.x + "/" + point.y; + } + + @Override + public Point unmarshal(String value) throws Exception { + int i = value.indexOf('/'); + if (i < 0) + throw new UnmarshalException("Invalid format"); + int x = Integer.parseInt(value.substring(0, i).trim()); + int y = Integer.parseInt(value.substring(i + 1).trim()); + return new Point(x, y); + } +} diff --git a/src/main/java/mobac/program/jaxb/PolygonAdapter.java b/src/main/java/mobac/program/jaxb/PolygonAdapter.java new file mode 100644 index 0000000..0105f2a --- /dev/null +++ b/src/main/java/mobac/program/jaxb/PolygonAdapter.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.awt.Point; +import java.awt.Polygon; +import java.util.Vector; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +/** + * Required {@link XmlAdapter} implementation for serializing a {@link Polygon} + */ +public class PolygonAdapter extends XmlAdapter { + + @Override + public PolygonType marshal(Polygon polygon) throws Exception { + Vector points = new Vector(polygon.npoints); + for (int i = 0; i < polygon.npoints; i++) { + Point p = new Point(polygon.xpoints[i], polygon.ypoints[i]); + points.add(p); + } + return new PolygonType(points); + } + + @Override + public Polygon unmarshal(PolygonType value) throws Exception { + int npoints = value.points.size(); + int[] xpoints = new int[npoints]; + int[] ypoints = new int[npoints]; + for (int i = 0; i < npoints; i++) { + Point p = value.points.get(i); + xpoints[i] = p.x; + ypoints[i] = p.y; + } + + return new Polygon(xpoints, ypoints, npoints); + } + +} diff --git a/src/main/java/mobac/program/jaxb/PolygonType.java b/src/main/java/mobac/program/jaxb/PolygonType.java new file mode 100644 index 0000000..d6fc114 --- /dev/null +++ b/src/main/java/mobac/program/jaxb/PolygonType.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import java.awt.Point; +import java.util.Vector; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +public class PolygonType { + + @XmlElement(name = "point") + @XmlJavaTypeAdapter(PointAdapter.class) + public Vector points; + + protected PolygonType() { + points = new Vector(20); + } + + public PolygonType(Vector points) { + this.points = points; + } + +} diff --git a/src/main/java/mobac/program/jaxb/TileImageTypeAdapter.java b/src/main/java/mobac/program/jaxb/TileImageTypeAdapter.java new file mode 100644 index 0000000..a307600 --- /dev/null +++ b/src/main/java/mobac/program/jaxb/TileImageTypeAdapter.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.jaxb; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import mobac.program.model.TileImageType; + +public class TileImageTypeAdapter extends XmlAdapter { + + @Override + public TileImageType unmarshal(String v) throws Exception { + return TileImageType.getTileImageType(v); + } + + @Override + public String marshal(TileImageType v) throws Exception { + return v.name(); + } + +} diff --git a/src/main/java/mobac/program/model/AnyAttributeMap.java b/src/main/java/mobac/program/model/AnyAttributeMap.java new file mode 100644 index 0000000..92172bb --- /dev/null +++ b/src/main/java/mobac/program/model/AnyAttributeMap.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.util.Comparator; +import java.util.TreeMap; + +import javax.xml.bind.annotation.XmlAnyAttribute; +import javax.xml.namespace.QName; + +/** + * A map implementation for catching all attributes via {@link XmlAnyAttribute} + */ +public class AnyAttributeMap extends TreeMap { + + public AnyAttributeMap() { + super(new QNameComparator()); + } + + public static class QNameComparator implements Comparator { + + public int compare(QName o1, QName o2) { + return o1.getLocalPart().compareTo(o2.getLocalPart()); + } + } + + public String getAttr(String key) { + return (String) get(new QName(key)); + } + + public void setAttr(String key, String value) { + put(new QName(key), value); + } + + public int getInt(TileImageParameters.Name key) { + return Integer.parseInt(getAttr(key.name())); + } + + public int getInt(String key) { + return Integer.parseInt(getAttr(key)); + } + + public void setInt(String key, int value) { + put(new QName(key), Integer.toString(value)); + } +} diff --git a/src/main/java/mobac/program/model/Atlas.java b/src/main/java/mobac/program/model/Atlas.java new file mode 100644 index 0000000..da24336 --- /dev/null +++ b/src/main/java/mobac/program/model/Atlas.java @@ -0,0 +1,221 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.io.StringWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.tree.TreeNode; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElements; +import javax.xml.bind.annotation.XmlRootElement; + +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.ToolTipProvider; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.commons.lang3.StringEscapeUtils; + +@XmlRootElement +public class Atlas implements AtlasInterface, ToolTipProvider, TreeNode { + + public static final int CURRENT_ATLAS_VERSION = 1; + + @XmlAttribute + private int version = 0; + + private String name = I18nUtils.localizedStringForKey("Unnamed"); + + @XmlElements({ @XmlElement(name = "Layer", type = Layer.class) }) + private List layers = new LinkedList(); + + private AtlasOutputFormat outputFormat = AtlasOutputFormat.FORMATS.get(0); + + public static Atlas newInstance() { + Atlas atlas = new Atlas(); + atlas.version = CURRENT_ATLAS_VERSION; + return atlas; + } + + private Atlas() { + super(); + } + + public void addLayer(LayerInterface l) { + layers.add(l); + } + + public void deleteLayer(LayerInterface l) { + layers.remove(l); + } + + public LayerInterface getLayer(int index) { + return layers.get(index); + } + + public int getLayerCount() { + return layers.size(); + } + + @XmlAttribute + public String getName() { + return name; + } + + public void setName(String newName) { + this.name = newName; + } + + @XmlAttribute + public AtlasOutputFormat getOutputFormat() { + return outputFormat; + } + + public void setOutputFormat(AtlasOutputFormat atlasOutputFormat) { + if (atlasOutputFormat == null) + throw new NullPointerException(); + this.outputFormat = atlasOutputFormat; + } + + @Override + public String toString() { + return getName() + " (" + outputFormat + ")"; + } + + public Iterator iterator() { + return layers.iterator(); + } + + public long calculateTilesToDownload() { + long tiles = 0; + for (LayerInterface layer : layers) + tiles += layer.calculateTilesToDownload(); + return tiles; + } + + public boolean checkData() { + if (name == null) // name set? + return true; + // Check for duplicate layer names + HashSet names = new HashSet(layers.size()); + for (LayerInterface layer : layers) + names.add(layer.getName()); + if (names.size() < layers.size()) + return true; // at least one duplicate name found + return false; + } + + public double getMinLat() { + double lat = 90d; + for (LayerInterface l : layers) { + lat = Math.min(lat, l.getMinLat()); + } + return lat; + } + + public double getMaxLat() { + double lat = -90d; + for (LayerInterface l : layers) { + lat = Math.max(lat, l.getMaxLat()); + } + return lat; + } + + public double getMinLon() { + double lon = 180d; + for (LayerInterface l : layers) { + lon = Math.min(lon, l.getMinLon()); + } + return lon; + } + + public double getMaxLon() { + double lon = -180d; + for (LayerInterface l : layers) { + lon = Math.max(lon, l.getMaxLon()); + } + return lon; + } + + public String getToolTip() { + StringWriter sw = new StringWriter(1024); + sw.write(""); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_atlas_title")); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_atlas_name", StringEscapeUtils.escapeHtml4(name))); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_atlas_layer", layers.size())); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_atlas_format", outputFormat.toString())); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_max_tile", calculateTilesToDownload())); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_area_start", + Utilities.prettyPrintLatLon(getMaxLat(), true), Utilities.prettyPrintLatLon(getMinLon(), false))); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_area_end", + Utilities.prettyPrintLatLon(getMinLat(), true), Utilities.prettyPrintLatLon(getMaxLon(), false))); + sw.write(""); + return sw.toString(); + } + + public Enumeration children() { + return Collections.enumeration(layers); + } + + public boolean getAllowsChildren() { + return true; + } + + public TreeNode getChildAt(int childIndex) { + return (TreeNode) layers.get(childIndex); + } + + public int getChildCount() { + return layers.size(); + } + + public int getIndex(TreeNode node) { + return layers.indexOf(node); + } + + public TreeNode getParent() { + return null; + } + + public boolean isLeaf() { + return false; + } + + public int getVersion() { + return version; + } + + public AtlasInterface deepClone() { + Atlas atlas = new Atlas(); + atlas.version = version; + atlas.name = name; + atlas.outputFormat = outputFormat; + for (LayerInterface layer : layers) { + atlas.layers.add(layer.deepClone(atlas)); + } + return atlas; + } + +} diff --git a/src/main/java/mobac/program/model/AtlasOutputFormat.java b/src/main/java/mobac/program/model/AtlasOutputFormat.java new file mode 100644 index 0000000..0df1834 --- /dev/null +++ b/src/main/java/mobac/program/model/AtlasOutputFormat.java @@ -0,0 +1,198 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Vector; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.program.annotations.AtlasCreatorName; +import mobac.program.atlascreators.AFTrack; +import mobac.program.atlascreators.AlpineQuestMap; +import mobac.program.atlascreators.AndNav; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.atlascreators.BackCountryNavigator; +import mobac.program.atlascreators.BigPlanetTracks; +import mobac.program.atlascreators.CacheBox; +import mobac.program.atlascreators.CacheWolf; +import mobac.program.atlascreators.GCLive; +import mobac.program.atlascreators.Galileo; +import mobac.program.atlascreators.GarminCustom; +import mobac.program.atlascreators.Glopus; +import mobac.program.atlascreators.GlopusMapFile; +import mobac.program.atlascreators.GoogleEarthOverlay; +import mobac.program.atlascreators.GpsSportsTracker; +import mobac.program.atlascreators.IPhone3MapTiles5; +import mobac.program.atlascreators.MBTiles; +import mobac.program.atlascreators.MGMaps; +import mobac.program.atlascreators.MagellanRmp; +import mobac.program.atlascreators.Maplorer; +import mobac.program.atlascreators.Maverick; +import mobac.program.atlascreators.MobileTrailExplorer; +import mobac.program.atlascreators.MobileTrailExplorerCache; +import mobac.program.atlascreators.NFComPass; +import mobac.program.atlascreators.NaviComputer; +import mobac.program.atlascreators.OSMAND; +import mobac.program.atlascreators.OSMAND_SQlite; +import mobac.program.atlascreators.OSMTracker; +import mobac.program.atlascreators.OruxMaps; +import mobac.program.atlascreators.OruxMapsSqlite; +import mobac.program.atlascreators.Osmdroid; +import mobac.program.atlascreators.OsmdroidGEMF; +import mobac.program.atlascreators.OsmdroidSQLite; +import mobac.program.atlascreators.Ozi; +import mobac.program.atlascreators.PNGWorldfile; +import mobac.program.atlascreators.PaperAtlasPdf; +import mobac.program.atlascreators.PaperAtlasPng; +import mobac.program.atlascreators.PathAway; +import mobac.program.atlascreators.RMapsSQLite; +import mobac.program.atlascreators.RunGPSAtlas; +import mobac.program.atlascreators.SportsTracker; +import mobac.program.atlascreators.TTQV; +import mobac.program.atlascreators.TileStoreDownload; +import mobac.program.atlascreators.TomTomRaster; +import mobac.program.atlascreators.TrekBuddy; +import mobac.program.atlascreators.TrekBuddyTared; +import mobac.program.atlascreators.TwoNavRMAP; +import mobac.program.atlascreators.Ublox; +import mobac.program.atlascreators.Viewranger; +import mobac.program.jaxb.AtlasOutputFormatAdapter; + +@XmlRootElement +@XmlJavaTypeAdapter(AtlasOutputFormatAdapter.class) +public class AtlasOutputFormat implements Comparable { + + public static List FORMATS; + + public static final AtlasOutputFormat TILESTORE = createByClass(TileStoreDownload.class); + + static { + FORMATS = new ArrayList(40); + FORMATS.add(createByClass(AFTrack.class)); + FORMATS.add(createByClass(AlpineQuestMap.class)); + FORMATS.add(createByClass(AndNav.class)); + FORMATS.add(createByClass(BackCountryNavigator.class)); + FORMATS.add(createByClass(BigPlanetTracks.class)); + FORMATS.add(createByClass(CacheBox.class)); + FORMATS.add(createByClass(CacheWolf.class)); + FORMATS.add(createByClass(Galileo.class)); + FORMATS.add(createByClass(GarminCustom.class)); + FORMATS.add(createByClass(GCLive.class)); + FORMATS.add(createByClass(Glopus.class)); + FORMATS.add(createByClass(GlopusMapFile.class)); + FORMATS.add(createByClass(GoogleEarthOverlay.class)); + FORMATS.add(createByClass(GpsSportsTracker.class)); + FORMATS.add(createByClass(IPhone3MapTiles5.class)); + FORMATS.add(createByClass(MagellanRmp.class)); + FORMATS.add(createByClass(Maplorer.class)); + FORMATS.add(createByClass(Maverick.class)); + FORMATS.add(createByClass(MBTiles.class)); + FORMATS.add(createByClass(MGMaps.class)); + FORMATS.add(createByClass(MobileTrailExplorer.class)); + FORMATS.add(createByClass(MobileTrailExplorerCache.class)); + FORMATS.add(createByClass(NaviComputer.class)); + FORMATS.add(createByClass(NFComPass.class)); + FORMATS.add(createByClass(OruxMaps.class)); + FORMATS.add(createByClass(OruxMapsSqlite.class)); + FORMATS.add(createByClass(OSMAND.class)); + FORMATS.add(createByClass(OSMAND_SQlite.class)); + FORMATS.add(createByClass(Osmdroid.class)); + FORMATS.add(createByClass(OsmdroidGEMF.class)); + FORMATS.add(createByClass(OsmdroidSQLite.class)); + FORMATS.add(createByClass(OSMTracker.class)); + FORMATS.add(createByClass(Ozi.class)); + FORMATS.add(createByClass(PaperAtlasPdf.class)); + FORMATS.add(createByClass(PaperAtlasPng.class)); + FORMATS.add(createByClass(PathAway.class)); + FORMATS.add(createByClass(PNGWorldfile.class)); + FORMATS.add(createByClass(RMapsSQLite.class)); + FORMATS.add(createByClass(RunGPSAtlas.class)); + FORMATS.add(createByClass(SportsTracker.class)); + FORMATS.add(createByClass(TomTomRaster.class)); + FORMATS.add(createByClass(TTQV.class)); + FORMATS.add(createByClass(TrekBuddyTared.class)); + FORMATS.add(createByClass(TrekBuddy.class)); + FORMATS.add(createByClass(TwoNavRMAP.class)); + FORMATS.add(createByClass(Ublox.class)); + FORMATS.add(createByClass(Viewranger.class)); + FORMATS.add(TILESTORE); + } + + public static Vector getFormatsAsVector() { + return new Vector(FORMATS); + } + + public static AtlasOutputFormat getFormatByName(String Name) { + for (AtlasOutputFormat af : FORMATS) { + if (af.getTypeName().equals(Name)) + return af; + } + throw new NoSuchElementException("Unknown atlas format: \"" + Name + "\""); + } + + private Class atlasCreatorClass; + private String typeName; + private String name; + + private static AtlasOutputFormat createByClass(Class atlasCreatorClass) { + AtlasCreatorName acName = atlasCreatorClass.getAnnotation(AtlasCreatorName.class); + if (acName == null) + throw new RuntimeException("AtlasCreator " + atlasCreatorClass.getName() + " has no name"); + String typeName = acName.type(); + if (typeName == null || typeName.length() == 0) + typeName = atlasCreatorClass.getSimpleName(); + String name = acName.value(); + return new AtlasOutputFormat(atlasCreatorClass, typeName, name); + } + + private AtlasOutputFormat(Class atlasCreatorClass, String typeName, String name) { + this.atlasCreatorClass = atlasCreatorClass; + this.typeName = typeName; + this.name = name; + } + + public String toString() { + return name; + } + + public Class getMapCreatorClass() { + return atlasCreatorClass; + } + + public String getTypeName() { + return typeName; + } + + public AtlasCreator createAtlasCreatorInstance() { + if (atlasCreatorClass == null) + return null; + try { + return atlasCreatorClass.newInstance(); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + public int compareTo(AtlasOutputFormat o) { + return getTypeName().compareTo(o.toString()); + } + +} diff --git a/src/main/java/mobac/program/model/AtlasTreeModel.java b/src/main/java/mobac/program/model/AtlasTreeModel.java new file mode 100644 index 0000000..d3627a4 --- /dev/null +++ b/src/main/java/mobac/program/model/AtlasTreeModel.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Toolkit; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +import javax.swing.JOptionPane; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import mobac.exceptions.InvalidNameException; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.AtlasObject; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; + +import org.apache.log4j.Logger; + + +public class AtlasTreeModel implements TreeModel { + + private static Logger log = Logger.getLogger(AtlasTreeModel.class); + + protected AtlasInterface atlasInterface; + + protected Set listeners = new HashSet(); + + public AtlasTreeModel() { + super(); + atlasInterface = Atlas.newInstance(); + } + + public void addTreeModelListener(TreeModelListener l) { + listeners.add(l); + } + + public void removeTreeModelListener(TreeModelListener l) { + listeners.remove(l); + } + + public void notifyStructureChanged() { + notifyStructureChanged((TreeNode) atlasInterface); + } + + public void notifyStructureChanged(TreeNode root) { + notifyStructureChanged(new TreeModelEvent(this, new Object[] { root })); + } + + /** + * IMPORTANT: This method have to be called BEFORE deleting the element in + * the data model!!! Otherwise the child index can not be retrieved anymore + * which is important. + * + * @param node + */ + public void notifyNodeDelete(TreeNode node) { + TreeNode parent = node.getParent(); + Object[] children = new Object[] { node }; + int childrenIdx = parent.getIndex(node); + if (childrenIdx == -1) { + // A problem detected - use fall back solution + notifyStructureChanged(); + return; + } + TreePath path = getNodePath(parent); + + TreeModelEvent event = new TreeModelEvent(this, path, new int[] { childrenIdx }, children); + for (TreeModelListener l : listeners) + l.treeNodesRemoved(event); + } + + protected void notifyStructureChanged(TreeModelEvent event) { + for (TreeModelListener l : listeners) + l.treeStructureChanged(event); + } + + public void notifyNodeInsert(TreeNode insertedNode) { + TreeNode parent = insertedNode.getParent(); + TreePath path = getNodePath(parent); + TreeNode[] childs = new TreeNode[] { insertedNode }; + int childId = parent.getIndex(insertedNode); + assert (childId <= 0); + TreeModelEvent event = new TreeModelEvent(this, path, new int[] { childId }, childs); + for (TreeModelListener l : listeners) + l.treeNodesInserted(event); + } + + public TreePath getNodePath(TreeNode node) { + LinkedList path = new LinkedList(); + TreeNode n = node; + while (n != null) { + path.addFirst(n); + n = n.getParent(); + } + return new TreePath(path.toArray()); + } + + public Object getChild(Object parent, int index) { + return ((TreeNode) parent).getChildAt(index); + } + + public int getChildCount(Object parent) { + return ((TreeNode) parent).getChildCount(); + } + + public int getIndexOfChild(Object parent, Object child) { + return ((TreeNode) parent).getIndex((TreeNode) child); + } + + public Object getRoot() { + return atlasInterface; + } + + public boolean isLeaf(Object node) { + return ((TreeNode) node).isLeaf(); + } + + public void valueForPathChanged(TreePath path, Object newValue) { + Object o = path.getLastPathComponent(); + boolean success = false; + try { + AtlasObject sel = (AtlasObject) o; + String newName = (String) newValue; + if (newName.length() == 0) + return; + sel.setName(newName); + success = true; + } catch (ClassCastException e) { + log.error("", e); + } catch (InvalidNameException e) { + log.error(e.getLocalizedMessage()); + JOptionPane.showMessageDialog(null, e.getLocalizedMessage(), "Renaming failed", + JOptionPane.ERROR_MESSAGE); + } finally { + if (!success) { + Toolkit.getDefaultToolkit().beep(); + } + } + } + + public void mergeLayers(LayerInterface source, LayerInterface target) + throws InvalidNameException { + + boolean sourceFound = false; + boolean targetFound = false; + for (LayerInterface l : atlasInterface) { + if (l.equals(source)) + sourceFound = true; + if (l.equals(target)) + targetFound = true; + } + if (!targetFound) + return; + // Check for duplicate names + HashSet names = new HashSet(); + for (MapInterface map : source) + names.add(map.getName()); + for (MapInterface map : target) + names.add(map.getName()); + if (names.size() < (source.getMapCount() + target.getMapCount())) + throw new InvalidNameException("Map naming conflict:\n" + + "The layers to be merged contain map(s) of the same name."); + + if (sourceFound) + atlasInterface.deleteLayer(source); + for (MapInterface map : source) { + target.addMap(map); + } + notifyNodeDelete((TreeNode) source); + notifyStructureChanged((TreeNode) target); + } + + public void moveMap(MapInterface map, LayerInterface targetLayer) { + notifyNodeDelete((TreeNode) map); + map.delete(); + targetLayer.addMap(map); + notifyNodeInsert((TreeNode) map); + } + + public AtlasInterface getAtlas() { + return atlasInterface; + } + + public void setAtlas(Atlas atlas) { + this.atlasInterface = atlas; + notifyStructureChanged(); + } + + public void save(Profile profile) throws Exception { + profile.save(atlasInterface); + } + + public void load(Profile profile) throws Exception { + atlasInterface = profile.load(); + notifyStructureChanged(); + } + +} diff --git a/src/main/java/mobac/program/model/Bookmark.java b/src/main/java/mobac/program/model/Bookmark.java new file mode 100644 index 0000000..d4c5271 --- /dev/null +++ b/src/main/java/mobac/program/model/Bookmark.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.interfaces.MapSource; + +@XmlAccessorType(XmlAccessType.FIELD) +public class Bookmark extends EastNorthCoordinate { + + @XmlAttribute + protected int zoom; + + @XmlAttribute + protected String name; + + @XmlAttribute + protected String mapSource; + + /** + * Needed for JAXB + */ + @SuppressWarnings("unused") + private Bookmark() { + + } + + public Bookmark(MapSource mapSource, int zoom, int pixelCoordinateX, int pixelCoordinateY) { + this(null, mapSource, zoom, pixelCoordinateX, pixelCoordinateY); + } + + public Bookmark(String name, MapSource mapSource, int zoom, int pixelCoordinateX, int pixelCoordinateY) { + super(mapSource.getMapSpace(), zoom, pixelCoordinateX, pixelCoordinateY); + this.mapSource = mapSource.getName(); + this.zoom = zoom; + this.name = name; + } + + public MapSource getMapSource() { + return MapSourcesManager.getInstance().getSourceByName(mapSource); + } + + public int getZoom() { + return zoom; + } + + public void setName(String name) { + if (name != null && name.trim().length() == 0) + name = null; + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + if (name != null) + return name; + return String.format("%s at lat=%.3f lon=%.3f (zoom = %d)", mapSource, lat, lon, zoom); + } + +} diff --git a/src/main/java/mobac/program/model/Coordinate.java b/src/main/java/mobac/program/model/Coordinate.java new file mode 100644 index 0000000..16ec671 --- /dev/null +++ b/src/main/java/mobac/program/model/Coordinate.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +public class Coordinate { + + public static final int MILISECOND = 1, SECOND = MILISECOND * 1000, MINUTE = SECOND * 60, DEGREE = MINUTE * 60; + + public static int doubleToInt(double value) { + int degree = (int) value; + int minute = (int) (value = (value - degree) * 60d); + int second = (int) (value = (value - minute) * 60d); + int milisecond = (int) (value = (value - second) * 1000d); + return degree * DEGREE + minute * MINUTE + second * SECOND + milisecond * MILISECOND; + } + + public static double intToDouble(int value) { + double degree = value / DEGREE; + double minute = (value = value % DEGREE) / MINUTE; + double second = (int) (value %= MINUTE) / SECOND; + double milisecond = (int) (value %= SECOND) / MILISECOND; + return degree + minute / 60d + second / 3600d + milisecond / 3600000d; + } + + public static int getDegree(int value) { + return value / DEGREE; + } + + public static int getMinute(int value) { + return Math.abs(value) % DEGREE / MINUTE; + } + + public static int getSecond(int value) { + return Math.abs(value) % MINUTE / SECOND; + } + + public static int getMilisecond(int value) { + return Math.abs(value) % SECOND / MILISECOND; + } +} diff --git a/src/main/java/mobac/program/model/CoordinateStringFormat.java b/src/main/java/mobac/program/model/CoordinateStringFormat.java new file mode 100644 index 0000000..06ad505 --- /dev/null +++ b/src/main/java/mobac/program/model/CoordinateStringFormat.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.text.NumberFormat; + +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.geo.CoordinateDm2Format; +import mobac.utilities.geo.CoordinateDms2Format; +import mobac.utilities.geo.CoordinateTileFormat; + +public enum CoordinateStringFormat { + + DEG_ENG(Utilities.FORMAT_6_DEC_ENG), // + DEG_LOCAL(Utilities.FORMAT_6_DEC), // + DEG_MIN_ENG(new CoordinateDm2Format(Utilities.DFS_ENG)), // + DEG_MIN_LOCAL(new CoordinateDm2Format(Utilities.DFS_LOCAL)), // + DEG_MIN_SEC_ENG(new CoordinateDms2Format(Utilities.DFS_ENG)), // + DEG_MIN_SEC_LOCAL(new CoordinateDms2Format(Utilities.DFS_LOCAL)), // + TILE_X_Y_Z(new CoordinateTileFormat(false), new CoordinateTileFormat(true)); + + /* + * formatButton.addDropDownItem(new JNumberFormatMenuItem()); formatButton.addDropDownItem(new + * JNumberFormatMenuItem("Deg Min Sec,2 (local)", + */ + //private final String displayName; + private NumberFormat numberFormatLatitude; + private NumberFormat numberFormatLongitude; + + private CoordinateStringFormat(NumberFormat numberFormat) { + //this.displayName = displayName; + this.numberFormatLatitude = numberFormat; + this.numberFormatLongitude = numberFormat; + } + + private CoordinateStringFormat(NumberFormat numberFormatLatitude, + NumberFormat numberFormatLongitude) { + //this.displayName = displayName; + this.numberFormatLatitude = numberFormatLatitude; + this.numberFormatLongitude = numberFormatLongitude; + } + +// public String getDisplayName() { +// return this.toString(); +// } + + public NumberFormat getNumberFormatLatitude() { + return numberFormatLatitude; + } + + public NumberFormat getNumberFormatLongitude() { + return numberFormatLongitude; + } + + @Override + public String toString() { + //return displayName; + switch(this) + { + case DEG_ENG:return I18nUtils.localizedStringForKey("lp_coords_fmt_degree_eng"); + case DEG_LOCAL:return I18nUtils.localizedStringForKey("lp_coords_fmt_degree_local"); + case DEG_MIN_ENG:return I18nUtils.localizedStringForKey("lp_coords_fmt_degree_min_eng"); + case DEG_MIN_LOCAL:return I18nUtils.localizedStringForKey("lp_coords_fmt_degree_min_local"); + case DEG_MIN_SEC_ENG:return I18nUtils.localizedStringForKey("lp_coords_fmt_degree_min_sec_eng"); + case DEG_MIN_SEC_LOCAL:return I18nUtils.localizedStringForKey("lp_coords_fmt_degree_min_sec_local"); + case TILE_X_Y_Z:return I18nUtils.localizedStringForKey("lp_coords_fmt_tile"); + } + return I18nUtils.localizedStringForKey("Undefined"); + } + +} diff --git a/src/main/java/mobac/program/model/EastNorthCoordinate.java b/src/main/java/mobac/program/model/EastNorthCoordinate.java new file mode 100644 index 0000000..839a258 --- /dev/null +++ b/src/main/java/mobac/program/model/EastNorthCoordinate.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Point; +import java.awt.geom.Point2D; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +import mobac.program.interfaces.MapSpace; +import mobac.utilities.Utilities; + + + +@XmlRootElement +public class EastNorthCoordinate { + + @XmlAttribute + public double lat; + @XmlAttribute + public double lon; + + public EastNorthCoordinate() { + lat = Double.NaN; + lon = Double.NaN; + } + + public EastNorthCoordinate(MapSpace mapSpace, int zoom, int pixelCoordinateX, + int pixelCoordinateY) { + this.lat = mapSpace.cYToLat(pixelCoordinateY, zoom); + this.lon = mapSpace.cXToLon(pixelCoordinateX, zoom); + } + + public EastNorthCoordinate(double lat, double lon) { + this.lat = lat; + this.lon = lon; + } + + public EastNorthCoordinate(Point2D.Double c) { + this.lat = c.y; + this.lon = c.x; + } + + public Point toTileCoordinate(MapSpace mapSpace, int zoom) { + int x = mapSpace.cLonToX(lon, zoom); + int y = mapSpace.cLatToY(lat, zoom); + return new Point(x, y); + } + + @Override + public String toString() { + return Utilities.prettyPrintLatLon(lat, true) + " " + + Utilities.prettyPrintLatLon(lon, false); + } + +} diff --git a/src/main/java/mobac/program/model/Layer.java b/src/main/java/mobac/program/model/Layer.java new file mode 100644 index 0000000..84597eb --- /dev/null +++ b/src/main/java/mobac/program/model/Layer.java @@ -0,0 +1,302 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Dimension; +import java.awt.Point; +import java.io.StringWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; + +import javax.swing.tree.TreeNode; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElements; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +import mobac.exceptions.InvalidNameException; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.CapabilityDeletable; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.ToolTipProvider; +import mobac.utilities.I18nUtils; +import mobac.utilities.MyMath; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +/** + * A layer holding one or multiple maps of the same map source and the same zoom level. The number of maps depends on + * the size of the covered area - if it is smaller than the specified maxMapSize then there will be only + * one map. + * + */ +@XmlRootElement +public class Layer implements LayerInterface, TreeNode, ToolTipProvider, CapabilityDeletable { + + private static Logger log = Logger.getLogger(Layer.class); + + @XmlTransient + private AtlasInterface atlasInterface; + + private String name; + + @XmlElements({ @XmlElement(name = "PolygonMap", type = MapPolygon.class), + @XmlElement(name = "Map", type = Map.class) }) + private LinkedList maps = new LinkedList(); + + protected Layer() { + } + + public Layer(AtlasInterface atlasInterface, String name) throws InvalidNameException { + this.atlasInterface = atlasInterface; + setName(name); + } + + public void addMapsAutocut(String mapNameBase, MapSource mapSource, EastNorthCoordinate minCoordinate, + EastNorthCoordinate maxCoordinate, int zoom, TileImageParameters parameters, int maxMapSize) + throws InvalidNameException { + MapSpace mapSpace = mapSource.getMapSpace(); + addMapsAutocut(mapNameBase, mapSource, minCoordinate.toTileCoordinate(mapSpace, zoom), + maxCoordinate.toTileCoordinate(mapSpace, zoom), zoom, parameters, maxMapSize, 0); + } + + public void addMapsAutocut(String mapNameBase, MapSource mapSource, Point minTileCoordinate, + Point maxTileCoordinate, int zoom, TileImageParameters parameters, int maxMapSize, int overlapTiles) + throws InvalidNameException { + log.trace("Adding new map(s): \"" + mapNameBase + "\" " + mapSource + " zoom=" + zoom + " min=" + + minTileCoordinate.x + "/" + minTileCoordinate.y + " max=" + maxTileCoordinate.x + "/" + + maxTileCoordinate.y); + + int tileSize = mapSource.getMapSpace().getTileSize(); + + minTileCoordinate.x -= minTileCoordinate.x % tileSize; + minTileCoordinate.y -= minTileCoordinate.y % tileSize; + + maxTileCoordinate.x += tileSize - 1 - (maxTileCoordinate.x % tileSize); + maxTileCoordinate.y += tileSize - 1 - (maxTileCoordinate.y % tileSize); + + Dimension tileDimension; + if (parameters == null) + tileDimension = new Dimension(tileSize, tileSize); + else + tileDimension = parameters.getDimension(); + // We adapt the max map size to the tile size so that we do + // not get ugly cutted/incomplete tiles at the borders + Dimension maxMapDimension = new Dimension(maxMapSize, maxMapSize); + maxMapDimension.width -= maxMapSize % tileDimension.width; + maxMapDimension.height -= maxMapSize % tileDimension.height; + + int mapWidth = maxTileCoordinate.x - minTileCoordinate.x; + int mapHeight = maxTileCoordinate.y - minTileCoordinate.y; + if (mapWidth < maxMapDimension.width && mapHeight < maxMapDimension.height) { + Map s = new Map(this, mapNameBase, mapSource, zoom, minTileCoordinate, maxTileCoordinate, parameters); + maps.add(s); + return; + } + Dimension nextMapStep = new Dimension(maxMapDimension.width - (tileDimension.width * overlapTiles), + maxMapDimension.height - (tileDimension.height * overlapTiles)); + + int maxXCounter = MyMath.divCeil(mapWidth, nextMapStep.width); + int maxYCounter = MyMath.divCeil(mapHeight, nextMapStep.height); + + int maxMapCounter = maxXCounter * maxYCounter; + int maxMapCountDigits = (int) Math.ceil(Math.log10(maxMapCounter)); + String mapNameFormat = "%s (%0" + maxMapCountDigits + "d)"; + int mapCounter = 0; + for (int mapX = minTileCoordinate.x; mapX < maxTileCoordinate.x; mapX += nextMapStep.width) { + for (int mapY = minTileCoordinate.y; mapY < maxTileCoordinate.y; mapY += nextMapStep.height) { + int maxX = Math.min(mapX + maxMapDimension.width, maxTileCoordinate.x); + int maxY = Math.min(mapY + maxMapDimension.height, maxTileCoordinate.y); + Point min = new Point(mapX, mapY); + Point max = new Point(maxX - 1, maxY - 1); + String mapName = String.format(mapNameFormat, new Object[] { mapNameBase, mapCounter++ }); + Map s = new Map(this, mapName, mapSource, zoom, min, max, parameters); + maps.add(s); + } + } + } + + public void delete() { + maps.clear(); + atlasInterface.deleteLayer(this); + } + + public AtlasInterface getAtlas() { + return atlasInterface; + } + + public void addMap(MapInterface map) { + // TODO: Add name collision check + maps.add(map); + map.setLayer(this); + } + + public MapInterface getMap(int index) { + return maps.get(index); + } + + public int getMapCount() { + return maps.size(); + } + + @XmlAttribute + public String getName() { + return name; + } + + public void setName(String newName) throws InvalidNameException { + if (atlasInterface != null) { + for (LayerInterface layer : atlasInterface) { + if ((layer != this) && newName.equals(layer.getName())) + throw new InvalidNameException("There is already a layer named \"" + newName + + "\" in this atlas.\nLayer names have to unique within an atlas."); + } + } + this.name = newName; + } + + @Override + public String toString() { + return name; + } + + public long calculateTilesToDownload() { + long result = 0; + for (MapInterface map : maps) + result += map.calculateTilesToDownload(); + return result; + } + + public double getMinLat() { + double lat = 90d; + for (MapInterface m : maps) { + lat = Math.min(lat, m.getMinLat()); + } + return lat; + } + + public double getMaxLat() { + double lat = -90d; + for (MapInterface m : maps) { + lat = Math.max(lat, m.getMaxLat()); + } + return lat; + } + + public double getMinLon() { + double lon = 180d; + for (MapInterface m : maps) { + lon = Math.min(lon, m.getMinLon()); + } + return lon; + } + + public double getMaxLon() { + double lon = -180d; + for (MapInterface m : maps) { + lon = Math.max(lon, m.getMaxLon()); + } + return lon; + } + + public String getToolTip() { + StringWriter sw = new StringWriter(1024); + sw.write(""); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_layer_title")); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_layer_map_count", maps.size())); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_max_tile", calculateTilesToDownload())); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_area_start", + Utilities.prettyPrintLatLon(getMaxLat(), true), Utilities.prettyPrintLatLon(getMinLon(), false))); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_area_end", + Utilities.prettyPrintLatLon(getMinLat(), true), Utilities.prettyPrintLatLon(getMaxLon(), false))); + sw.write(""); + return sw.toString(); + } + + public Iterator iterator() { + return maps.iterator(); + } + + public Enumeration children() { + return Collections.enumeration(maps); + } + + public boolean getAllowsChildren() { + return true; + } + + public TreeNode getChildAt(int childIndex) { + return (TreeNode) maps.get(childIndex); + } + + public int getChildCount() { + return maps.size(); + } + + public int getIndex(TreeNode node) { + return maps.indexOf(node); + } + + public TreeNode getParent() { + return (TreeNode) atlasInterface; + } + + public boolean isLeaf() { + return false; + } + + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.atlasInterface = (Atlas) parent; + } + + public boolean checkData() { + if (atlasInterface == null) + return true; + if (name == null) + return true; + // Check for duplicate map names + HashSet names = new HashSet(maps.size()); + for (MapInterface map : maps) + names.add(map.getName()); + if (names.size() < maps.size()) + return true; // at least one duplicate name found + return false; + } + + public void deleteMap(Map map) { + maps.remove(map); + } + + public LayerInterface deepClone(AtlasInterface atlas) { + Layer layer = new Layer(); + layer.atlasInterface = atlas; + layer.name = name; + for (MapInterface map : maps) + layer.maps.add(map.deepClone(layer)); + return layer; + } + +} diff --git a/src/main/java/mobac/program/model/Map.java b/src/main/java/mobac/program/model/Map.java new file mode 100644 index 0000000..cca9f30 --- /dev/null +++ b/src/main/java/mobac/program/model/Map.java @@ -0,0 +1,329 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Dimension; +import java.awt.Point; +import java.io.StringWriter; +import java.util.Enumeration; + +import javax.swing.tree.TreeNode; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlTransient; + +import mobac.exceptions.InvalidNameException; +import mobac.program.JobDispatcher.Job; +import mobac.program.download.jobenumerators.DownloadJobEnumerator; +import mobac.program.interfaces.CapabilityDeletable; +import mobac.program.interfaces.DownloadJobListener; +import mobac.program.interfaces.DownloadableElement; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.TileFilter; +import mobac.program.interfaces.ToolTipProvider; +import mobac.program.tilefilter.DummyTileFilter; +import mobac.utilities.I18nUtils; +import mobac.utilities.tar.TarIndexedArchive; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.log4j.Logger; + +public class Map implements MapInterface, ToolTipProvider, CapabilityDeletable, TreeNode, DownloadableElement { + + protected String name; + + protected Layer layer; + + protected TileImageParameters parameters = null; + + @XmlAttribute + protected Point maxTileCoordinate = null; + + @XmlAttribute + protected Point minTileCoordinate = null; + + @XmlAttribute + protected MapSource mapSource = null; + + protected Dimension tileDimension = null; + + @XmlAttribute + protected int zoom; + + private static Logger log = Logger.getLogger(Map.class); + + protected Map() { + } + + protected Map(Layer layer, String name, MapSource mapSource, int zoom, Point minTileCoordinate, + Point maxTileCoordinate, TileImageParameters parameters) { + this.layer = layer; + this.maxTileCoordinate = maxTileCoordinate; + this.minTileCoordinate = minTileCoordinate; + this.name = name; + this.mapSource = mapSource; + this.zoom = zoom; + this.parameters = parameters; + calculateRuntimeValues(); + } + + protected void calculateRuntimeValues() { + if (mapSource == null) + throw new RuntimeException("The map source of map " + name + " is unknown to MOBAC"); + if (parameters == null) { + int tileSize = mapSource.getMapSpace().getTileSize(); + tileDimension = new Dimension(tileSize, tileSize); + } else + tileDimension = parameters.getDimension(); + } + + public LayerInterface getLayer() { + return layer; + } + + @XmlTransient + public void setLayer(LayerInterface layer) { + this.layer = (Layer) layer; + } + + public MapSource getMapSource() { + return mapSource; + } + + public Point getMaxTileCoordinate() { + return maxTileCoordinate; + } + + public Point getMinTileCoordinate() { + return minTileCoordinate; + } + + @XmlAttribute + public String getName() { + return name; + } + + public int getZoom() { + return zoom; + } + + @Override + public String toString() { + return getName(); + } + + public TileImageParameters getParameters() { + return parameters; + } + + public void setParameters(TileImageParameters parameters) { + this.parameters = parameters; + } + + public String getInfoText() { + return "Map\n name=" + name + "\n mapSource=" + mapSource + "\n zoom=" + zoom + "\n maxTileCoordinate=" + + maxTileCoordinate.x + "/" + maxTileCoordinate.y + "\n minTileCoordinate=" + minTileCoordinate.x + "/" + + minTileCoordinate.y + "\n parameters=" + parameters; + } + + public String getToolTip() { + MapSpace mapSpace = mapSource.getMapSpace(); + EastNorthCoordinate tl = new EastNorthCoordinate(mapSpace, zoom, minTileCoordinate.x, minTileCoordinate.y); + EastNorthCoordinate br = new EastNorthCoordinate(mapSpace, zoom, maxTileCoordinate.x, maxTileCoordinate.y); + + StringWriter sw = new StringWriter(1024); + sw.write(""); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_map_title")); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_map_source", + StringEscapeUtils.escapeHtml4(mapSource.toString()), StringEscapeUtils.escapeHtml4(mapSource.getName()))); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_map_zoom_lv", zoom)); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_map_area_start", tl.toString(), minTileCoordinate.x, + minTileCoordinate.y)); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_map_area_end", br.toString(), maxTileCoordinate.x, + maxTileCoordinate.y)); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_map_size", + (maxTileCoordinate.x - minTileCoordinate.x + 1), (maxTileCoordinate.y - minTileCoordinate.y + 1))); + if (parameters != null) { + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_tile_size"), parameters.getWidth(), + parameters.getHeight())); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_tile_format"), parameters.getFormat() + .toString())); + } else { + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_tile_format_origin")); + } + + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_max_tile"), calculateTilesToDownload())); + sw.write(""); + return sw.toString(); + } + + public Dimension getTileSize() { + return tileDimension; + } + + public double getMinLat() { + return mapSource.getMapSpace().cYToLat(maxTileCoordinate.y, zoom); + } + + public double getMaxLat() { + return mapSource.getMapSpace().cYToLat(minTileCoordinate.y, zoom); + } + + public double getMinLon() { + return mapSource.getMapSpace().cXToLon(minTileCoordinate.x, zoom); + } + + public double getMaxLon() { + return mapSource.getMapSpace().cXToLon(maxTileCoordinate.x, zoom); + } + + public void delete() { + layer.deleteMap(this); + } + + public void setName(String newName) throws InvalidNameException { + if (layer != null) { + for (MapInterface map : layer) { + if ((map != this) && (newName.equals(map.getName()))) + throw new InvalidNameException("There is already a map named \"" + newName + + "\" in this layer.\nMap names have to unique within an layer."); + } + } + this.name = newName; + } + + public Enumeration children() { + return null; + } + + public boolean getAllowsChildren() { + return false; + } + + public TreeNode getChildAt(int childIndex) { + return null; + } + + public int getChildCount() { + return 0; + } + + public int getIndex(TreeNode node) { + return 0; + } + + public TreeNode getParent() { + return (TreeNode) layer; + } + + public boolean isLeaf() { + return true; + } + + public long calculateTilesToDownload() { + int tileSize = mapSource.getMapSpace().getTileSize(); + // This algorithm has to be identically to those used in + // @DownloadJobEnumerator + int xMin = minTileCoordinate.x / tileSize; + int xMax = maxTileCoordinate.x / tileSize; + int yMin = minTileCoordinate.y / tileSize; + int yMax = maxTileCoordinate.y / tileSize; + int width = xMax - xMin + 1; + int height = yMax - yMin + 1; + int tileCount = width * height; + // TODO correct tile count in case of multi-layer maps + // if (mapSource instanceof MultiLayerMapSource) { + // // We have a map with two layers and for each layer we have to + // // download the tiles - therefore double the tileCount + // tileCount *= 2; + // } + return tileCount; + } + + public boolean checkData() { + boolean result = false; + boolean[] checks = { name == null, // 0 + layer == null, // 1 + maxTileCoordinate == null, // 2 + minTileCoordinate == null, // 3 + mapSource == null, // 4 + zoom < 0 // 5 + }; + + for (int i = 0; i < checks.length; i++) + if (checks[i]) { + log.error("Problem detectected with map \"" + name + "\" check: " + i); + result = true; + } + // Automatically correct bad ordered min/max coordinates + try { + if (minTileCoordinate.x > maxTileCoordinate.x) { + int tmp = maxTileCoordinate.x; + maxTileCoordinate.x = minTileCoordinate.x; + minTileCoordinate.x = tmp; + } + if (minTileCoordinate.y > maxTileCoordinate.y) { + int tmp = maxTileCoordinate.y; + maxTileCoordinate.y = minTileCoordinate.y; + minTileCoordinate.y = tmp; + } + } catch (Exception e) { + } + + return result; + } + + public MapInterface deepClone(LayerInterface newLayer) { + try { + Map map = this.getClass().newInstance(); + map.layer = (Layer) newLayer; + map.mapSource = mapSource; + map.maxTileCoordinate = (Point) maxTileCoordinate.clone(); + map.minTileCoordinate = (Point) minTileCoordinate.clone(); + map.name = name; + if (parameters != null) + map.parameters = (TileImageParameters) parameters.clone(); + else + map.parameters = null; + map.tileDimension = (Dimension) tileDimension.clone(); + map.zoom = zoom; + return map; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Needs to be public - otherwise it will be kicked by ProGuard! + */ + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.layer = (Layer) parent; + calculateRuntimeValues(); + } + + public Enumeration getDownloadJobs(TarIndexedArchive tileArchive, DownloadJobListener listener) { + return new DownloadJobEnumerator(this, mapSource, tileArchive, listener); + } + + public TileFilter getTileFilter() { + return new DummyTileFilter(); + } + +} diff --git a/src/main/java/mobac/program/model/MapPolygon.java b/src/main/java/mobac/program/model/MapPolygon.java new file mode 100644 index 0000000..f88c401 --- /dev/null +++ b/src/main/java/mobac/program/model/MapPolygon.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.geom.Area; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import mobac.program.Logging; +import mobac.program.interfaces.LayerInterface; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.TileFilter; +import mobac.program.tilefilter.PolygonTileFilter; +import mobac.utilities.I18nUtils; +import mobac.utilities.MyMath; + +@XmlRootElement +public class MapPolygon extends Map { + + @XmlElement + protected Polygon polygon; + protected long calculatedTileCount = -1; + + protected MapPolygon() { + } + + + public static MapPolygon createTrackEnclosure(Layer layer, String name, MapSource mapSource, int zoom, + EastNorthCoordinate[] trackPoints, int pixelDistance, TileImageParameters parameters) { + + MapSpace mapSpace = mapSource.getMapSpace(); + Area area = new Area(); + for (int i = 1; i < trackPoints.length; i++) { + EastNorthCoordinate point1 = trackPoints[i - 1]; + EastNorthCoordinate point2 = trackPoints[i]; + + int y1 = mapSpace.cLatToY(point1.lat, zoom); + int y2 = mapSpace.cLatToY(point2.lat, zoom); + int x1 = mapSpace.cLonToX(point1.lon, zoom); + int x2 = mapSpace.cLonToX(point2.lon, zoom); + + Line2D.Double ln = new Line2D.Double(x1, y1, x2, y2); + double indent = pixelDistance; // distance from central line + double length = ln.getP1().distance(ln.getP2()); + + double dx_li = (ln.getX2() - ln.getX1()) / length * indent; + double dy_li = (ln.getY2() - ln.getY1()) / length * indent; + + // moved p1 point + double p1X = ln.getX1() - dx_li; + double p1Y = ln.getY1() - dy_li; + + // line moved to the left + double lX1 = ln.getX1() - dy_li; + double lY1 = ln.getY1() + dx_li; + double lX2 = ln.getX2() - dy_li; + double lY2 = ln.getY2() + dx_li; + + // moved p2 point + double p2X = ln.getX2() + dx_li; + double p2Y = ln.getY2() + dy_li; + + // line moved to the right + double rX1_ = ln.getX1() + dy_li; + double rY1 = ln.getY1() - dx_li; + double rX2 = ln.getX2() + dy_li; + double rY2 = ln.getY2() - dx_li; + + Path2D p = new Path2D.Double(); + p.moveTo(lX1, lY1); + p.lineTo(lX2, lY2); + p.lineTo(p2X, p2Y); + p.lineTo(rX2, rY2); + p.lineTo(rX1_, rY1); + p.lineTo(p1X, p1Y); + p.lineTo(lX1, lY1); + + area.add(new Area(p)); + } + PathIterator pi = area.getPathIterator(null); + ArrayList xPoints = new ArrayList(100); + ArrayList yPoints = new ArrayList(100); + double coords[] = new double[6]; + while (!pi.isDone()) { + int type = pi.currentSegment(coords); + switch (type) { + case PathIterator.SEG_MOVETO: + case PathIterator.SEG_LINETO: + case PathIterator.SEG_CLOSE: + xPoints.add((int) coords[0]); + yPoints.add((int) coords[1]); + break; + default: + Logging.LOG.warn("Area to polygon conversion: unexpected segment type found: " + type + " " + + Arrays.toString(coords)); + } + pi.next(); + } + int[] xp = new int[xPoints.size()]; + int[] yp = new int[yPoints.size()]; + for (int i = 0; i < xp.length; i++) { + xp[i] = xPoints.get(i); + yp[i] = yPoints.get(i); + } + Polygon polygon = new Polygon(xp, yp, xp.length); + return new MapPolygon(layer, name, mapSource, zoom, polygon, parameters); + } + + public static MapPolygon createFromMapPolygon(Layer layer, String name, int newZoom, MapPolygon map) { + Polygon oldPolygon = map.getPolygon(); + int oldZoom = map.getZoom(); + MapSpace mapSpace = map.getMapSource().getMapSpace(); + int xPoints[] = new int[oldPolygon.npoints]; + int yPoints[] = new int[oldPolygon.npoints]; + Point p = new Point(); + for (int i = 0; i < xPoints.length; i++) { + p.x = oldPolygon.xpoints[i]; + p.y = oldPolygon.ypoints[i]; + Point nP = mapSpace.changeZoom(p, oldZoom, newZoom); + xPoints[i] = nP.x; + yPoints[i] = nP.y; + } + Polygon newPolygon = new Polygon(xPoints, yPoints, xPoints.length); + return new MapPolygon(layer, name, map.getMapSource(), newZoom, newPolygon, map.getParameters()); + } + + public MapPolygon(Layer layer, String name, MapSource mapSource, int zoom, Polygon polygon, + TileImageParameters parameters) { + super(layer, name, mapSource, zoom, null, null, parameters); + this.polygon = polygon; + Rectangle bounds = polygon.getBounds(); + int mapSourceTileSize = mapSource.getMapSpace().getTileSize(); + // Make sure the minimum tile coordinate starts/ends on the edge of a tile from the map source + int minx = MyMath.roundDownToNearest(bounds.x, mapSourceTileSize); + int miny = MyMath.roundDownToNearest(bounds.y, mapSourceTileSize); + int maxx = MyMath.roundUpToNearest(bounds.x + bounds.width, mapSourceTileSize) - 1; + int maxy = MyMath.roundUpToNearest(bounds.y + bounds.height, mapSourceTileSize) - 1; + minTileCoordinate = new Point(minx, miny); + maxTileCoordinate = new Point(maxx, maxy); + internalCalculateTilesToDownload(); + } + + @Override + public long calculateTilesToDownload() { + if (calculatedTileCount < 0) + internalCalculateTilesToDownload(); + return calculatedTileCount; + } + + protected void internalCalculateTilesToDownload() { + int tileSize = mapSource.getMapSpace().getTileSize(); + double tileSizeD = tileSize; + int xMin = minTileCoordinate.x; + int xMax = maxTileCoordinate.x; + int yMin = minTileCoordinate.y; + int yMax = maxTileCoordinate.y; + + int count = 0; + for (int x = xMin; x <= xMax; x += tileSize) { + for (int y = yMin; y <= yMax; y += tileSize) { + if (polygon.intersects(x, y, tileSizeD, tileSizeD)) + count++; + } + } + calculatedTileCount = count; + } + + @Override + public String getToolTip() { + MapSpace mapSpace = mapSource.getMapSpace(); + EastNorthCoordinate tl = new EastNorthCoordinate(mapSpace, zoom, minTileCoordinate.x, minTileCoordinate.y); + EastNorthCoordinate br = new EastNorthCoordinate(mapSpace, zoom, maxTileCoordinate.x, maxTileCoordinate.y); + + StringWriter sw = new StringWriter(1024); + sw.write(""); + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_polygon_map_title")); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_map_source_short"), mapSource.getName())); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_map_zoom_lv"), zoom)); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_polygon_map_point"), polygon.npoints)); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_map_area_start"), tl, minTileCoordinate.x, minTileCoordinate.y)); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_map_area_end"), br, maxTileCoordinate.x, maxTileCoordinate.y)); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_map_size"), (maxTileCoordinate.x - minTileCoordinate.x + 1), (maxTileCoordinate.y - minTileCoordinate.y + 1))); + if (parameters != null) { + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_tile_size"), parameters.getWidth(), parameters.getHeight())); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_tile_format"), parameters.getFormat())); + } else + sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_tile_format_origin")); + sw.write(String.format(I18nUtils.localizedStringForKey("lp_atlas_info_max_tile"),calculateTilesToDownload())); + sw.write(""); + return sw.toString(); + } + + public Polygon getPolygon() { + return polygon; + } + + public TileFilter getTileFilter() { + return new PolygonTileFilter(this); + } + + @Override + public MapInterface deepClone(LayerInterface newLayer) { + MapPolygon map = (MapPolygon) super.deepClone(newLayer); + map.polygon = new Polygon(polygon.xpoints, polygon.ypoints, polygon.npoints); + return map; + } + +} diff --git a/src/main/java/mobac/program/model/MapSelection.java b/src/main/java/mobac/program/model/MapSelection.java new file mode 100644 index 0000000..b7d1277 --- /dev/null +++ b/src/main/java/mobac/program/model/MapSelection.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Point; + +import mobac.gui.mapview.JMapViewer; +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.program.interfaces.MapInterface; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; + +public class MapSelection { + + public static final int LAT_MAX = 85; + public static final int LAT_MIN = -85; + public static final int LON_MAX = 179; + public static final int LON_MIN = -179; + + private final MapSource mapSource; + private final MapSpace mapSpace; + private final int mapSourceTileSize; + private final int zoom; + private int minTileCoordinate_x; + private int minTileCoordinate_y; + private int maxTileCoordinate_x; + private int maxTileCoordinate_y; + + public MapSelection(MapSource mapSource, EastNorthCoordinate max, EastNorthCoordinate min) { + super(); + this.mapSource = mapSource; + this.mapSpace = mapSource.getMapSpace(); + mapSourceTileSize = this.mapSpace.getTileSize(); + zoom = JMapViewer.MAX_ZOOM; + int x1 = mapSpace.cLonToX(min.lon, zoom); + int x2 = mapSpace.cLonToX(max.lon, zoom); + int y1 = mapSpace.cLatToY(min.lat, zoom); + int y2 = mapSpace.cLatToY(max.lat, zoom); + setCoordinates(x1, x2, y1, y2); + } + + public MapSelection(MapInterface map) { + this(map.getMapSource(), map.getMaxTileCoordinate(), map.getMinTileCoordinate(), map.getZoom()); + } + + /** + * @param mapSource + * @param p1 + * tile coordinate + * @param p2 + * tile coordinate + * @param zoom + */ + public MapSelection(MapSource mapSource, Point p1, Point p2, int zoom) { + super(); + this.mapSource = mapSource; + this.mapSpace = mapSource.getMapSpace(); + mapSourceTileSize = mapSpace.getTileSize(); + this.zoom = zoom; + setCoordinates(p1.x, p2.x, p1.y, p2.y); + } + + public MapSelection(MapSource mapSource, MercatorPixelCoordinate c1, MercatorPixelCoordinate c2) { + if (c1.getZoom() != c2.getZoom()) + throw new RuntimeException("Different zoom levels - unsuported!"); + this.mapSource = mapSource; + this.mapSpace = mapSource.getMapSpace(); + mapSourceTileSize = mapSpace.getTileSize(); + this.zoom = c1.getZoom(); + setCoordinates(c1.getX(), c2.getX(), c1.getY(), c2.getY()); + } + + protected void setCoordinates(int x1, int x2, int y1, int y2) { + maxTileCoordinate_x = Math.max(x1, x2); + minTileCoordinate_x = Math.min(x1, x2); + maxTileCoordinate_y = Math.max(y1, y2); + minTileCoordinate_y = Math.min(y1, y2); + } + + /** + * Is an area selected or only one point? + * + * @return + */ + public boolean isAreaSelected() { + boolean result = maxTileCoordinate_x != minTileCoordinate_x && maxTileCoordinate_y != minTileCoordinate_y; + return result; + } + + /** + * Warning: maximum lat/lon is the top right corner of the map selection! + * + * @return maximum lat/lon + */ + public EastNorthCoordinate getMax() { + return new EastNorthCoordinate(mapSpace, zoom, maxTileCoordinate_x, minTileCoordinate_y); + } + + /** + * Warning: minimum lat/lon is the bottom left corner of the map selection! + * + * @return minimum lat/lon + */ + public EastNorthCoordinate getMin() { + return new EastNorthCoordinate(mapSpace, zoom, minTileCoordinate_x, maxTileCoordinate_y); + } + + /** + * Returns the top left tile x- and y-tile-number (minimum) of the selected area marked by the {@link MapSelection}. + * + * @param aZoomLevel + * @return tile number [0..2zoom] + */ + public Point getTopLeftTileNumber(int aZoomlevel) { + Point tlc = getTopLeftPixelCoordinate(aZoomlevel); + tlc.x /= mapSourceTileSize; + tlc.y /= mapSourceTileSize; + return tlc; + } + + public MercatorPixelCoordinate getTopLeftPixelCoordinate() { + return new MercatorPixelCoordinate(mapSpace, minTileCoordinate_x, minTileCoordinate_y, zoom); + } + + /** + * Returns the top left tile x- and y-tile-coordinate (minimum) of the selected area marked by the + * {@link MapSelection}. + * + * @param aZoomlevel + * @return tile coordinate [0..(256 * 2zoom)] + */ + public Point getTopLeftPixelCoordinate(int aZoomlevel) { + int zoomDiff = this.zoom - aZoomlevel; + int x = minTileCoordinate_x; + int y = minTileCoordinate_y; + if (zoomDiff < 0) { + zoomDiff = -zoomDiff; + x <<= zoomDiff; + y <<= zoomDiff; + } else { + x >>= zoomDiff; + y >>= zoomDiff; + } + return new Point(x, y); + } + + /** + * Returns the bottom right tile x- and y-tile-number (minimum) of the selected area marked by the + * {@link MapSelection}. + * + * @param aZoomlevel + * @return tile number [0..2zoom] + */ + public Point getBottomRightTileNumber(int aZoomlevel) { + Point brc = getBottomRightPixelCoordinate(aZoomlevel); + brc.x = brc.x / mapSourceTileSize; + brc.y = brc.y / mapSourceTileSize; + return brc; + } + + /** + * Returns the bottom right tile x- and y-tile-coordinate (minimum) of the selected area regarding the zoom level + * specified at creation time of this {@link MapSelection} instance. + * + * @return tile coordinate [0..(256 * 2zoom)] + */ + public MercatorPixelCoordinate getBottomRightPixelCoordinate() { + return new MercatorPixelCoordinate(mapSpace, maxTileCoordinate_x, maxTileCoordinate_y, zoom); + } + + /** + * Returns the bottom right tile x- and y-tile-coordinate (minimum) of the selected area marked by the + * {@link MapSelection}. + * + * @param aZoomlevel + * @return tile coordinate [0..(256 * 2zoom)] + */ + public Point getBottomRightPixelCoordinate(int aZoomlevel) { + int zoomDiff = this.zoom - aZoomlevel; + int x = maxTileCoordinate_x; + int y = maxTileCoordinate_y; + if (zoomDiff < 0) { + zoomDiff = -zoomDiff; + x <<= zoomDiff; + y <<= zoomDiff; + } else { + x >>= zoomDiff; + y >>= zoomDiff; + } + return new Point(x, y); + } + + /** + * Return the amount of tiles for the current selection in the specified zoom level. + * + * @param zoom + * is the zoom level to calculate the amount of tiles for + * @return the amount of tiles in the current selection in the supplied zoom level + */ + public long calculateNrOfTiles(int zoom) { + Point max = getBottomRightTileNumber(zoom); + Point min = getTopLeftTileNumber(zoom); + long width = max.x - min.x + 1; + long height = max.y - min.y + 1; + long tileCount = width * height; + if (mapSource instanceof AbstractMultiLayerMapSource) { + int mapLayerCount = ((AbstractMultiLayerMapSource) mapSource).getLayerMapSources().length; + tileCount *= mapLayerCount; + } + return tileCount; + } + + public long[] calculateNrOfTilesEx(int zoom) { + Point max = getBottomRightTileNumber(zoom); + Point min = getTopLeftTileNumber(zoom); + long width = max.x - min.x + 1; + long height = max.y - min.y + 1; + long tileCount = width * height; + if (mapSource instanceof AbstractMultiLayerMapSource) { + int mapLayerCount = ((AbstractMultiLayerMapSource) mapSource).getLayerMapSources().length; + tileCount *= mapLayerCount; + } + return new long[] { tileCount, width, height }; + } + + @Override + public String toString() { + EastNorthCoordinate max = getMax(); + EastNorthCoordinate min = getMin(); + return String.format("lat/lon: max(%6f/%6f) min(%6f/%6f)", new Object[] { max.lat, max.lon, min.lat, min.lon }); + } + +} diff --git a/src/main/java/mobac/program/model/MapSourceLoaderInfo.java b/src/main/java/mobac/program/model/MapSourceLoaderInfo.java new file mode 100644 index 0000000..392d751 --- /dev/null +++ b/src/main/java/mobac/program/model/MapSourceLoaderInfo.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.io.File; + +public class MapSourceLoaderInfo { + + public enum LoaderType { + MAPPACK, // map pack file + XML, // custom map xml + BSH // BeanShell script + ; + }; + + protected final LoaderType loaderType; + + protected final File sourceFile; + + protected final String revision; + + public MapSourceLoaderInfo(LoaderType loaderType, File sourceFile) { + this(loaderType, sourceFile, null); + } + + public MapSourceLoaderInfo(LoaderType loaderType, File sourceFile, String revision) { + super(); + this.loaderType = loaderType; + this.sourceFile = sourceFile; + this.revision = revision; + } + + public LoaderType getLoaderType() { + return loaderType; + } + + public File getSourceFile() { + return sourceFile; + } + + public String getRevision() { + return revision; + } + +} diff --git a/src/main/java/mobac/program/model/MapSourcesListModel.java b/src/main/java/mobac/program/model/MapSourcesListModel.java new file mode 100644 index 0000000..e7798ca --- /dev/null +++ b/src/main/java/mobac/program/model/MapSourcesListModel.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Vector; + +import javax.swing.AbstractListModel; + +import mobac.program.interfaces.MapSource; + +public class MapSourcesListModel extends AbstractListModel { + + ArrayList mapSources; + + public MapSourcesListModel(Vector source) { + this.mapSources = new ArrayList(source); + } + + public Object getElementAt(int index) { + return mapSources.get(index); + } + + public int getSize() { + return mapSources.size(); + } + + public Vector getVector() { + return new Vector(mapSources); + } + + public MapSource removeElement(int index) { + fireIntervalRemoved((Object) this, index, index); + return mapSources.remove(index); + } + + public void addElement(MapSource element) { + mapSources.add(element); + fireIntervalAdded((Object) this, mapSources.size(), mapSources.size()); + } + + public boolean moveUp(int index) { + if (index < 1) + return false; + MapSource ms = mapSources.remove(index - 1); + mapSources.add(index, ms); + fireContentsChanged(this, index - 1, index); + return true; + } + + public boolean moveDown(int index) { + if (index + 1 >= mapSources.size()) + return false; + MapSource ms = mapSources.remove(index + 1); + mapSources.add(index, ms); + fireContentsChanged(this, index, index + 1); + return true; + } + + public void sort() { + Collections.sort(mapSources, new Comparator() { + + public int compare(MapSource o1, MapSource o2) { + return o1.toString().compareTo(o2.toString()); + } + + }); + fireContentsChanged(mapSources, 0, mapSources.size()); + } +} diff --git a/src/main/java/mobac/program/model/MercatorPixelCoordinate.java b/src/main/java/mobac/program/model/MercatorPixelCoordinate.java new file mode 100644 index 0000000..163f327 --- /dev/null +++ b/src/main/java/mobac/program/model/MercatorPixelCoordinate.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import mobac.gui.mapview.JMapViewer; +import mobac.program.interfaces.MapSpace; + + +/** + * Coordinate point in Mercator projection regarding a world with height and + * width 230 pixels (222 tiles with size 256 pixels). This + * is the maximum size a int can hold. + */ +public class MercatorPixelCoordinate { + + private final MapSpace mapSpace; + private final int x; + private final int y; + private final int zoom; + + public MercatorPixelCoordinate(MapSpace mapSpace, int x, int y, int zoom) { + super(); + this.mapSpace = mapSpace; + this.x = x; + this.y = y; + this.zoom = zoom; + } + + public MercatorPixelCoordinate(MapSpace mapSpace, double lat, double lon) { + super(); + this.mapSpace = mapSpace; + this.x = mapSpace.cLonToX(lon, JMapViewer.MAX_ZOOM); + this.y = mapSpace.cLatToY(lat, JMapViewer.MAX_ZOOM); + this.zoom = JMapViewer.MAX_ZOOM; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getZoom() { + return zoom; + } + + public MapSpace getMapSpace() { + return mapSpace; + } + + public EastNorthCoordinate getEastNorthCoordinate() { + double lon = mapSpace.cXToLon(x, zoom); + double lat = mapSpace.cYToLat(y, zoom); + return new EastNorthCoordinate(lat, lon); + } + + public MercatorPixelCoordinate adaptToZoomlevel(int aZoomlevel) { + int zoomDiff = this.zoom - aZoomlevel; + int new_x = x; + int new_y = y; + if (zoomDiff < 0) { + zoomDiff = -zoomDiff; + new_x <<= zoomDiff; + new_y <<= zoomDiff; + } else { + new_x >>= zoomDiff; + new_y >>= zoomDiff; + } + return new MercatorPixelCoordinate(mapSpace, new_x, new_y, aZoomlevel); + } + + @Override + public String toString() { + return "x=" + x + " y=" + y + " zoom=" + zoom; + } + +} diff --git a/src/main/java/mobac/program/model/NumericDocument.java b/src/main/java/mobac/program/model/NumericDocument.java new file mode 100644 index 0000000..66403d2 --- /dev/null +++ b/src/main/java/mobac/program/model/NumericDocument.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Toolkit; + +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.PlainDocument; + +public class NumericDocument extends PlainDocument { + private static final long serialVersionUID = 1L; + public static final String NUMERIC = "0123456789"; + + public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException { + + if (str == null) + return; + + for (char c : str.toCharArray()) { + if (NUMERIC.indexOf(c) == -1) { + Toolkit.getDefaultToolkit().beep(); + return; + } + } + + super.insertString(offset, str, attr); + + } +} diff --git a/src/main/java/mobac/program/model/PaperSize.java b/src/main/java/mobac/program/model/PaperSize.java new file mode 100644 index 0000000..76a44e6 --- /dev/null +++ b/src/main/java/mobac/program/model/PaperSize.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import com.itextpdf.text.PageSize; +import com.itextpdf.text.Rectangle; + +public class PaperSize { + + public static enum Format { + A0(PageSize.A0), A1(PageSize.A1), A2(PageSize.A2), A3(PageSize.A3), A4(PageSize.A4), A5(PageSize.A5), A6( + PageSize.A6), A7(PageSize.A7), A8(PageSize.A8), A9(PageSize.A9), A10(PageSize.A10), ARCH_A( + PageSize.ARCH_A), ARCH_B(PageSize.ARCH_B), ARCH_C(PageSize.ARCH_C), ARCH_D(PageSize.ARCH_D), ARCH_E( + PageSize.ARCH_E), B0(PageSize.B0), B1(PageSize.B1), B2(PageSize.B2), B3(PageSize.B3), B4(PageSize.B4), B5( + PageSize.B5), B6(PageSize.B6), B7(PageSize.B7), B8(PageSize.B8), B9(PageSize.B9), B10(PageSize.B10), + // CROWN_OCTAVO(PageSize.CROWN_OCTAVO), + // CROWN_QUARTO(PageSize.CROWN_QUARTO), + // DEMY_OCTAVO(PageSize.DEMY_OCTAVO), + // DEMY_QUARTO(PageSize.DEMY_QUARTO), + // EXECUTIVE(PageSize.EXECUTIVE), + // FLSA(PageSize.FLSA), + // FLSE(PageSize.FLSE), + // HALFLETTER(PageSize.HALFLETTER), + // ID_1(PageSize.ID_1), + // ID_2(PageSize.ID_2), + // ID_3(PageSize.ID_3), + // LARGE_CROWN_OCTAVO(PageSize.LARGE_CROWN_OCTAVO), + // LARGE_CROWN_QUARTO(PageSize.LARGE_CROWN_QUARTO), + // LEDGER(PageSize.LEDGER), + // LEGAL(PageSize.LEGAL), + // LETTER(PageSize.LETTER), + // NOTE(PageSize.NOTE), + // PENGUIN_LARGE_PAPERBACK(PageSize.PENGUIN_LARGE_PAPERBACK), + // PENGUIN_SMALL_PAPERBACK(PageSize.PENGUIN_SMALL_PAPERBACK), + // POSTCARD(PageSize.POSTCARD), + // ROYAL_OCTAVO(PageSize.ROYAL_OCTAVO), + // ROYAL_QUARTO(PageSize.ROYAL_QUARTO), + // SMALL_PAPERBACK(PageSize.SMALL_PAPERBACK), + // TABLOID(PageSize.TABLOID) + ; + + public final float width, height; + + private Format(final Rectangle rectangle) { + width = rectangle.getWidth(); + height = rectangle.getHeight(); + } + } + + public final double width, height; + public final boolean landscape; + public final Format format; + + public PaperSize(Format format, boolean landscape) { + if (landscape) { + width = format.height; + height = format.width; + } else { + width = format.width; + height = format.height; + } + this.landscape = landscape; + this.format = format; + } + + public PaperSize(double width, double height) { + this.width = width; + this.height = height; + format = null; + landscape = width > height; + } + + public Rectangle createRectangle() { + return new Rectangle((float) width, (float) height); + } +} diff --git a/src/main/java/mobac/program/model/Profile.java b/src/main/java/mobac/program/model/Profile.java new file mode 100644 index 0000000..2740440 --- /dev/null +++ b/src/main/java/mobac/program/model/Profile.java @@ -0,0 +1,229 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.JOptionPane; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.ValidationEvent; +import javax.xml.bind.ValidationEventHandler; +import javax.xml.bind.ValidationEventLocator; + +import mobac.gui.panels.JProfilesPanel; +import mobac.program.DirectoryManager; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.interfaces.AtlasObject; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +/** + * A profile is a saved atlas. The available profiles ({@link Profile} instances) are visible in the + * profilesCombo in the {@link JProfilesPanel}. + */ +public class Profile implements Comparable { + + private static Logger log = Logger.getLogger(Profile.class); + + public static final String PROFILE_NAME_REGEX = "[\\w _-]+"; + + public static final String PROFILE_FILENAME_PREFIX = "mobac-profile-"; + + public static final Pattern PROFILE_FILENAME_PATTERN = Pattern.compile(PROFILE_FILENAME_PREFIX + "(" + + PROFILE_NAME_REGEX + ").xml"); + + public static final Profile DEFAULT = new Profile(); + + private File file; + private String name; + private static Vector profiles = new Vector(); + + /** + * Profiles management method + */ + public static void updateProfiles() { + File profilesDir = DirectoryManager.atlasProfilesDir; + final Set deletedProfiles = new HashSet(); + deletedProfiles.addAll(profiles); + profilesDir.list(new FilenameFilter() { + + public boolean accept(File dir, String fileName) { + Matcher m = PROFILE_FILENAME_PATTERN.matcher(fileName); + if (m.matches()) { + String profileName = m.group(1); + Profile profile = new Profile(new File(dir, fileName), profileName); + if (!deletedProfiles.remove(profile)) + profiles.add(profile); + } + return false; + } + }); + for (Profile p : deletedProfiles) + profiles.remove(p); + Collections.sort(profiles); + } + + /** + * Profiles management method + */ + public static Vector getProfiles() { + updateProfiles(); + return profiles; + } + + /** + * Load a profile by it's name + * + * @param name + */ + public Profile(String name) { + this(new File(DirectoryManager.atlasProfilesDir, getProfileFileName(name)), name); + } + + /** + * Default profile + */ + protected Profile() { + this(new File(DirectoryManager.atlasProfilesDir, "mobac-profile.xml"), ""); + } + + protected Profile(File file, String name) { + super(); + this.file = file; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public File getFile() { + return file; + } + + public String getName() { + return name; + } + + public boolean exists() { + return file.isFile(); + } + + public void delete() { + if (!file.delete()) + file.deleteOnExit(); + } + + public int compareTo(Profile o) { + return file.compareTo(o.file); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Profile)) + return false; + Profile p = (Profile) obj; + return file.equals(p.file); + } + + @Override + public int hashCode() { + assert false : "hashCode not designed"; + return -1; + } + + public void save(AtlasInterface atlasInterface) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(Atlas.class); + Marshaller m = context.createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + FileOutputStream fo = null; + try { + fo = new FileOutputStream(file); + m.marshal(atlasInterface, fo); + } catch (FileNotFoundException e) { + throw new JAXBException(e); + } finally { + Utilities.closeStream(fo); + } + } + + public AtlasInterface load() throws JAXBException { + JAXBContext context = JAXBContext.newInstance(Atlas.class); + Unmarshaller um = context.createUnmarshaller(); + um.setEventHandler(new ValidationEventHandler() { + + public boolean handleEvent(ValidationEvent event) { + ValidationEventLocator loc = event.getLocator(); + String file = loc.getURL().getFile(); + int lastSlash = file.lastIndexOf('/'); + if (lastSlash > 0) + file = file.substring(lastSlash + 1); + int ret = JOptionPane.showConfirmDialog( + null, + String.format(I18nUtils.localizedStringForKey("msg_error_load_atlas_profile"), + event.getMessage(), file, loc.getLineNumber(), loc.getColumnNumber()), + I18nUtils.localizedStringForKey("msg_error_load_atlas_profile_title"), + JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE); + log.error(event.toString()); + return (ret == JOptionPane.YES_OPTION); + } + }); + try { + AtlasInterface newAtlas = (AtlasInterface) um.unmarshal(file); + return newAtlas; + } catch (Exception e) { + throw new JAXBException(e.getMessage(), e); + } + } + + public static boolean checkAtlas(AtlasInterface atlasInterface) { + return checkAtlasObject(atlasInterface); + } + + public static String getProfileFileName(String profileName) { + return PROFILE_FILENAME_PREFIX + profileName + ".xml"; + } + + private static boolean checkAtlasObject(Object o) { + boolean result = false; + if (o instanceof AtlasObject) { + result |= ((AtlasObject) o).checkData(); + } + if (o instanceof Iterable) { + Iterable it = (Iterable) o; + for (Object ao : it) { + result |= checkAtlasObject(ao); + } + } + return result; + } +} diff --git a/src/main/java/mobac/program/model/ProxyType.java b/src/main/java/mobac/program/model/ProxyType.java new file mode 100644 index 0000000..35cd5fb --- /dev/null +++ b/src/main/java/mobac/program/model/ProxyType.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import mobac.utilities.I18nUtils; + +public enum ProxyType { + SYSTEM, // + APP_SETTINGS, // + CUSTOM, // + CUSTOM_W_AUTH; + + //private String text; + +// private ProxyType(String text) { +// this.text = text; +// } + + @Override + public String toString() { + switch(this) + { + case SYSTEM: return I18nUtils.localizedStringForKey("set_net_proxy_settings_java"); + case APP_SETTINGS: return I18nUtils.localizedStringForKey("set_net_proxy_settings_application"); + case CUSTOM: return I18nUtils.localizedStringForKey("set_net_proxy_settings_custom"); + case CUSTOM_W_AUTH: return I18nUtils.localizedStringForKey("set_net_proxy_settings_custom_auth"); + } + return I18nUtils.localizedStringForKey("Undefined"); + } + +} diff --git a/src/main/java/mobac/program/model/SelectedZoomLevels.java b/src/main/java/mobac/program/model/SelectedZoomLevels.java new file mode 100644 index 0000000..765f3fa --- /dev/null +++ b/src/main/java/mobac/program/model/SelectedZoomLevels.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TreeSet; + +import mobac.gui.components.JZoomCheckBox; + +public class SelectedZoomLevels { + + private TreeSet zoomLevels = new TreeSet(); + + public SelectedZoomLevels(JZoomCheckBox[] zoomCheckboxes) { + for (JZoomCheckBox cb : zoomCheckboxes) { + if (cb.isSelected()) + setZoomLevelSelected(cb.getZoomLevel()); + } + } + + public SelectedZoomLevels(List zoomLevelList) { + zoomLevels.addAll(zoomLevelList); + } + + protected SelectedZoomLevels() { + } + + public void setZoomLevelSelected(int zoomLevel) { + zoomLevels.add(new Integer(zoomLevel)); + } + + public int[] getZoomLevels() { + int result[] = new int[zoomLevels.size()]; + int i = 0; + for (Integer z : zoomLevels) { + result[i++] = z.intValue(); + } + return result; + } + + public List getZoomLevelList() { + return new ArrayList(zoomLevels); + } + + public int getZoomLevelCount() { + return zoomLevels.size(); + } + + @Override + public String toString() { + return "ZoomLevels: " + Arrays.toString(getZoomLevels()); + } + +} diff --git a/src/main/java/mobac/program/model/Settings.java b/src/main/java/mobac/program/model/Settings.java new file mode 100644 index 0000000..f9025d2 --- /dev/null +++ b/src/main/java/mobac/program/model/Settings.java @@ -0,0 +1,520 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Toolkit; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Vector; +import java.util.concurrent.TimeUnit; + +import javax.swing.JOptionPane; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.ValidationEvent; +import javax.xml.bind.ValidationEventHandler; +import javax.xml.bind.annotation.XmlAccessOrder; +import javax.xml.bind.annotation.XmlAccessorOrder; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +import mobac.gui.actions.GpxLoad; +import mobac.gui.panels.JCoordinatesPanel; +import mobac.mapsources.MapSourcesManager; +import mobac.program.DirectoryManager; +import mobac.program.ProgramInfo; +import mobac.utilities.I18nUtils; +import mobac.utilities.Utilities; +import mobac.utilities.stream.ThrottledInputStream; + +import org.apache.log4j.Logger; + +@XmlRootElement +@XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL) +public class Settings { + + private static Logger log = Logger.getLogger(Settings.class); + private static Settings instance = new Settings(); + + public static final File FILE = new File(DirectoryManager.userSettingsDir, "settings.xml"); + + private static long SETTINGS_LAST_MODIFIED = 0; + + private static final String SYSTEM_PROXY_HOST = System.getProperty("http.proxyHost"); + private static final String SYSTEM_PROXY_PORT = System.getProperty("http.proxyPort"); + + @XmlElement(defaultValue = "") + private String version; + + public int maxMapSize = 65536; + + public int mapOverlapTiles = 0; + + public boolean tileStoreEnabled = true; + + /** + * Mapview related settings + */ + public int mapviewZoom = 3; + public int mapviewGridZoom = -1; + public EastNorthCoordinate mapviewCenterCoordinate = new EastNorthCoordinate(50, 9); + + public Point mapviewSelectionMax = null; + public Point mapviewSelectionMin = null; + @XmlElementWrapper(name = "selectedZoomLevels") + @XmlElement(name = "zoomLevel") + public List selectedZoomLevels = null; + + @XmlElement(nillable = false) + public String mapviewMapSource = null; + + public String elementName = null; + + private String userAgent = null; + + public int downloadThreadCount = 2; + public int downloadRetryCount = 1; + + private boolean customTileProcessing = false; + private Dimension tileSize = new Dimension(256, 256); + private TileImageFormat tileImageFormat = TileImageFormat.PNG; + + public CoordinateStringFormat coordinateNumberFormat = CoordinateStringFormat.DEG_LOCAL; + + public final Directories directories = new Directories(); + + public static class Directories { + @XmlElement + private String atlasOutputDirectory = null; + + @XmlElement + public String tileStoreDirectory; + + @XmlElement + private String mapSourcesDirectory; + } + + @XmlElementWrapper(name = "placeBookmarks") + @XmlElement(name = "bookmark") + public List placeBookmarks = new ArrayList(); + + /** + * Connection timeout in seconds (default 10 seconds) + */ + public int httpConnectionTimeout = 10; + + /** + * Read timeout in seconds (default 10 seconds) + */ + public int httpReadTimeout = 10; + + /** + * Maximum expiration (in milliseconds) acceptable. If a server sets an expiration time larger than this value it is + * truncated to this value on next download. + */ + public long tileMaxExpirationTime = TimeUnit.DAYS.toMillis(365); + + /** + * Minimum expiration (in milliseconds) acceptable. If a server sets an expiration time smaller than this value it + * is truncated to this value on next download. + */ + public long tileMinExpirationTime = TimeUnit.DAYS.toMillis(5); + + /** + * Expiration time (in milliseconds) of a tile if the server does not provide an expiration time + */ + public long tileDefaultExpirationTime = TimeUnit.DAYS.toMillis(28); + + public String googleLanguage = "en"; + public String osmHikingTicket = ""; + + /** + * Development mode enabled/disabled + *

+ * In development mode one additional map source is available for using MOBAC Debug TileServer + *

+ */ + @XmlElement + public boolean devMode = false; + + /** + * Saves the last used directory of the GPX file chooser dialog. Used in {@link GpxLoad}. + */ + public String gpxFileChooserDir = ""; + + public final AtlasFormatSpecificSettings atlasFormatSpecificSettings = new AtlasFormatSpecificSettings(); + + public static class AtlasFormatSpecificSettings { + + @XmlElement + public Integer garminCustomMaxMapCount = 100; + } + + public final MainWindowSettings mainWindow = new MainWindowSettings(); + + public static class MainWindowSettings { + public Dimension size = new Dimension(); + public Point position = new Point(-1, -1); + public boolean maximized = true; + + public boolean leftPanelVisible = true; + public boolean rightPanelVisible = true; + + @XmlElementWrapper(name = "collapsedPanels") + @XmlElement(name = "collapsedPanel") + public Vector collapsedPanels = new Vector(); + } + + /** + * Network settings + */ + private ProxyType proxyType = ProxyType.CUSTOM; + private String customProxyHost = ""; + private String customProxyPort = ""; + private String customProxyUserName = ""; + private String customProxyPassword = ""; + private long bandwidthLimit = 0; + + @XmlElementWrapper(name = "mapSourcesDisabled") + @XmlElement(name = "mapSource") + public Vector mapSourcesDisabled = new Vector(); + + @XmlElementWrapper(name = "mapSourcesEnabled") + @XmlElement(name = "mapSource") + public Vector mapSourcesEnabled = new Vector(); + + @XmlElement(name = "MapSourcesUpdate") + public final MapSourcesUpdate mapSourcesUpdate = new MapSourcesUpdate(); + + public static class MapSourcesUpdate { + /** + * Last ETag value retrieved while online map source update. + * + * @see MapSourcesManager#mapsourcesOnlineUpdate() + * @see http://en.wikipedia.org/wiki/HTTP_ETag + */ + public String etag; + + public Date lastUpdate; + } + + public transient UnitSystem unitSystem = UnitSystem.Metric; + + public final SettingsPaperAtlas paperAtlas = new SettingsPaperAtlas(); + public final SettingsWgsGrid wgsGrid = new SettingsWgsGrid(); + + public boolean ignoreDlErrors = false; + + public String localeLanguage = null; + public String localeCountry = null; + + private Settings() { + elementName = "Layer";// no need i18n for it + Dimension dScreen = Toolkit.getDefaultToolkit().getScreenSize(); + mainWindow.size.width = (int) (0.9f * dScreen.width); + mainWindow.size.height = (int) (0.9f * dScreen.height); + mainWindow.collapsedPanels.add(JCoordinatesPanel.NAME); + mainWindow.collapsedPanels.add("Gpx"); + + Locale defaultLocale = Locale.getDefault(); + if (defaultLocale.equals(new Locale("zh", "CN"))) { + localeLanguage = "zh"; + localeCountry = "CN"; + } else if (defaultLocale.equals(new Locale("zh", "TW"))) { + localeLanguage = "zh"; + localeCountry = "TW"; + } else if (defaultLocale.equals(new Locale("ja", "JP"))) { + localeLanguage = "ja"; + localeCountry = "JP"; + } else if (defaultLocale.equals(new Locale("fr", "FR"))) { + localeLanguage = "fr"; + localeCountry = "FR"; + } else { + localeLanguage = "en"; + localeCountry = ""; + } + } + + public static Settings getInstance() { + return instance; + } + + public static void load() throws JAXBException { + try { + JAXBContext context = JAXBContext.newInstance(Settings.class); + Unmarshaller um = context.createUnmarshaller(); + um.setEventHandler(new ValidationEventHandler() { + + public boolean handleEvent(ValidationEvent event) { + + log.warn("Problem on loading settings.xml: " + event.getMessage()); + return true; + } + }); + instance = (Settings) um.unmarshal(FILE); + instance.wgsGrid.checkValues(); + instance.paperAtlas.checkValues(); + SETTINGS_LAST_MODIFIED = FILE.lastModified(); + + // Settings 重新加载之后,必须更新语言资源 + I18nUtils.updateLocalizedStringFormSettings(); + + } finally { + Settings s = getInstance(); + s.applyProxySettings(); + } + } + + public static boolean checkSettingsFileModified() { + if (SETTINGS_LAST_MODIFIED == 0) + return false; + // Check if the settings.xml has been modified + // since it has been loaded + long lastModified = FILE.lastModified(); + return (SETTINGS_LAST_MODIFIED != lastModified); + } + + public static void save() throws JAXBException { + getInstance().version = ProgramInfo.getVersion(); + JAXBContext context = JAXBContext.newInstance(Settings.class); + Marshaller m = context.createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + ByteArrayOutputStream bo = null; + FileOutputStream fo = null; + try { + // First we write to a buffer and if that works be write the buffer + // to disk. Direct writing to file may result in an defect xml file + // in case of an error + bo = new ByteArrayOutputStream(); + m.marshal(getInstance(), bo); + fo = new FileOutputStream(FILE); + fo.write(bo.toByteArray()); + fo.close(); + SETTINGS_LAST_MODIFIED = FILE.lastModified(); + } catch (IOException e) { + throw new JAXBException(e); + } finally { + Utilities.closeStream(fo); + } + } + + public static void loadOrQuit() { + try { + load(); + } catch (JAXBException e) { + log.error(e); + JOptionPane + .showMessageDialog(null, I18nUtils.localizedStringForKey(I18nUtils + .localizedStringForKey("msg_settings_file_can_not_parse")), I18nUtils + .localizedStringForKey("Error"), JOptionPane.ERROR_MESSAGE); + System.exit(0); + } + } + + public String getUserAgent() { + if (userAgent != null) + return userAgent; + else + return ProgramInfo.getUserAgent(); + } + + public void setUserAgent(String userAgent) { + if (userAgent != null) { + userAgent = userAgent.trim(); + if (userAgent.length() == 0) + userAgent = null; + } + this.userAgent = userAgent; + } + + public boolean isCustomTileSize() { + return customTileProcessing; + } + + public void setCustomTileSize(boolean customTileSize) { + this.customTileProcessing = customTileSize; + } + + public Dimension getTileSize() { + return tileSize; + } + + public void setTileSize(Dimension tileSize) { + this.tileSize = tileSize; + } + + public TileImageFormat getTileImageFormat() { + return tileImageFormat; + } + + public void setTileImageFormat(TileImageFormat tileImageFormat) { + this.tileImageFormat = tileImageFormat; + } + + public ProxyType getProxyType() { + return proxyType; + } + + public void setProxyType(ProxyType proxyType) { + this.proxyType = proxyType; + } + + public String getCustomProxyHost() { + return customProxyHost; + } + + public String getCustomProxyPort() { + return customProxyPort; + } + + public void setCustomProxyHost(String proxyHost) { + this.customProxyHost = proxyHost; + } + + public void setCustomProxyPort(String proxyPort) { + this.customProxyPort = proxyPort; + } + + public String getCustomProxyUserName() { + return customProxyUserName; + } + + public void setCustomProxyUserName(String customProxyUserName) { + this.customProxyUserName = customProxyUserName; + } + + public String getCustomProxyPassword() { + return customProxyPassword; + } + + public void setCustomProxyPassword(String customProxyPassword) { + this.customProxyPassword = customProxyPassword; + } + + public void applyProxySettings() { + boolean useSystemProxies = false; + String newProxyHost = null; + String newProxyPort = null; + Authenticator newAuthenticator = null; + switch (proxyType) { + case SYSTEM: + log.info("Applying proxy configuration: system settings"); + useSystemProxies = true; + break; + case APP_SETTINGS: + newProxyHost = SYSTEM_PROXY_HOST; + newProxyPort = SYSTEM_PROXY_PORT; + log.info("Applying proxy configuration: host=" + newProxyHost + " port=" + newProxyPort); + break; + case CUSTOM: + newProxyHost = customProxyHost; + newProxyPort = customProxyPort; + log.info("Applying proxy configuration: host=" + newProxyHost + " port=" + newProxyPort); + break; + case CUSTOM_W_AUTH: + newProxyHost = customProxyHost; + newProxyPort = customProxyPort; + newAuthenticator = new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(customProxyUserName, customProxyPassword.toCharArray()); + } + }; + log.info("Applying proxy configuration: host=" + newProxyHost + " port=" + newProxyPort + " user=" + + customProxyUserName); + break; + } + Utilities.setHttpProxyHost(newProxyHost); + Utilities.setHttpProxyPort(newProxyPort); + Authenticator.setDefault(newAuthenticator); + System.setProperty("java.net.useSystemProxies", Boolean.toString(useSystemProxies)); + } + + public long getBandwidthLimit() { + return bandwidthLimit; + } + + public void setBandwidthLimit(long bandwidthLimit) { + this.bandwidthLimit = bandwidthLimit; + ThrottledInputStream.setBandwidth(bandwidthLimit); + } + + @XmlElement + public void setUnitSystem(UnitSystem unitSystem) { + if (unitSystem == null) + unitSystem = UnitSystem.Metric; + this.unitSystem = unitSystem; + } + + public UnitSystem getUnitSystem() { + return unitSystem; + } + + @XmlTransient + public File getMapSourcesDirectory() { + String mapSourcesDirCfg = directories.mapSourcesDirectory; + File mapSourcesDir; + if (mapSourcesDirCfg == null || mapSourcesDirCfg.trim().length() == 0) + mapSourcesDir = DirectoryManager.mapSourcesDir; + else + mapSourcesDir = new File(mapSourcesDirCfg); + return mapSourcesDir; + } + + @XmlTransient + public File getAtlasOutputDirectory() { + if (directories.atlasOutputDirectory != null) + return new File(directories.atlasOutputDirectory); + return new File(DirectoryManager.currentDir, "atlases"); + } + + public String getAtlasOutputDirectoryString() { + if (directories.atlasOutputDirectory == null) + return ""; + return directories.atlasOutputDirectory; + } + + /** + * + * @param dir + * null or empty string resets to default directory otherwise set the new atlas output + * directory. + */ + public void setAtlasOutputDirectory(String dir) { + if (dir != null && dir.trim().length() == 0) + dir = null; + directories.atlasOutputDirectory = dir; + } + + public String getVersion() { + return version; + } + +} diff --git a/src/main/java/mobac/program/model/SettingsPaperAtlas.java b/src/main/java/mobac/program/model/SettingsPaperAtlas.java new file mode 100644 index 0000000..c7e496f --- /dev/null +++ b/src/main/java/mobac/program/model/SettingsPaperAtlas.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.gui.mapview.WgsGrid.WgsDensity; +import mobac.program.jaxb.PaperSizeAdapter; +import mobac.program.model.PaperSize.Format; + +@XmlRootElement +public class SettingsPaperAtlas implements Cloneable { + + public static final int + + COMPRESSION_DEFAULT = 6, COMPRESSION_MAX = 9, COMPRESSION_MIN = 0, + + CROP_DEFAULT = 15, CROP_MAX = 100, CROP_MIN = 0, + + DPI_DEFAULT = 96, DPI_MAX = 300, DPI_MIN = 72; + + public static final double + + MARGIN_DEFAULT = 22.6d, MARGIN_MAX = 144d, MARGIN_MIN = 0.0d, + + OVERLAP_DEFAULT = 28.2d, OVERLAP_MIN = 0d, OVERLAP_MAX = 144d, + + PAPER_SIZE_MAX = 16384d, PAPER_SIZE_MIN = 1d; + + public static final PaperSize PAPER_SIZE_DEFAULT = new PaperSize(Format.A4, false); + + public int compression = COMPRESSION_DEFAULT, crop = CROP_DEFAULT, dpi = DPI_DEFAULT; + + public double marginBottom = MARGIN_DEFAULT, marginLeft = MARGIN_DEFAULT, marginRight = MARGIN_DEFAULT, + marginTop = MARGIN_DEFAULT, overlap = OVERLAP_DEFAULT; + + public boolean compass = true, pageNumbers = true, scaleBar = true, wgsEnabled = true; + + @XmlElement(defaultValue = "A4") + @XmlJavaTypeAdapter(PaperSizeAdapter.class) + public PaperSize paperSize = PAPER_SIZE_DEFAULT; + + public WgsDensity wgsDensity = SettingsWgsGrid.DEFAULT_DENSITY; + + @Override + public SettingsPaperAtlas clone() { + try { + return (SettingsPaperAtlas) super.clone(); + } catch (Exception e) { + throw new InternalError(); + } + } + + public void checkValues() { + if (compression < COMPRESSION_MIN || compression > COMPRESSION_MAX) { + compression = COMPRESSION_DEFAULT; + } + if (crop < CROP_MIN || crop > CROP_MAX) { + crop = CROP_DEFAULT; + } + if (dpi < DPI_MIN || dpi > DPI_MAX) { + dpi = DPI_DEFAULT; + } + if (marginBottom < MARGIN_MIN || marginBottom > MARGIN_MAX) { + marginBottom = MARGIN_DEFAULT; + } + if (marginLeft < MARGIN_MIN || marginLeft > MARGIN_MAX) { + marginLeft = MARGIN_DEFAULT; + } + if (marginRight < MARGIN_MIN || marginRight > MARGIN_MAX) { + marginRight = MARGIN_DEFAULT; + } + if (marginTop < MARGIN_MIN || marginTop > MARGIN_MAX) { + marginTop = MARGIN_DEFAULT; + } + if (paperSize == null) { + paperSize = PAPER_SIZE_DEFAULT; + } + if (wgsDensity == null) { + wgsDensity = SettingsWgsGrid.DEFAULT_DENSITY; + } + } +} diff --git a/src/main/java/mobac/program/model/SettingsWgsGrid.java b/src/main/java/mobac/program/model/SettingsWgsGrid.java new file mode 100644 index 0000000..3a4f723 --- /dev/null +++ b/src/main/java/mobac/program/model/SettingsWgsGrid.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Color; +import java.awt.Font; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import mobac.gui.mapview.WgsGrid.WgsDensity; +import mobac.program.jaxb.ColorAdapter; +import mobac.program.jaxb.FontAdapter; + +public class SettingsWgsGrid implements Cloneable { + + public static final Color DEFAULT_COLOR = Color.BLUE; + + public static final WgsDensity DEFAULT_DENSITY = WgsDensity.SECOND_1; + + public static final Font DEFAULT_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + + public static final float WIDTH_DEFAULT = 1f; + public static final float WIDTH_MIN = 0.5f; + public static final float WIDTH_MAX = 5f; + + @XmlElement(defaultValue = "#0000FF") + @XmlJavaTypeAdapter(ColorAdapter.class) + public Color color = DEFAULT_COLOR; + + public boolean compressLabels = false; + + public WgsDensity density = DEFAULT_DENSITY; + + public boolean enabled = false; + + @XmlElement(defaultValue = "SansSerif-PLAIN-12") + @XmlJavaTypeAdapter(FontAdapter.class) + public Font font = DEFAULT_FONT; + + public float width = WIDTH_DEFAULT; + + @Override + public SettingsWgsGrid clone() { + try { + return (SettingsWgsGrid) super.clone(); + } catch (Exception e) { + return new SettingsWgsGrid(); + } + } + + public void checkValues() { + if (width < WIDTH_MIN || width > WIDTH_MAX) { + width = WIDTH_DEFAULT; + } + if (density == null) { + density = DEFAULT_DENSITY; + } + if (color == null) { + color = DEFAULT_COLOR; + } + if (font == null) { + font = DEFAULT_FONT; + } + } +} diff --git a/src/main/java/mobac/program/model/TileImageFormat.java b/src/main/java/mobac/program/model/TileImageFormat.java new file mode 100644 index 0000000..a6ddd20 --- /dev/null +++ b/src/main/java/mobac/program/model/TileImageFormat.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.io.OutputStream; +import java.util.ArrayList; + +import javax.swing.JComboBox; + +import mobac.gui.MainGUI; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.tiledatawriter.TileImageJpegDataWriter; +import mobac.program.tiledatawriter.TileImagePng4DataWriter; +import mobac.program.tiledatawriter.TileImagePng8DataWriter; +import mobac.program.tiledatawriter.TileImagePngDataWriter; +import mobac.utilities.I18nUtils; + +/** + * Defines all available image formats selectable in the {@link JComboBox} in the {@link MainGUI}. Each element of this + * enumeration contains one instance of an {@link TileImageDataWriter} instance that can perform one or more image + * operations (e.g. color reduction) and then saves the image to an {@link OutputStream}. + * + * @see TileImageDataWriter + * @see TileImagePngDataWriter + * @see TileImagePng4DataWriter + * @see TileImagePng8DataWriter + * @see TileImageJpegDataWriter + */ +public enum TileImageFormat { + + // PNG("PNG", new TileImagePngDataWriter()), // + // PNG8Bit(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_png_8bit"), new TileImagePng8DataWriter()), // + // PNG4Bit(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_png_4bit"), new TileImagePng4DataWriter()), // + // JPEG100(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q100"), new TileImageJpegDataWriter(1.00)), // + // JPEG99(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q99"), new TileImageJpegDataWriter(0.99)), // + // JPEG95(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q100"), new TileImageJpegDataWriter(0.95)), // + // JPEG90(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q90"), new TileImageJpegDataWriter(0.90)), // + // JPEG85(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q85"), new TileImageJpegDataWriter(0.85)), // + // JPEG80(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q80"), new TileImageJpegDataWriter(0.80)), // + // JPEG70(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q70"), new TileImageJpegDataWriter(0.70)), // + // JPEG60(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q60"), new TileImageJpegDataWriter(0.60)), // + // JPEG50(MainGUI.localizedStringForKey("lp_tile_param_image_fmt_jpg_q50"), new TileImageJpegDataWriter(0.50)); // + + PNG(new TileImagePngDataWriter(), "lp_tile_param_image_fmt_png"), // + PNG8Bit(new TileImagePng8DataWriter(), "lp_tile_param_image_fmt_png_8bit"), // + PNG4Bit(new TileImagePng4DataWriter(), "lp_tile_param_image_fmt_png_4bit"), // + JPEG100(new TileImageJpegDataWriter(1.00), "lp_tile_param_image_fmt_jpg_q100"), // + JPEG99(new TileImageJpegDataWriter(0.99), "lp_tile_param_image_fmt_jpg_q99"), // + JPEG95(new TileImageJpegDataWriter(0.95), "lp_tile_param_image_fmt_jpg_q95"), // + JPEG90(new TileImageJpegDataWriter(0.90), "lp_tile_param_image_fmt_jpg_q90"), // + JPEG85(new TileImageJpegDataWriter(0.85), "lp_tile_param_image_fmt_jpg_q85"), // + JPEG80(new TileImageJpegDataWriter(0.80), "lp_tile_param_image_fmt_jpg_q80"), // + JPEG70(new TileImageJpegDataWriter(0.70), "lp_tile_param_image_fmt_jpg_q70"), // + JPEG60(new TileImageJpegDataWriter(0.60), "lp_tile_param_image_fmt_jpg_q60"), // + JPEG50(new TileImageJpegDataWriter(0.50), "lp_tile_param_image_fmt_jpg_q50"); // + + // private final String description; + + private final TileImageDataWriter dataWriter; + + private final String translationKey; + + private TileImageFormat(TileImageDataWriter dataWriter, String translationKey) { + // this.description = description; + this.dataWriter = dataWriter; + this.translationKey = translationKey; + } + + @Override + public String toString() { + return I18nUtils.localizedStringForKey(translationKey); + } + + public TileImageDataWriter getDataWriter() { + return dataWriter; + } + + public TileImageType getType() { + return dataWriter.getType(); + } + + /** + * File extension + * + * @return + */ + public String getFileExt() { + return dataWriter.getType().getFileExt(); + } + + public static TileImageFormat[] getPngFormats() { + return getFormats(TileImageType.PNG); + } + + public static TileImageFormat[] getJpgFormats() { + return getFormats(TileImageType.JPG); + } + + protected static TileImageFormat[] getFormats(TileImageType tileImageType) { + ArrayList list = new ArrayList(); + for (TileImageFormat format : values()) { + if (tileImageType.equals(format.getType())) + list.add(format); + } + TileImageFormat[] result = new TileImageFormat[0]; + result = list.toArray(result); + return result; + } + +} diff --git a/src/main/java/mobac/program/model/TileImageParameters.java b/src/main/java/mobac/program/model/TileImageParameters.java new file mode 100644 index 0000000..47ce499 --- /dev/null +++ b/src/main/java/mobac/program/model/TileImageParameters.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import java.awt.Dimension; + +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAnyAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public final class TileImageParameters implements Cloneable { + + public static enum Name { + width, height, format, format_png, format_jpg + } + + @XmlAnyAttribute + protected AnyAttributeMap attr = new AnyAttributeMap(); + + /** + * Default constructor as required by JAXB + */ + protected TileImageParameters() { + super(); + } + + private TileImageParameters(AnyAttributeMap attrMap) { + attr.putAll(attrMap); + } + + protected void afterUnmarshal(Unmarshaller u, Object parent) { + // read all values once for detecting problems + attr.getInt(Name.height.name()); + attr.getInt(Name.width.name()); + TileImageFormat.valueOf(attr.getAttr("format")); + } + + public TileImageParameters(int width, int height, TileImageFormat format) { + super(); + attr.setAttr("format", format.name()); + attr.setInt("height", height); + attr.setInt("width", width); + } + + public int getWidth() { + return attr.getInt("width"); + } + + public int getHeight() { + return attr.getInt("height"); + } + + public Dimension getDimension() { + return new Dimension(getWidth(), getHeight()); + } + + public TileImageFormat getFormat() { + return TileImageFormat.valueOf(attr.getAttr("format")); + } + + @Override + public String toString() { + return "Tile size: (" + getWidth() + "/" + getHeight() + ") " + getFormat().toString() + ")"; + } + + @Override + public Object clone() throws CloneNotSupportedException { + return new TileImageParameters(attr); + } + +} diff --git a/src/main/java/mobac/program/model/TileImageType.java b/src/main/java/mobac/program/model/TileImageType.java new file mode 100644 index 0000000..e77ba9d --- /dev/null +++ b/src/main/java/mobac/program/model/TileImageType.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +/** + * Supported file extensions of all {@link TileImageFormat} enums. + */ +public enum TileImageType { + PNG("png"), JPG("jpeg"), GIF("gif"); + + private final String mime; + + private TileImageType(String mime) { + this.mime = mime; + } + + public String getFileExt() { + return name().toLowerCase(); + } + + public String getMimeType() { + return mime; + } + + public static TileImageType getTileImageType(String type) { + try { + return TileImageType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + for (TileImageType t : TileImageType.values()) { + if (t.getFileExt().equalsIgnoreCase(type)) + return t; + } + throw e; + } + } +} diff --git a/src/main/java/mobac/program/model/UnitSystem.java b/src/main/java/mobac/program/model/UnitSystem.java new file mode 100644 index 0000000..179d8a0 --- /dev/null +++ b/src/main/java/mobac/program/model/UnitSystem.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.model; + +import mobac.utilities.I18nUtils; + +public enum UnitSystem { + + Metric(6367.5d, 1000, 2.54d, "km", "m", "cm"), // + Imperial(3963.192d, 5280, 1d, "mi", "ft", "in"); + + /** + * Points per inch, default value for PDF format. + */ + public static final double PPI = 72d; + + public static double pointsToPixels(double points, int dpi) { + return points / PPI * dpi; + } + + public static double pixelsToPoints(double pixels, int dpi) { + return pixels / dpi * PPI; + } + + public final double earthRadius; + public final String unitLarge; + public final String unitSmall; + public final String unitTiny; + public final int unitFactor; + public final double inchFactor; + public final double maxAngularDistSmall; + + private UnitSystem(double earthRadius, int unitFactor, double inchFactor, String unitLarge, String unitSmall, + String unitTiny) { + this.earthRadius = earthRadius; + this.unitFactor = unitFactor; + this.inchFactor = inchFactor; + this.unitLarge = unitLarge; + this.unitSmall = unitSmall; + this.unitTiny = unitTiny; + this.maxAngularDistSmall = 1d / (earthRadius * unitFactor); + } + + private double unitsToInches(double units) { + return units / inchFactor; + } + + private double inchesToUnits(double inches) { + return inches * inchFactor; + } + + public double unitsToPoints(double units) { + return unitsToInches(units) * PPI; + } + + public double pointsToUnits(double points) { + return inchesToUnits(points / PPI); + } + + public double unitsToPixels(double units, int dpi) { + return unitsToInches(units) * dpi; + } + + public double pixelsToUnits(double pixels, int dpi) { + return inchesToUnits(pixels / dpi); + } + + public String toString() { + if (Metric.equals(this)) { + return I18nUtils.localizedStringForKey("set_display_unit_system_metric"); + } else { + return I18nUtils.localizedStringForKey("set_display_unit_system_imperial"); + } + } + +} diff --git a/src/main/java/mobac/program/model/package-info.java b/src/main/java/mobac/program/model/package-info.java new file mode 100644 index 0000000..7bd233f --- /dev/null +++ b/src/main/java/mobac/program/model/package-info.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +/** + * Package level definition of adapters for JAXB + */ +@XmlJavaTypeAdapters({ @XmlJavaTypeAdapter(value = PointAdapter.class, type = java.awt.Point.class), + @XmlJavaTypeAdapter(value = DimensionAdapter.class, type = java.awt.Dimension.class), + @XmlJavaTypeAdapter(value = PolygonAdapter.class, type = java.awt.Polygon.class) }) +package mobac.program.model; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters; + +import mobac.program.jaxb.DimensionAdapter; +import mobac.program.jaxb.PointAdapter; +import mobac.program.jaxb.PolygonAdapter; + diff --git a/src/main/java/mobac/program/tiledatawriter/ImageWriterWarningListener.java b/src/main/java/mobac/program/tiledatawriter/ImageWriterWarningListener.java new file mode 100644 index 0000000..768eb6b --- /dev/null +++ b/src/main/java/mobac/program/tiledatawriter/ImageWriterWarningListener.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tiledatawriter; + +import javax.imageio.ImageWriter; +import javax.imageio.event.IIOWriteWarningListener; + +import org.apache.log4j.Logger; + +/** + * Allows to capture non-fatal warnings that may occur upon writing an image. At the moment all warnings are simply + * logged. + */ +public class ImageWriterWarningListener implements IIOWriteWarningListener { + + private static final Logger log = Logger.getLogger(ImageWriterWarningListener.class); + + public static final IIOWriteWarningListener INSTANCE = new ImageWriterWarningListener(); + + public void warningOccurred(ImageWriter source, int imageIndex, String warning) { + if (log.isDebugEnabled()) + log.warn(warning + " - caused by: " + source + " on imageIndex " + imageIndex); + else + log.warn(warning); + } + +} diff --git a/src/main/java/mobac/program/tiledatawriter/TileImageJpegDataWriter.java b/src/main/java/mobac/program/tiledatawriter/TileImageJpegDataWriter.java new file mode 100644 index 0000000..fb7b0a6 --- /dev/null +++ b/src/main/java/mobac/program/tiledatawriter/TileImageJpegDataWriter.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tiledatawriter; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageType; + +import org.apache.commons.io.output.NullOutputStream; +import org.apache.log4j.Logger; + +public class TileImageJpegDataWriter implements TileImageDataWriter { + + protected static final Logger log = Logger.getLogger(TileImageJpegDataWriter.class); + + protected ImageWriter jpegImageWriter = null; + + protected ImageWriteParam iwp = null; + + protected float jpegCompressionLevel; + + /** + * + * @param jpegCompressionLevel + * a float between 0 and 1; 1 specifies minimum compression and maximum quality + */ + public TileImageJpegDataWriter(double jpegCompressionLevel) { + this((float) jpegCompressionLevel); + } + + public TileImageJpegDataWriter(float jpegCompressionLevel) { + this.jpegCompressionLevel = (float) jpegCompressionLevel; + } + + public TileImageJpegDataWriter(TileImageJpegDataWriter jpegWriter) { + this(jpegWriter.getJpegCompressionLevel()); + } + + public void initialize() { + if (log.isTraceEnabled()) { + String s = "Available JPEG image writers:"; + Iterator writers = ImageIO.getImageWritersByFormatName("jpeg"); + while (writers.hasNext()) { + ImageWriter w = writers.next(); + s += "\n\t" + w.getClass().getName(); + } + log.trace(s); + } + jpegImageWriter = ImageIO.getImageWritersByFormatName("jpeg").next(); + if (jpegImageWriter == null) + throw new NullPointerException("Unable to create a JPEG image writer"); + jpegImageWriter.addIIOWriteWarningListener(ImageWriterWarningListener.INSTANCE); + log.debug("Used JPEG image writer: " + jpegImageWriter.getClass().getName()); + iwp = jpegImageWriter.getDefaultWriteParam(); + iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + iwp.setCompressionQuality(jpegCompressionLevel); + } + + public void setJpegCompressionLevel(float jpegCompressionLevel) { + this.jpegCompressionLevel = jpegCompressionLevel; + iwp.setCompressionQuality(jpegCompressionLevel); + } + + public float getJpegCompressionLevel() { + return jpegCompressionLevel; + } + + public void processImage(BufferedImage image, OutputStream out) throws IOException { + + if (image.getColorModel().hasAlpha()) { + // Javas JPEG writes has a bug when the image has alpha transparency + // see http://stackoverflow.com/questions/4386446/problem-using-imageio-write-jpg-file + + BufferedImage imageRGB = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g = imageRGB.createGraphics(); + g.drawImage(image, null, 0, 0); + g.dispose(); + image = imageRGB; + } + + ImageOutputStream imageOut = ImageIO.createImageOutputStream(out); + jpegImageWriter.setOutput(imageOut); + IIOImage ioImage = new IIOImage(image, null, null); + jpegImageWriter.write(null, ioImage, iwp); + } + + public void dispose() { + jpegImageWriter.dispose(); + jpegImageWriter = null; + } + + public TileImageType getType() { + return TileImageType.JPG; + } + + public static boolean performOpenJDKJpegTest() { + try { + TileImageJpegDataWriter writer = new TileImageJpegDataWriter(0.99d); + writer.initialize(); + OutputStream out = new NullOutputStream(); + BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); + writer.processImage(image, out); + return true; + } catch (Exception e) { + log.debug("Jpeg test failed", e); + return false; + } + } +} diff --git a/src/main/java/mobac/program/tiledatawriter/TileImagePng4DataWriter.java b/src/main/java/mobac/program/tiledatawriter/TileImagePng4DataWriter.java new file mode 100644 index 0000000..0439b32 --- /dev/null +++ b/src/main/java/mobac/program/tiledatawriter/TileImagePng4DataWriter.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tiledatawriter; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; + +import mobac.optional.JavaAdvancedImaging; +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageType; +import mobac.utilities.imageio.Png4BitWriter; + +public class TileImagePng4DataWriter implements TileImageDataWriter { + + public TileImagePng4DataWriter() { + } + + public void initialize() { + } + + public void processImage(BufferedImage image, OutputStream out) throws IOException { + BufferedImage image2 = JavaAdvancedImaging.colorReduceMedianCut(image, 16); + Png4BitWriter.writeImage(out, image2); + } + + public void dispose() { + } + + public TileImageType getType() { + return TileImageType.PNG; + } +} diff --git a/src/main/java/mobac/program/tiledatawriter/TileImagePng8DataWriter.java b/src/main/java/mobac/program/tiledatawriter/TileImagePng8DataWriter.java new file mode 100644 index 0000000..6e6e677 --- /dev/null +++ b/src/main/java/mobac/program/tiledatawriter/TileImagePng8DataWriter.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tiledatawriter; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; + +import mobac.optional.JavaAdvancedImaging; + +public class TileImagePng8DataWriter extends TileImagePngDataWriter { + + public TileImagePng8DataWriter() { + } + + @Override + public void processImage(BufferedImage image, OutputStream out) throws IOException { + BufferedImage image2 = JavaAdvancedImaging.colorReduceMedianCut(image, 256); + super.processImage(image2, out); + } + +} diff --git a/src/main/java/mobac/program/tiledatawriter/TileImagePngDataWriter.java b/src/main/java/mobac/program/tiledatawriter/TileImagePngDataWriter.java new file mode 100644 index 0000000..ffc0f52 --- /dev/null +++ b/src/main/java/mobac/program/tiledatawriter/TileImagePngDataWriter.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tiledatawriter; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; + +import mobac.program.interfaces.TileImageDataWriter; +import mobac.program.model.TileImageType; + +import org.apache.log4j.Logger; + +public class TileImagePngDataWriter implements TileImageDataWriter { + + protected Logger log; + + protected ImageWriter pngImageWriter = null; + + public TileImagePngDataWriter() { + log = Logger.getLogger(this.getClass()); + } + + public void initialize() { + if (log.isTraceEnabled()) { + String s = "Available PNG image writers:"; + Iterator writers = ImageIO.getImageWritersByFormatName("png"); + while (writers.hasNext()) { + ImageWriter w = writers.next(); + s += "\n\t" + w.getClass().getName(); + } + log.trace(s); + } + pngImageWriter = ImageIO.getImageWritersByFormatName("png").next(); + pngImageWriter.addIIOWriteWarningListener(ImageWriterWarningListener.INSTANCE); + log.debug("Used PNG image writer: " + pngImageWriter.getClass().getName()); + } + + public void processImage(BufferedImage image, OutputStream out) throws IOException { + pngImageWriter.setOutput(ImageIO.createImageOutputStream(out)); + IIOImage ioImage = new IIOImage(image, null, null); + pngImageWriter.write(ioImage); + } + + public void dispose() { + pngImageWriter.dispose(); + pngImageWriter = null; + } + + public TileImageType getType() { + return TileImageType.PNG; + } + +} diff --git a/src/main/java/mobac/program/tilefilter/DummyTileFilter.java b/src/main/java/mobac/program/tilefilter/DummyTileFilter.java new file mode 100644 index 0000000..63417f6 --- /dev/null +++ b/src/main/java/mobac/program/tilefilter/DummyTileFilter.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilefilter; + +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.TileFilter; + +/** + * Dummy tile filter - always returns true for any tile. + */ +public class DummyTileFilter implements TileFilter { + + public boolean testTile(int x, int y, int zoom, MapSource mapSource) { + return true; + } + +} diff --git a/src/main/java/mobac/program/tilefilter/PolygonTileFilter.java b/src/main/java/mobac/program/tilefilter/PolygonTileFilter.java new file mode 100644 index 0000000..1b20ecf --- /dev/null +++ b/src/main/java/mobac/program/tilefilter/PolygonTileFilter.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilefilter; + +import java.awt.Polygon; + +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.TileFilter; +import mobac.program.model.MapPolygon; + +public class PolygonTileFilter implements TileFilter { + + private final Polygon polygon; + private final int tileSize; + private final int polygonZoom; + + public PolygonTileFilter(MapPolygon map) { + this(map.getPolygon(), map.getZoom(), map.getMapSource()); + } + + public PolygonTileFilter(Polygon polygon, int polygonZoom, MapSource mapSource) { + super(); + this.polygon = polygon; + this.polygonZoom = polygonZoom; + this.tileSize = mapSource.getMapSpace().getTileSize(); + } + + public boolean testTile(int x, int y, int zoom, MapSource mapSource) { + if (polygonZoom != zoom) + throw new RuntimeException("Wrong zoom level!"); + int tileCoordinateX = x * tileSize; + int tileCoordinateY = y * tileSize; + return polygon.intersects(tileCoordinateX, tileCoordinateY, tileSize, tileSize); + } + +} diff --git a/src/main/java/mobac/program/tilestore/TileStore.java b/src/main/java/mobac/program/tilestore/TileStore.java new file mode 100644 index 0000000..943c761 --- /dev/null +++ b/src/main/java/mobac/program/tilestore/TileStore.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilestore; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.swing.JOptionPane; + +import mobac.exceptions.TileStoreException; +import mobac.program.DirectoryManager; +import mobac.program.interfaces.MapSource; +import mobac.program.model.Settings; +import mobac.program.tilestore.berkeleydb.BerkeleyDbTileStore; +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +public abstract class TileStore { + + protected static TileStore INSTANCE = null; + + protected Logger log; + + protected File tileStoreDir; + + public static synchronized void initialize() { + if (INSTANCE != null) + return; + try { + INSTANCE = new BerkeleyDbTileStore(); + } catch (TileStoreException e) { + String errMsg = I18nUtils.localizedStringForKey("msg_tile_store_access_conflict"); + JOptionPane.showMessageDialog(null, errMsg, + I18nUtils.localizedStringForKey("msg_tile_store_access_conflict_title"), JOptionPane.ERROR_MESSAGE); + System.exit(1); + } + } + + public static TileStore getInstance() { + return INSTANCE; + } + + protected TileStore() { + log = Logger.getLogger(this.getClass()); + String tileStorePath = Settings.getInstance().directories.tileStoreDirectory; + if (tileStorePath != null) + tileStoreDir = new File(tileStorePath); + else + tileStoreDir = DirectoryManager.tileStoreDir; + log.debug("Tile store path: " + tileStoreDir); + } + + public abstract void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource) throws IOException; + + public abstract void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource, + long timeLastModified, long timeExpires, String eTag) throws IOException; + + /** + * + * @param x + * @param y + * @param zoom + * @param mapSource + * @return + */ + public abstract TileStoreEntry getTile(int x, int y, int zoom, MapSource mapSource); + + public abstract boolean contains(int x, int y, int zoom, MapSource mapSource); + + public abstract void prepareTileStore(MapSource mapSource); + + public abstract void clearStore(String storeName); + + public abstract String[] getAllStoreNames(); + + /** + * Returns true if the tile store directory of the specified {@link MapSource} exists. + * + * @param mapSource + * @return + */ + public abstract boolean storeExists(MapSource mapSource); + + /** + * + * @param mapSourceName + * @return + * @throws InterruptedException + */ + public abstract TileStoreInfo getStoreInfo(String mapSourceName) throws InterruptedException; + + /** + * + * @param mapSource + * @param zoom + * @param tileNumMin + * @param tileNumMax + * @return + * @throws InterruptedException + */ + public abstract BufferedImage getCacheCoverage(MapSource mapSource, int zoom, Point tileNumMin, Point tileNumMax) + throws InterruptedException; + + public abstract void closeAll(); + + public abstract void putTile(TileStoreEntry tile, MapSource mapSource); + + public abstract TileStoreEntry createNewEntry(int x, int y, int zoom, byte[] data, long timeLastModified, + long timeExpires, String eTag); + + /** + * Creates a new {@link TileStoreEntry} that represents a missing tile in a sparse map source + * + * @param x + * @param y + * @param zoom + * @return + */ + public abstract TileStoreEntry createNewEmptyEntry(int x, int y, int zoom); +} diff --git a/src/main/java/mobac/program/tilestore/TileStoreEntry.java b/src/main/java/mobac/program/tilestore/TileStoreEntry.java new file mode 100644 index 0000000..281a0c1 --- /dev/null +++ b/src/main/java/mobac/program/tilestore/TileStoreEntry.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilestore; + +public interface TileStoreEntry { + + public int getX(); + + public int getY(); + + public int getZoom(); + + /** + * This function does never return a null value! + * + * @return tile data + */ + public byte[] getData(); + + /** + * The time and date in UTC when this map tile has been downloaded + * respectively has been checked the last time via HTTP If-None-Match, + * If-Modified-Since or a HTTP HEAD request. + * + * @return Time in UTC + */ + public long getTimeDownloaded(); + + public void update(long timeExpires); + + /** + * + * @return Last modification time in UTC or 0 if not supported + * by the server + */ + public long getTimeLastModified(); + + /** + * + * @return Expiration time in UTC or 0 if not supported by the + * server + */ + public long getTimeExpires(); + + /** + * The eTag contained in the HTTP answer on the last download/check request. + * + * @return eTag or null if not supported by the server + */ + public String geteTag(); + +} diff --git a/src/main/java/mobac/program/tilestore/TileStoreInfo.java b/src/main/java/mobac/program/tilestore/TileStoreInfo.java new file mode 100644 index 0000000..407e7f8 --- /dev/null +++ b/src/main/java/mobac/program/tilestore/TileStoreInfo.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilestore; + +public class TileStoreInfo { + + int tileCount; + long storeSize; + + public TileStoreInfo(long storeSize, int tileCount) { + super(); + this.storeSize = storeSize; + this.tileCount = tileCount; + } + + /** + * @return Number of tiles stored in the tile store + */ + public int getTileCount() { + return tileCount; + } + + /** + * @return store size in bytes + */ + public long getStoreSize() { + return storeSize; + } + +} diff --git a/src/main/java/mobac/program/tilestore/berkeleydb/BerkeleyDbTileStore.java b/src/main/java/mobac/program/tilestore/berkeleydb/BerkeleyDbTileStore.java new file mode 100644 index 0000000..178fc18 --- /dev/null +++ b/src/main/java/mobac/program/tilestore/berkeleydb/BerkeleyDbTileStore.java @@ -0,0 +1,645 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilestore.berkeleydb; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.WritableRaster; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import mobac.exceptions.TileStoreException; +import mobac.program.interfaces.MapSource; +import mobac.program.model.Settings; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreEntry; +import mobac.program.tilestore.TileStoreInfo; +import mobac.program.tilestore.berkeleydb.TileDbEntry.TileDbKey; +import mobac.utilities.GUIExceptionHandler; +import mobac.utilities.Utilities; +import mobac.utilities.file.DeleteFileFilter; +import mobac.utilities.file.DirInfoFileFilter; +import mobac.utilities.file.DirectoryFileFilter; + +import com.sleepycat.je.DatabaseException; +import com.sleepycat.je.Environment; +import com.sleepycat.je.EnvironmentConfig; +import com.sleepycat.je.EnvironmentLockedException; +import com.sleepycat.persist.EntityCursor; +import com.sleepycat.persist.EntityStore; +import com.sleepycat.persist.PrimaryIndex; +import com.sleepycat.persist.StoreConfig; +import com.sleepycat.persist.evolve.Mutations; +import com.sleepycat.persist.evolve.Renamer; + +/** + * The new database based tile store implementation. + */ +public class BerkeleyDbTileStore extends TileStore { + + /** + * Max count of tile stores opened + */ + private static final int MAX_CONCURRENT_ENVIRONMENTS = 5; + + private EnvironmentConfig envConfig; + + private Map tileDbMap; + + private FileLock tileStoreLock = null; + + private Mutations mutations; + + public BerkeleyDbTileStore() throws TileStoreException { + super(); + acquireTileStoreLock(); + tileDbMap = new TreeMap(); + + envConfig = new EnvironmentConfig(); + envConfig.setTransactional(false); + envConfig.setLocking(true); + envConfig.setExceptionListener(GUIExceptionHandler.getInstance()); + envConfig.setAllowCreate(true); + envConfig.setSharedCache(true); + envConfig.setCachePercent(50); + + mutations = new Mutations(); + + String oldPackage1 = "tac.tilestore.berkeleydb"; + String oldPackage2 = "tac.program.tilestore.berkeleydb"; + String entry = ".TileDbEntry"; + String key = ".TileDbEntry$TileDbKey"; + mutations.addRenamer(new Renamer(oldPackage1 + entry, 0, TileDbEntry.class.getName())); + mutations.addRenamer(new Renamer(oldPackage1 + key, 0, TileDbKey.class.getName())); + mutations.addRenamer(new Renamer(oldPackage1 + entry, 1, TileDbEntry.class.getName())); + mutations.addRenamer(new Renamer(oldPackage1 + key, 1, TileDbKey.class.getName())); + mutations.addRenamer(new Renamer(oldPackage2 + entry, 2, TileDbEntry.class.getName())); + mutations.addRenamer(new Renamer(oldPackage2 + key, 2, TileDbKey.class.getName())); + + // for (Renamer r : mutations.getRenamers()) + // log.debug(r.toString()); + Runtime.getRuntime().addShutdownHook(new ShutdownThread(true)); + } + + protected void acquireTileStoreLock() throws TileStoreException { + try { + // Get a file channel for the file + File file = new File(tileStoreDir, "lock"); + if (!tileStoreDir.isDirectory()) + try { + Utilities.mkDirs(tileStoreDir); + } catch (IOException e) { + throw new TileStoreException("Unable to create tile store directory: \"" + tileStoreDir.getPath() + + "\""); + } + FileChannel channel = new RandomAccessFile(file, "rw").getChannel(); + + // Use the file channel to create a lock on the file. + // This method blocks until it can retrieve the lock. + + // Try acquiring the lock without blocking. This method returns + // null or throws an exception if the file is already locked. + tileStoreLock = channel.tryLock(); + if (tileStoreLock == null) + throw new TileStoreException("Unable to obtain tile store lock - " + + "another instance of Mobile Atlas Creator is running!"); + + // // Release the lock + // lock.release(); + // + // // Close the file + // channel.close(); + } catch (Exception e) { + log.error("", e); + throw new TileStoreException(e.getMessage(), e.getCause()); + } + } + + @Override + public TileStoreEntry createNewEntry(int x, int y, int zoom, byte[] data, long timeLastModified, long timeExpires, + String eTag) { + return new TileDbEntry(x, y, zoom, data, timeLastModified, timeExpires, eTag); + } + + @Override + public TileStoreEntry createNewEmptyEntry(int x, int y, int zoom) { + long time = System.currentTimeMillis(); + long timeExpires = time + Settings.getInstance().tileDefaultExpirationTime; + // We set the tile data to an empty array because we can not store null + return new TileDbEntry(x, y, zoom, new byte[] {}, time, timeExpires, ""); + } + + private TileDatabase getTileDatabase(MapSource mapSource) throws DatabaseException { + TileDatabase db; + if (tileDbMap == null) + // Tile store has been closed already + return null; + String storeName = mapSource.getName(); + if (storeName == null) + return null; + synchronized (tileDbMap) { + db = tileDbMap.get(storeName); + } + if (db != null) + return db; + try { + synchronized (tileDbMap) { + cleanupDatabases(); + db = tileDbMap.get(storeName); + if (db == null) { + db = new TileDatabase(storeName); + db.lastAccess = System.currentTimeMillis(); + tileDbMap.put(mapSource.getName(), db); + } + return db; + } + } catch (Exception e) { + log.error("Error creating tile store db \"" + mapSource.getName() + "\"", e); + throw new TileStoreException(e); + } + } + + private TileDatabase getTileDatabase(String storeName) throws DatabaseException { + TileDatabase db; + if (tileDbMap == null) + // Tile store has been closed already + return null; + if (storeName == null) + return null; + synchronized (tileDbMap) { + db = tileDbMap.get(storeName); + } + if (db != null) + return db; + try { + synchronized (tileDbMap) { + cleanupDatabases(); + db = tileDbMap.get(storeName); + if (db == null) { + db = new TileDatabase(storeName); + db.lastAccess = System.currentTimeMillis(); + tileDbMap.put(storeName, db); + } + return db; + } + } catch (Exception e) { + log.error("Error creating tile store db \"" + storeName + "\"", e); + throw new TileStoreException(e); + } + } + + @Override + public TileStoreInfo getStoreInfo(String storeName) throws InterruptedException { + int tileCount = getNrOfTiles(storeName); + long storeSize = getStoreSize(storeName); + return new TileStoreInfo(storeSize, tileCount); + } + + @Override + public void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource) throws IOException { + this.putTileData(tileData, x, y, zoom, mapSource, -1, -1, null); + } + + @Override + public void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource, long timeLastModified, + long timeExpires, String eTag) throws IOException { + TileDbEntry tile = new TileDbEntry(x, y, zoom, tileData, timeLastModified, timeExpires, eTag); + TileDatabase db = null; + try { + if (log.isTraceEnabled()) + log.trace("Saved " + mapSource.getName() + " " + tile); + db = getTileDatabase(mapSource); + if (db != null) + db.put(tile); + } catch (Exception e) { + if (db != null) + db.close(); + log.error("Faild to write tile to tile store \"" + mapSource.getName() + "\"", e); + } + } + + @Override + public void putTile(TileStoreEntry tile, MapSource mapSource) { + TileDatabase db = null; + try { + if (log.isTraceEnabled()) + log.trace("Saved " + mapSource.getName() + " " + tile); + db = getTileDatabase(mapSource); + db.put((TileDbEntry) tile); + } catch (Exception e) { + if (db != null) + db.close(); + log.error("Faild to write tile to tile store \"" + mapSource.getName() + "\"", e); + } + } + + @Override + public TileStoreEntry getTile(int x, int y, int zoom, MapSource mapSource) { + TileDatabase db = null; + try { + db = getTileDatabase(mapSource); + if (db == null) + return null; + TileStoreEntry tile = db.get(new TileDbKey(x, y, zoom)); + if (log.isTraceEnabled()) { + if (tile == null) + log.trace("Tile store cache miss: (x,y,z)" + x + "/" + y + "/" + zoom + " " + mapSource.getName()); + else + log.trace("Loaded " + mapSource.getName() + " " + tile); + } + return tile; + } catch (Exception e) { + if (db != null) + db.close(); + log.error("failed to retrieve tile from tile store \"" + mapSource.getName() + "\"", e); + return null; + } + } + + public boolean contains(int x, int y, int zoom, MapSource mapSource) { + try { + return getTileDatabase(mapSource).contains(new TileDbKey(x, y, zoom)); + } catch (DatabaseException e) { + log.error("", e); + return false; + } + } + + public void prepareTileStore(MapSource mapSource) { + try { + getTileDatabase(mapSource); + } catch (DatabaseException e) { + } + } + + public void clearStore(String storeName) { + File databaseDir = getStoreDir(storeName); + + TileDatabase db; + synchronized (tileDbMap) { + db = tileDbMap.get(storeName); + if (db != null) + db.close(false); + if (databaseDir.exists()) { + DeleteFileFilter dff = new DeleteFileFilter(); + databaseDir.listFiles(dff); + databaseDir.delete(); + log.debug("Tilestore " + storeName + " cleared: " + dff); + } + tileDbMap.remove(storeName); + } + } + + /** + * This method returns the amount of tiles in the store of tiles which is specified by the {@link MapSource} object. + * + * @param mapSourceName + * the store to calculate number of tiles in + * @return the amount of tiles in the specified store. + * @throws InterruptedException + */ + public int getNrOfTiles(String mapSourceName) throws InterruptedException { + try { + File storeDir = getStoreDir(mapSourceName); + if (!storeDir.isDirectory()) + return 0; + TileDatabase db = getTileDatabase(mapSourceName); + int tileCount = (int) db.entryCount(); + db.close(); + return tileCount; + } catch (DatabaseException e) { + log.error("", e); + return -1; + } + } + + public long getStoreSize(String storeName) throws InterruptedException { + File tileStore = getStoreDir(storeName); + if (tileStore.exists()) { + DirInfoFileFilter diff = new DirInfoFileFilter(); + try { + tileStore.listFiles(diff); + } catch (RuntimeException e) { + throw new InterruptedException(); + } + return diff.getDirSize(); + } else { + return 0; + } + } + + public BufferedImage getCacheCoverage(MapSource mapSource, int zoom, Point tileNumMin, Point tileNumMax) + throws InterruptedException { + TileDatabase db; + try { + db = getTileDatabase(mapSource); + return db.getCacheCoverage(zoom, tileNumMin, tileNumMax); + } catch (DatabaseException e) { + log.error("", e); + return null; + } + } + + protected void cleanupDatabases() { + if (tileDbMap.size() < MAX_CONCURRENT_ENVIRONMENTS) + return; + synchronized (tileDbMap) { + List list = new ArrayList(tileDbMap.values()); + Collections.sort(list, new Comparator() { + + public int compare(TileDatabase o1, TileDatabase o2) { + if (o1.lastAccess == o2.lastAccess) + return 0; + return (o1.lastAccess < o2.lastAccess) ? -1 : 1; + } + }); + for (int i = 0; i < list.size() - 2; i++) + list.get(i).close(); + } + } + + public void closeAll() { + Thread t = new ShutdownThread(false); + t.start(); + try { + t.join(); + } catch (InterruptedException e) { + log.error("", e); + } + } + + /** + * Returns true if the tile store directory of the specified {@link MapSource} exists. + * + * @param mapSource + * @return + */ + public boolean storeExists(MapSource mapSource) { + File tileStore = getStoreDir(mapSource); + return (tileStore.isDirectory()) && (tileStore.exists()); + } + + /** + * Returns the directory used for storing the tile database of the {@link MapSource} specified by + * mapSource + * + * @param mapSource + * @return + */ + protected File getStoreDir(MapSource mapSource) { + return getStoreDir(mapSource.getName()); + } + + /** + * @param mapSourceName + * @return directory used for storing the tile database belonging to mapSource + */ + protected File getStoreDir(String mapSourceName) { + return new File(tileStoreDir, "db-" + mapSourceName); + } + + public String[] getAllStoreNames() { + File[] dirs = tileStoreDir.listFiles(new DirectoryFileFilter()); + ArrayList storeNames = new ArrayList(dirs.length); + for (File d : dirs) { + String name = d.getName(); + if (name.startsWith("db-")) { + name = name.substring(3); + storeNames.add(name); + } + } + String[] result = new String[storeNames.size()]; + storeNames.toArray(result); + return result; + } + + private class ShutdownThread extends DelayedInterruptThread { + + private final boolean shutdown; + + public ShutdownThread(boolean shutdown) { + super("DBShutdown"); + this.shutdown = shutdown; + } + + @Override + public void run() { + log.debug("Closing all tile databases..."); + synchronized (tileDbMap) { + for (TileDatabase db : tileDbMap.values()) { + db.close(false); + } + tileDbMap.clear(); + if (shutdown) { + tileDbMap = null; + try { + tileStoreLock.release(); + } catch (IOException e) { + log.error("", e); + } + } + } + log.debug("All tile databases has been closed"); + } + } + + protected class TileDatabase { + + final String mapSourceName; + final Environment env; + final EntityStore store; + final PrimaryIndex tileIndex; + boolean dbClosed = false; + + long lastAccess; + + public TileDatabase(String mapSourceName) throws IOException, EnvironmentLockedException, DatabaseException { + this(mapSourceName, getStoreDir(mapSourceName)); + } + + public TileDatabase(String mapSourceName, File databaseDirectory) throws IOException, + EnvironmentLockedException, DatabaseException { + log.debug("Opening tile store db: \"" + databaseDirectory + "\""); + File storeDir = databaseDirectory; + DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); + try { + t.pauseInterrupt(); + this.mapSourceName = mapSourceName; + lastAccess = System.currentTimeMillis(); + + Utilities.mkDirs(storeDir); + + env = new Environment(storeDir, envConfig); + + StoreConfig storeConfig = new StoreConfig(); + storeConfig.setAllowCreate(true); + storeConfig.setTransactional(false); + storeConfig.setMutations(mutations); + store = new EntityStore(env, "TilesEntityStore", storeConfig); + + tileIndex = store.getPrimaryIndex(TileDbKey.class, TileDbEntry.class); + } finally { + if (t.interruptedWhilePaused()) + close(); + t.resumeInterrupt(); + } + } + + public boolean isClosed() { + return dbClosed; + } + + public long entryCount() throws DatabaseException { + return tileIndex.count(); + } + + public void put(TileDbEntry tile) throws DatabaseException { + DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); + try { + t.pauseInterrupt(); + tileIndex.put(tile); + } finally { + if (t.interruptedWhilePaused()) + close(); + t.resumeInterrupt(); + } + } + + public boolean contains(TileDbKey key) throws DatabaseException { + return tileIndex.contains(key); + } + + public TileDbEntry get(TileDbKey key) throws DatabaseException { + return tileIndex.get(key); + } + + public PrimaryIndex getTileIndex() { + return tileIndex; + } + + public BufferedImage getCacheCoverage(int zoom, Point tileNumMin, Point tileNumMax) throws DatabaseException, + InterruptedException { + log.debug("Loading cache coverage for region " + tileNumMin + " " + tileNumMax + " of zoom level " + zoom); + DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); + int width = tileNumMax.x - tileNumMin.x + 1; + int height = tileNumMax.y - tileNumMin.y + 1; + byte ff = (byte) 0xFF; + byte[] colors = new byte[] { 120, 120, 120, 120, // alpha-gray + 10, ff, 0, 120 // alpha-green + }; + IndexColorModel colorModel = new IndexColorModel(2, 2, colors, 0, true); + BufferedImage image = null; + try { + image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel); + } catch (Throwable e) { + log.error("Failed to create coverage image: " + e.toString()); + image = null; + System.gc(); + return null; + } + WritableRaster raster = image.getRaster(); + + // We are loading the coverage of the selected area column by column which is much faster than loading the + // whole region at once + for (int x = tileNumMin.x; x <= tileNumMax.x; x++) { + TileDbKey fromKey = new TileDbKey(x, tileNumMin.y, zoom); + TileDbKey toKey = new TileDbKey(x, tileNumMax.y, zoom); + EntityCursor cursor = tileIndex.keys(fromKey, true, toKey, true); + try { + TileDbKey key = cursor.next(); + while (key != null) { + int pixelx = key.x - tileNumMin.x; + int pixely = key.y - tileNumMin.y; + raster.setSample(pixelx, pixely, 0, 1); + key = cursor.next(); + if (t.isInterrupted()) { + log.debug("Cache coverage loading aborted"); + throw new InterruptedException(); + } + } + } finally { + cursor.close(); + } + } + return image; + } + + protected void purge() { + try { + store.sync(); + env.cleanLog(); + } catch (DatabaseException e) { + log.error("database compression failed: ", e); + } + } + + public void close() { + close(true); + } + + public void close(boolean removeFromMap) { + if (dbClosed) + return; + if (removeFromMap) { + synchronized (tileDbMap) { + TileDatabase db2 = tileDbMap.get(mapSourceName); + if (db2 == this) + tileDbMap.remove(mapSourceName); + } + } + DelayedInterruptThread t = (DelayedInterruptThread) Thread.currentThread(); + try { + t.pauseInterrupt(); + try { + log.debug("Closing tile store db \"" + mapSourceName + "\""); + if (store != null) + store.close(); + } catch (Exception e) { + log.error("", e); + } + try { + env.close(); + } catch (Exception e) { + log.error("", e); + } finally { + dbClosed = true; + } + } finally { + if (t.interruptedWhilePaused()) + close(); + t.resumeInterrupt(); + } + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + + } +} diff --git a/src/main/java/mobac/program/tilestore/berkeleydb/DelayedInterruptThread.java b/src/main/java/mobac/program/tilestore/berkeleydb/DelayedInterruptThread.java new file mode 100644 index 0000000..b6473d4 --- /dev/null +++ b/src/main/java/mobac/program/tilestore/berkeleydb/DelayedInterruptThread.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilestore.berkeleydb; + +import java.util.concurrent.ThreadFactory; + +/** + * The Berkeley DB has some problems when someone interrupts the thread that is currently performing IO activity. + * Therefore before executing any DB we allow to disable the {@link #interrupt()} method via {@link #pauseInterrupt()}. + * After the "interrupt sensitive section" {@link #resumeInterrupt()} restores the regular behavior. If the thread has + * been interrupted while interrupt was disabled {@link #resumeInterrupt()} catches up this. + */ +public class DelayedInterruptThread extends Thread { + + private boolean interruptPaused = false; + private boolean interruptedWhilePaused = false; + + public DelayedInterruptThread(String name) { + super(name); + } + + public DelayedInterruptThread(Runnable target) { + super(target); + } + + public DelayedInterruptThread(Runnable target, String name) { + super(target, name); + } + + @Override + public void interrupt() { + if (interruptPaused) + interruptedWhilePaused = true; + else + super.interrupt(); + } + + public void pauseInterrupt() { + interruptPaused = true; + } + + public void resumeInterrupt() { + interruptPaused = false; + if (interruptedWhilePaused) + this.interrupt(); + } + + public boolean interruptedWhilePaused() { + return interruptedWhilePaused; + } + + public static ThreadFactory createThreadFactory() { + return new DIThreadFactory(); + } + + private static class DIThreadFactory implements ThreadFactory { + + public Thread newThread(Runnable r) { + return new DelayedInterruptThread(r); + } + + } +} diff --git a/src/main/java/mobac/program/tilestore/berkeleydb/TileDbEntry.java b/src/main/java/mobac/program/tilestore/berkeleydb/TileDbEntry.java new file mode 100644 index 0000000..a1172b5 --- /dev/null +++ b/src/main/java/mobac/program/tilestore/berkeleydb/TileDbEntry.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.program.tilestore.berkeleydb; + +import java.util.Date; + +import mobac.program.tilestore.TileStoreEntry; + +import com.sleepycat.persist.model.Entity; +import com.sleepycat.persist.model.KeyField; +import com.sleepycat.persist.model.Persistent; +import com.sleepycat.persist.model.PrimaryKey; + +@Entity(version = 3) +public class TileDbEntry implements TileStoreEntry { + + @PrimaryKey + protected TileDbKey tileKey; + + private byte[] data; + private String eTag = null; + + private long timeDownloaded; + + private long timeLastModified; + private long timeExpires; + + protected TileDbEntry() { + // required for deserialization + } + + public TileDbEntry(int x, int y, int zoom, byte[] data) { + tileKey = new TileDbKey(x, y, zoom); + if (data == null) + throw new NullPointerException("Tile data can not be null!"); + this.data = data; + this.timeDownloaded = System.currentTimeMillis(); + } + + public TileDbEntry(int x, int y, int zoom, byte[] data, long timeLastModified, long timeExpires, String eTag) { + this(x, y, zoom, data); + this.timeLastModified = timeLastModified; + this.timeExpires = timeExpires; + this.eTag = eTag; + } + + public void update(long timeExpires) { + timeDownloaded = System.currentTimeMillis(); + this.timeExpires = timeExpires; + } + + public int getX() { + return tileKey.x; + } + + public int getY() { + return tileKey.y; + } + + public int getZoom() { + return tileKey.zoom; + } + + public byte[] getData() { + return data; + } + + public String geteTag() { + return eTag; + } + + public long getTimeLastModified() { + return timeLastModified; + } + + public long getTimeDownloaded() { + return timeDownloaded; + } + + public long getTimeExpires() { + return timeExpires; + } + + public String shortInfo() { + return String.format("Tile z%d/%d/%d", tileKey.zoom, tileKey.x, tileKey.y); + } + + @Override + public String toString() { + String tlm = (timeLastModified <= 0) ? "-" : new Date(timeLastModified).toString(); + String txp = (timeExpires <= 0) ? "-" : new Date(timeExpires).toString(); + return String.format("Tile z%d/%d/%d dl[%s] lm[%s] exp[%s] eTag[%s]", tileKey.zoom, tileKey.x, tileKey.y, + new Date(timeDownloaded), tlm, txp, eTag); + } + + @Persistent(version = 3) + public static class TileDbKey { + + @KeyField(1) + public int zoom; + + @KeyField(2) + public int x; + + @KeyField(3) + public int y; + + protected TileDbKey() { + } + + public TileDbKey(int x, int y, int zoom) { + super(); + this.x = x; + this.y = y; + this.zoom = zoom; + } + + @Override + public String toString() { + return "[x=" + x + ", y=" + y + ", zoom=" + zoom + "]"; + } + + } + +} diff --git a/src/main/java/mobac/utilities/Charsets.java b/src/main/java/mobac/utilities/Charsets.java new file mode 100644 index 0000000..b3374b1 --- /dev/null +++ b/src/main/java/mobac/utilities/Charsets.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.nio.charset.Charset; + +public class Charsets { + + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + +} diff --git a/src/main/java/mobac/utilities/ExtensionClassLoader.java b/src/main/java/mobac/utilities/ExtensionClassLoader.java new file mode 100644 index 0000000..facebe0 --- /dev/null +++ b/src/main/java/mobac/utilities/ExtensionClassLoader.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import mobac.utilities.file.RegexFileFilter; + +import org.apache.log4j.Logger; + + +/** + * Allows to load JAR files at runtime including their JNI libraries. + */ +public class ExtensionClassLoader extends URLClassLoader { + + private static final Logger log = Logger.getLogger(ExtensionClassLoader.class); + private File jarDir; + + private ExtensionClassLoader(URL[] urls, File jarDir) throws MalformedURLException { + super(urls); + this.jarDir = jarDir; + } + + /** + * Creates an {@link ExtensionClassLoader} for loading JARs at runtime that + * have JNI dependencies. + * + * @param dirList + * list of directories to search in. The first directory + * containing at least one jar matching the + * regexFilePattern is taken. The other directories + * are ignored. + * @param regexFilePattern + * jar/jni regex search pattern + * @return The {@link ClassLoader} that is able to load classes from the + * loaded JARs and the correspondant JNI libraries + * @throws MalformedURLException + * @throws FileNotFoundException + */ + public static ExtensionClassLoader create(File[] dirList, String regexFilePattern) + throws FileNotFoundException { + File[] jarFiles = null; + File jarDir = null; + for (File dir : dirList) { + if (dir == null || !dir.isDirectory()) + continue; + File[] files = dir.listFiles(new RegexFileFilter(regexFilePattern)); + if (files.length > 0) { + log.debug("Directory: \"" + dir.getAbsolutePath() + "\""); + log.debug("Pattern: \"" + regexFilePattern + "\""); + jarFiles = files; + jarDir = dir; + break; + } + } + if (jarFiles == null) + throw new FileNotFoundException("No directory containing \"" + regexFilePattern + + "\" found."); + final URL[] urls = new URL[jarFiles.length]; + for (int i = 0; i < urls.length; i++) { + try { + urls[i] = new URL("jar", "", "file:" + jarFiles[i].getAbsolutePath() + "!/"); + } catch (MalformedURLException e) { + log.error("", e); + } + } + final File jarDir_ = jarDir; + ExtensionClassLoader ecl = AccessController + .doPrivileged(new PrivilegedAction() { + public ExtensionClassLoader run() { + try { + return new ExtensionClassLoader(urls, jarDir_); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + }); + return ecl; + } + + @Override + protected String findLibrary(String libname) { + String mappedLibname = System.mapLibraryName(libname); + File f = new File(jarDir, mappedLibname); + if (f.isFile()) + return f.getAbsolutePath(); + else + return null; + } + +} diff --git a/src/main/java/mobac/utilities/GBC.java b/src/main/java/mobac/utilities/GBC.java new file mode 100644 index 0000000..209463a --- /dev/null +++ b/src/main/java/mobac/utilities/GBC.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +// License: GPL. Copyright 2007 by Immanuel Scholz and others +package mobac.utilities; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.Insets; + +import javax.swing.Box; + +/** + * A wrapper for GridBagConstraints which has sane default static creators and + * member functions to chain calling. + * + * @author imi + */ +public class GBC extends GridBagConstraints { + + private static final long serialVersionUID = 1L; + + /** + * Use public static creator functions to create an GBC. + */ + private GBC() { + } + + /** + * Create a standard constraint (which is not the last). + * + * @return A standard constraint with no filling. + */ + public static GBC std() { + GBC c = new GBC(); + c.anchor = WEST; + return c; + } + + /** + * Create the constraint for the last elements on a line. + * + * @return A constraint which indicates the last item on a line. + */ + public static GBC eol() { + GBC c = std(); + c.gridwidth = REMAINDER; + return c; + } + + /** + * Create the constraint for the last elements on a line and on a paragraph. + * This is merely a shortcut for eol().insets(0,0,0,10) + * + * @return A constraint which indicates the last item on a line. + */ + public static GBC eop() { + return eol().insets(0, 0, 0, 10); + } + + /** + * Try to fill both, horizontal and vertical + * + * @return This constraint for chaining. + */ + public GBC fill() { + return fill(BOTH); + } + + public GBC toggleEol() { + if (gridwidth == GBC.REMAINDER) + gridwidth = 1; + else + gridwidth = GBC.REMAINDER; + return this; + } + + public GBC gridwidth(int value) { + gridwidth = value; + return this; + } + + public GBC gridheight(int value) { + gridheight = value; + return this; + } + + public GBC gridx(int value) { + gridx = value; + return this; + } + + public GBC gridxy(int x, int y) { + gridx = x; + gridy = y; + return this; + } + + public GBC gridy(int value) { + gridy = value; + return this; + } + + /** + * Set fill to the given value + * + * @param value + * The filling value, either NONE, HORIZONTAL, VERTICAL or BOTH + * @return This constraint for chaining. + */ + public GBC fill(int value) { + fill = value; + if (value == HORIZONTAL || value == BOTH) + weightx = 1.0; + if (value == VERTICAL || value == BOTH) + weighty = 1.0; + return this; + } + + public GBC fillH() { + return fill(GBC.HORIZONTAL); + } + + /** + * Set the anchor of this GBC to a. + * + * @param a + * The new anchor, e.g. GBC.CENTER or GBC.EAST. + * @return This constraint for chaining. + */ + public GBC anchor(int a) { + anchor = a; + return this; + } + + /** + * Adds insets to this GBC. + * + * @param left + * The left space of the insets + * @param top + * The top space of the insets + * @param right + * The right space of the insets + * @param bottom + * The bottom space of the insets + * @return This constraint for chaining. + */ + public GBC insets(int left, int top, int right, int bottom) { + insets = new Insets(top, left, bottom, right); + return this; + } + + /** + * This is a helper to easily create a glue with a minimum default value. + * + * @param x + * If higher than 0, this will be a horizontal glue with x as + * minimum horizontal strut. + * @param y + * If higher than 0, this will be a vertical glue with y as + * minimum vertical strut. + */ + public static Component glue(int x, int y) { + short maxx = x > 0 ? Short.MAX_VALUE : 0; + short maxy = y > 0 ? Short.MAX_VALUE : 0; + return new Box.Filler(new Dimension(x, y), new Dimension(x, y), new Dimension(maxx, maxy)); + } +} diff --git a/src/main/java/mobac/utilities/GBCTable.java b/src/main/java/mobac/utilities/GBCTable.java new file mode 100644 index 0000000..ec6a36c --- /dev/null +++ b/src/main/java/mobac/utilities/GBCTable.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.awt.Insets; + +public class GBCTable { + + public static final int DEFAULT_ROW_SPACING = 4, DEFAULT_COL_SPACING = 8; + + private final int colSpacing, rowSpacing; + private final GBC gbc = GBC.std(); + + private int x, y, yBegin; + + public GBCTable() { + this(DEFAULT_ROW_SPACING, DEFAULT_COL_SPACING); + } + + public GBCTable(int spacing) { + this(spacing, spacing); + } + + public GBCTable(int rowSpacing, int colSpacing) { + this.rowSpacing = rowSpacing; + this.colSpacing = colSpacing; + begin(); + } + + public GBC begin() { + return begin(1, 1); + } + + public GBC begin(int x, int y) { + this.x = x; + this.y = yBegin = y; + reset(); + return gbc; + } + + public GBC incX() { + return nextCol(1); + } + + public GBC nextCol(int x) { + this.x += x; + y = yBegin; + reset(); + return gbc; + } + + public GBC incY() { + return nextRow(1); + } + + public GBC nextRow(int y) { + this.y += y; + reset(); + return gbc; + } + + /** + * Ensures the {@link gbc} object was not modified. Reseting the values is cheaper than creating a new object. + */ + private void reset() { + gbc.gridwidth = gbc.gridheight = 1; + gbc.weightx = gbc.weighty = 0d; + gbc.fill = GBC.HORIZONTAL; + gbc.anchor = GBC.WEST; + if (gbc.insets == null) { + gbc.insets = new Insets(0, 0, 0, 0); + } else { + gbc.insets.top = gbc.insets.bottom = gbc.insets.left = gbc.insets.right = 0; + } + if ((gbc.gridx = x) > 1) { + gbc.insets.left = colSpacing; + } + if ((gbc.gridy = y) > 1) { + gbc.insets.top = rowSpacing; + } + } + +} diff --git a/src/main/java/mobac/utilities/GUIExceptionHandler.java b/src/main/java/mobac/utilities/GUIExceptionHandler.java new file mode 100644 index 0000000..07744b4 --- /dev/null +++ b/src/main/java/mobac/utilities/GUIExceptionHandler.java @@ -0,0 +1,354 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.awt.AWTEvent; +import java.awt.BorderLayout; +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.swing.JCheckBox; +import javax.swing.JEditorPane; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.UIManager; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; + +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.interfaces.ExceptionExtendedInfo; + +import org.apache.log4j.Logger; + +import com.sleepycat.je.ExceptionEvent; +import com.sleepycat.je.ExceptionListener; + +public class GUIExceptionHandler implements Thread.UncaughtExceptionHandler, ExceptionListener { + + private static final GUIExceptionHandler instance = new GUIExceptionHandler(); + + private static final Logger log = Logger.getLogger(GUIExceptionHandler.class); + + private static final double MB_DIV = 1024d * 1024d; + + private static final String URL_BUGTRACKER = "http://sourceforge.net/p/mobac/bugs/"; + + static { + Thread.setDefaultUncaughtExceptionHandler(instance); + } + + public static void registerForCurrentThread() { + Thread t = Thread.currentThread(); + log.trace("Registering MOBAC exception handler for thread \"" + t.getName() + "\" [" + t.getId() + "]"); + t.setUncaughtExceptionHandler(instance); + } + + public static GUIExceptionHandler getInstance() { + return instance; + } + + private GUIExceptionHandler() { + super(); + } + + /** + * Implementation for {@link com.sleepycat.je.ExceptionListener} + */ + public void exceptionThrown(ExceptionEvent paramExceptionEvent) { + Exception e = paramExceptionEvent.getException(); + log.error("Exception in tile store: " + paramExceptionEvent.toString(), e); + showExceptionDialog(e); + } + + /** + * Implementation for {@link Thread.UncaughtExceptionHandler} + */ + public void uncaughtException(Thread t, Throwable e) { + processException(t, e); + } + + public static void processException(Throwable e) { + processException(Thread.currentThread(), e); + } + + public static void processFatalExceptionSimpleDialog(String dialogMessage, Throwable e) { + log.error(dialogMessage + ": " + e.getMessage(), e); + String[] options = { I18nUtils.localizedStringForKey("Exit"), + I18nUtils.localizedStringForKey("dlg_download_show_error_report") }; + int a = JOptionPane.showOptionDialog(null, dialogMessage, + I18nUtils.localizedStringForKey("Error"), 0, JOptionPane.ERROR_MESSAGE, null, options, + options[0]); + if (a == 1) + GUIExceptionHandler.showExceptionDialog(e); + System.exit(1); + } + + public static void processException(Thread thread, Throwable t) { + log.error("Uncaught exception: ", t); + showExceptionDialog(thread, t, null); + } + + public static void processException(Thread thread, Throwable t, AWTEvent newEvent) { + String eventText = newEvent.toString(); + log.error("Uncaught exception on processing event " + eventText, t); + if (eventText.length() > 100) { + String[] parts = eventText.split(","); + StringWriter sw = new StringWriter(eventText.length() + 20); + sw.write(parts[0]); + int len = parts[0].length(); + for (int i = 1; i < parts.length; i++) { + String s = parts[i]; + if (s.length() + len > 80) { + sw.write("\n\t"); + len = 0; + } + sw.write(s); + } + eventText = "Event: " + sw.toString(); + } + showExceptionDialog(thread, t, eventText); + } + + public static String prop(String key) { + String s = System.getProperty(key); + if (s != null) + return s; + else + return ""; + } + + public static void showExceptionDialog(Throwable t) { + showExceptionDialog(null, Thread.currentThread(), t, null); + } + + public static void showExceptionDialog(String message, Throwable t) { + showExceptionDialog(message, Thread.currentThread(), t, null); + } + + public static void showExceptionDialog(Thread thread, Throwable t, String additionalInfo) { + showExceptionDialog(null, thread, t, additionalInfo); + } + + public static synchronized void showExceptionDialog(String message, Thread thread, Throwable t, + String additionalInfo) { + try { + if (t != null) { + // Recursive check - in case the Exception was thrown in a Component.paint() method + // this may lead to an infinite recursion with one message dialog per recursion loop + String thisClassName = GUIExceptionHandler.class.getName(); + for (StackTraceElement ste : t.getStackTrace()) { + if (ste.getClassName().startsWith(thisClassName) + && "showExceptionDialog".equals(ste.getMethodName())) { + log.error("Recursive error loop detected - aborting"); + return; + } + } + } + + StringBuilder sb = new StringBuilder(2048); + if (message != null) + sb.append("Error message: " + message + "\n"); + sb.append("Version: " + ProgramInfo.getCompleteTitle()); + sb.append("\nPlatform: " + prop("os.name") + " (" + prop("os.version") + ")"); + String windowManager = System.getProperty("sun.desktop"); + if (windowManager != null) + sb.append(" (" + windowManager + ")"); + + String dist = OSUtilities.getLinuxDistributionName(); + if (dist != null) + sb.append("\nDistribution name: " + dist); + + sb.append("\nJava VM: " + prop("java.vm.name") + " (" + prop("java.runtime.version") + ")"); + Runtime r = Runtime.getRuntime(); + sb.append(String.format("\nMax heap size: %3.2f MiB", r.maxMemory() / MB_DIV)); + // sb.append("\nMapsources rev: " + MapSourcesUpdater.getCurrentMapSourcesRev()); + + sb.append("\nCPU cores: " + Runtime.getRuntime().availableProcessors()); + + if (thread != null) + sb.append("\n\nThread: " + thread.getName()); + + if (additionalInfo != null) + sb.append("\n\n" + additionalInfo); + + if (t instanceof ExceptionExtendedInfo) { + ExceptionExtendedInfo ei = (ExceptionExtendedInfo) t; + sb.append("\n"); + sb.append(ei.getExtendedInfo()); + } + + String guiText; + String dialogTitle; + JPanel panel = new JPanel(new BorderLayout()); + if (t != null) { + sb.append("\n\nError hierarchy:"); + Throwable tmp = t; + while (tmp != null) { + sb.append("\n " + tmp.getClass().getSimpleName() + ": " + tmp.getMessage()); + tmp = tmp.getCause(); + } + + String exceptionName = t.getClass().getSimpleName(); + dialogTitle = "Unexpected Exception: " + exceptionName; + StringWriter stack = new StringWriter(); + t.printStackTrace(new PrintWriter(stack)); + sb.append("\n\n#############################################################\n\n"); + sb.append(stack.getBuffer().toString()); + sb.append("\n#############################################################"); + + guiText = "An unexpected exception occurred (" + exceptionName + ")
" + + "

Please create a ticket in the bug tracker " + "on SourceForge.net
" + + "Please include a detailed description of your performed actions
" + + "before the error occurred.

Be sure to include the following information:"; + } else { + dialogTitle = "System report"; + guiText = "This is a user generated system report.
" + + "

It can be used for providing detailed version information
" + + "in case a new ticket in the bug tracker is created " + "on SourceForge.net
" + + "Please include a detailed description of your problem
" + + "and what steps are necessary to reproduce the problem.
" + "

"; + } + JEditorPane text = new JEditorPane("text/html", ""); + text.setOpaque(true); + text.setBackground(UIManager.getColor("JFrame.background")); + text.setEditable(false); + text.addHyperlinkListener(new HyperlinkListener() { + + public void hyperlinkUpdate(HyperlinkEvent e) { + if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) + return; + try { + Desktop.getDesktop().browse(e.getURL().toURI()); + } catch (Exception e1) { + log.error("", e1); + } + } + }); + panel.add(text, BorderLayout.NORTH); + try { + StringSelection contents = new StringSelection(sb.toString()); + ClipboardOwner owner = new ClipboardOwner() { + public void lostOwnership(Clipboard clipboard, Transferable contents) { + } + }; + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(contents, owner); + guiText += "

(The following text has already been copied to your clipboard.)

"; + } catch (RuntimeException x) { + log.error("", x); + } + text.setText("" + guiText + ""); + + JCheckBox quickMOBACcb = new JCheckBox("Do not continue and quit program by pressing OK"); + JTextArea info = new JTextArea(sb.toString(), 20, 60); + info.setCaretPosition(0); + info.setEditable(false); + info.setMinimumSize(new Dimension(200, 150)); + panel.add(new JScrollPane(info), BorderLayout.CENTER); + panel.add(quickMOBACcb, BorderLayout.SOUTH); + panel.setMinimumSize(new Dimension(700, 300)); + panel.validate(); + JOptionPane.showMessageDialog(null, panel, dialogTitle, JOptionPane.ERROR_MESSAGE); + if (quickMOBACcb.isSelected()) { + log.warn("User selected to quit MOBAC after an exception"); + System.exit(1); + } + } catch (Exception e1) { + e1.printStackTrace(); + } + } + + public static void installToolkitEventQueueProxy() { + + boolean isLikeJava7 = Float.parseFloat(System.getProperty("java.specification.version")) > 1.6; + + EventQueueProxy eventQueueProxy = new EventQueueProxy(); + // Bug handling. See + // http://stackoverflow.com/questions/3158254/how-to-replace-the-awt-eventqueue-with-own-implementation + try { + if (isLikeJava7) + EventQueue.invokeAndWait(eventQueueProxy); + else + eventQueueProxy.run(); + } catch (Exception e) { + log.error("", e); + } + } + + /** + * Catching all Runtime Exceptions in Swing + * + * http://ruben42.wordpress.com/2009/03/30/catching-all-runtime-exceptions-in-swing/ + */ + protected static class EventQueueProxy extends EventQueue implements Runnable { + + /** + * Installs the {@link EventQueueProxy} + */ + @Override + public void run() { + EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue(); + queue.push(this); + } + + protected void dispatchEvent(AWTEvent newEvent) { + try { + super.dispatchEvent(newEvent); + } catch (Throwable e) { + if (e instanceof ArrayIndexOutOfBoundsException) { + StackTraceElement[] st = e.getStackTrace(); + if (st.length > 0) { + if ("sun.font.FontDesignMetrics".equals(st[0].getClassName())) { + log.error("Ignored JRE bug exception " + e.getMessage() + " caused by : " + st[0]); + // This is a known JRE bug - we just ignore it + return; + } + } + } + GUIExceptionHandler.processException(Thread.currentThread(), e); + } + } + } + + public static void main(String[] args) { + for (;;) { + try { + Logging.configureConsoleLogging(); + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + throw new RuntimeException("Test", new Exception("Inner")); + } catch (Exception e) { + showExceptionDialog("Test 123", e); + } catch (Error e) { + showExceptionDialog(e); + } + break; + } + } + +} diff --git a/src/main/java/mobac/utilities/I18nUtils.java b/src/main/java/mobac/utilities/I18nUtils.java new file mode 100644 index 0000000..6f6f6c8 --- /dev/null +++ b/src/main/java/mobac/utilities/I18nUtils.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; +import java.util.ResourceBundle.Control; + +import mobac.Main; +import mobac.program.model.Settings; + +public class I18nUtils { + + // MP: return application's resource strings + private static ResourceBundle STRING_RESOURCE = null; + + public static String localizedStringForKey(String key, Object... args) { + if (STRING_RESOURCE == null) + I18nUtils.updateLocalizedStringFormSettings(); + String str = null; + try { + str = STRING_RESOURCE.getString(key); + if (args.length > 0) + str = String.format(str, args); + } catch (Exception e) { + str = key; + } + if (str == null) { + // always return a valid string + str = ""; + } + return str; + } + + public static synchronized void updateLocalizedStringFormSettings() { + Settings settings = Settings.getInstance(); + // force to use Simplify-Chinese Locale, will update later + Locale locale = null; + if (settings != null) { + locale = new Locale(settings.localeLanguage, settings.localeCountry); + } else { + locale = Locale.getDefault(); + } + + STRING_RESOURCE = ResourceBundle.getBundle("mobac.resources.text.localize", locale, new UTF8Control()); + } + + /** + * http://stackoverflow.com/questions/4659929/how-to-use-utf-8-in-resource-properties-with-resourcebundle + */ + public static class UTF8Control extends Control { + public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, + boolean reload) throws IllegalAccessException, InstantiationException, IOException { + // The below is a copy of the default implementation. + String bundleName = toBundleName(baseName, locale); + String resourceName = toResourceName(bundleName, "properties"); + ResourceBundle bundle = null; + InputStream stream = null; + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + connection.setUseCaches(false); + stream = connection.getInputStream(); + } + } + } else { + stream = loader.getResourceAsStream(resourceName); + } + if (stream != null) { + try { + // Only this line is changed to make it to read properties files as UTF-8. + bundle = new PropertyResourceBundle(new InputStreamReader(stream, Charsets.UTF_8)); + } finally { + stream.close(); + } + } + return bundle; + } + } + + public static InputStream getI18nResourceAsStream(String name, String extension) { + + Settings s = Settings.getInstance(); + String country = s.localeCountry; + String language = s.localeLanguage; + InputStream in; + in = Main.class.getResourceAsStream(String.format("%s_%s_%s.%s", name, language, country, extension)); + if (in != null) + return in; + in = Main.class.getResourceAsStream(String.format("%s_%s.%s", name, language, extension)); + if (in != null) + return in; + in = Main.class.getResourceAsStream(String.format("%s.%s", name, extension)); + return in; + } +} diff --git a/src/main/java/mobac/utilities/Juli2Log4jHandler.java b/src/main/java/mobac/utilities/Juli2Log4jHandler.java new file mode 100644 index 0000000..9d73082 --- /dev/null +++ b/src/main/java/mobac/utilities/Juli2Log4jHandler.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.text.MessageFormat; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.apache.log4j.Logger; +import org.apache.log4j.Priority; + +/** + * A JULI (java.util.logging) handler that redirects java.util.logging messages + * to Log4J http://wiki.apache.org/myfaces/Trinidad_and_Common_Logging
+ * User: josh Date: Jun 4, 2008 Time: 3:31:21 PM + * + * http://shrubbery.mynetgear.net/wiki/Routing_java.util. + * logging_messages_to_Log4J + */ +public class Juli2Log4jHandler extends Handler { + + public void publish(LogRecord record) { + org.apache.log4j.Logger log4j = getTargetLogger(record.getSourceClassName()); + Priority priority = toLog4j(record.getLevel()); + log4j.log(priority, toLog4jMessage(record), record.getThrown()); + } + + static Logger getTargetLogger(String loggerName) { + return Logger.getLogger(loggerName); + } + + public static Logger getTargetLogger(Class clazz) { + return getTargetLogger(clazz.getName()); + } + + private String toLog4jMessage(LogRecord record) { + String message = record.getMessage(); + // Format message + try { + Object parameters[] = record.getParameters(); + if (parameters != null && parameters.length != 0) { + // Check for the first few parameters ? + if (message.indexOf("{0}") >= 0 || message.indexOf("{1}") >= 0 + || message.indexOf("{2}") >= 0 || message.indexOf("{3}") >= 0) { + message = MessageFormat.format(message, parameters); + } + } + } catch (Exception ex) { + // ignore Exception + } + return message; + } + + private org.apache.log4j.Level toLog4j(Level level) {// converts levels + if (Level.SEVERE == level) { + return org.apache.log4j.Level.ERROR; + } else if (Level.WARNING == level) { + return org.apache.log4j.Level.WARN; + } else if (Level.INFO == level) { + return org.apache.log4j.Level.INFO; + } else if (Level.OFF == level) { + return org.apache.log4j.Level.OFF; + } + return org.apache.log4j.Level.OFF; + } + + @Override + public void flush() { + // nothing to do + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/src/main/java/mobac/utilities/MapDataFileParser.java b/src/main/java/mobac/utilities/MapDataFileParser.java new file mode 100644 index 0000000..719d0ab --- /dev/null +++ b/src/main/java/mobac/utilities/MapDataFileParser.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import mobac.gui.mapview.JMapViewer; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.MapSpace; + + + +public class MapDataFileParser { + + private static final String K = "[\\s ]*,[\\s ]*"; + + private static final String HEAD_REGEX = "^OziExplorer Map Data File Version (\\d\\.\\d)"; + private static final String POINT_REGEX = "^Point(\\d\\d)@xy@(\\d+)@(\\d+)@in@deg@" + + "(\\d+)@(\\d+(?:\\.\\d+)?)@(N|S)@(\\d+)@(\\d+(?:\\.\\d+)?)@(E|W)@grid@@@" + ".*"; + private static final String MMPLL_REGEX = "^MMPLL,(\\d)@(\\d+\\.\\d+)@(\\d+\\.\\d+)"; + + public MapDataFileParser(File mapFile) throws IOException, MapFileFormatException { + this(new FileInputStream(mapFile)); + } + + public MapDataFileParser(InputStream in) throws IOException, MapFileFormatException { + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String line = br.readLine(); + Matcher m = Pattern.compile(HEAD_REGEX).matcher(line); + if (!m.matches()) + throw new MapFileFormatException("OziExplorer Map Data File Version not found"); + String fileVersion = m.group(1); + line = br.readLine(); + // System.out.println(POINT_REGEX.replace("@", K)); + Pattern pointP = Pattern.compile(POINT_REGEX.replaceAll("\\@", K)); + Pattern mmpllP = Pattern.compile(MMPLL_REGEX.replaceAll("\\@", K)); + ArrayList mapPoints = new ArrayList(); + while (line != null) { + String cLine = line; + line = br.readLine(); + m = pointP.matcher(cLine); + if (m.matches()) { + MapPoint p = new MapPoint(); + // p.num = Integer.parseInt(m.group(1)); + p.x = Integer.parseInt(m.group(2)); + p.y = Integer.parseInt(m.group(3)); + double lat = Integer.parseInt(m.group(4)) + (Double.parseDouble(m.group(5)) / 60.0); + if ("S".equalsIgnoreCase(m.group(6))) + lat = -lat; + p.lat = lat; + double lon = Integer.parseInt(m.group(7)) + (Double.parseDouble(m.group(8)) / 60.0); + if ("W".equalsIgnoreCase(m.group(9))) + lon = -lon; + p.lon = lon; + mapPoints.add(p); + // System.out.println(m.group(0)); + continue; + } + m = mmpllP.matcher(cLine); + if (m.matches()) { + MapPoint p = new MapPoint(); + p.lon = Double.parseDouble(m.group(2)); + p.lat = Double.parseDouble(m.group(3)); + mapPoints.add(p); + } + + } + System.out.println("File version: " + fileVersion); + // System.out.println("Callibration / map border points:"); + double lat_max = Double.MIN_VALUE; + double lon_max = Double.MIN_VALUE; + double lat_min = Double.MAX_VALUE; + double lon_min = Double.MAX_VALUE; + for (MapPoint p : mapPoints) { + // System.out.println("\t" + p); + lat_max = Math.max(lat_max, p.lat); + lon_max = Math.max(lon_max, p.lon); + lat_min = Math.min(lat_min, p.lat); + lon_min = Math.min(lon_min, p.lon); + } + System.out.println(String.format("Max point (lat/lon): %4f %4f", lat_max, lon_max)); + System.out.println(String.format("Min point (lat/lon): %4f %4f", lat_min, lon_min)); + + MapSpace ms = MercatorPower2MapSpace.INSTANCE_256; + int x1 = ms.cLonToX(lon_max, JMapViewer.MAX_ZOOM); + int x2 = ms.cLonToX(lon_min, JMapViewer.MAX_ZOOM); + int diff = Math.abs(x1 - x2); + for (int i = 1; i < 10; i++) { + System.out.println((JMapViewer.MAX_ZOOM - i) + " : " + diff); + diff /= 2; + } + br.close(); + } + + public static class MapPoint { + // int num; + int x; + int y; + double lat; + double lon; + + public String toString() { + return String.format("%6f %6f", lat, lon); + } + } + + public static class MapFileFormatException extends Exception { + + private static final long serialVersionUID = 1L; + + public MapFileFormatException(String message, Throwable cause) { + super(message, cause); + } + + public MapFileFormatException(String message) { + super(message); + } + + } + + public static void main(String[] args) { + try { + File f = new File("atlases/Test_2009-10-01_164148/Test1/Test1 14/Test1 14.map"); + MapDataFileParser p = new MapDataFileParser(f); + System.out.println(p); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/mobac/utilities/MyMath.java b/src/main/java/mobac/utilities/MyMath.java new file mode 100644 index 0000000..70a7d05 --- /dev/null +++ b/src/main/java/mobac/utilities/MyMath.java @@ -0,0 +1,179 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +public class MyMath { + + /** + * + * Rounds down to the nearest multiple of toNearest + * + * @param value + * positive value + * @param toNearest + * positive value + * @return + */ + public static int roundDownToNearest(int value, int toNearest) { + int remainder = value % toNearest; + value -= remainder; + return value; + } + + /** + * + * Rounds up to the nearest multiple of toNearest + * + * @param value + * positive value + * @param toNearest + * positive value + * @return + */ + public static int roundUpToNearest(int value, int toNearest) { + int remainder = value % toNearest; + if (remainder == 0) + return value; + value += (toNearest - remainder); + return value; + } + + /** + * + * @param value + * @return + */ + public static int ceil(double value) { + return (int) Math.ceil(value); + } + + /** + * Divides value by divisor and performs the function "ceil" on the fractional result. + * Warning: This method works only with positive input values! + *

+ * Internally only integer arithmetics is used (no floating point arithmetics) + *

+ * + * @param value + * positive value + * @param divisor + * positive value + * @return + */ + public static int divCeil(int value, int divisor) { + int result = value / divisor; + int remainder = value % divisor; + if (remainder != 0) + result++; + return result; + } + + /** + * Divides value by divisor and performs the function "ceil" on the fractional + * result.Warning: This method works only with positive input values! + *

+ * Internally only integer arithmetics is used (no floating point arithmetics) + *

+ * + * @param value + * positive value + * @param divisor + * positive value + * @return + */ + public static long divCeil(long value, long divisor) { + long result = value / divisor; + if (value % divisor != 0) + result++; + return result; + } + + /** + * Divides value by divisor and performs the function "round" on the fractional result. + * Warning: This method works only with positive input values! + *

+ * Internally only integer arithmetics is used (no floating point arithmetics) + *

+ * + * @param value + * positive value + * @param divisor + * positive value + * @return + */ + public static int divRound(int value, int divisor) { + int result = value / divisor; + int remainder = value % divisor; + if (remainder > (divisor >> 1)) + result++; + return result; + } + + /** + * Divides value by divisor and performs the function "round" on the fractional result. + * Warning: This method works only with positive input values! + *

+ * Internally only integer arithmetics is used (no floating point arithmetics) + *

+ * + * @param value + * positive value + * @param divisor + * positive value + * @return + */ + public static long divRound(long value, long divisor) { + long result = value / divisor; + long remainder = value % divisor; + if (remainder > (divisor >> 1)) + result++; + return result; + } + + /** + * Examples: + * + *
+	 *     0.1 to     1.0
+	 *     0.5 to     1.0
+	 *     0.9 to     1.0
+	 *     1.1 to     1.0
+	 *     1.9 to     2.0
+	 *     9.9 to    10.0
+	 *    15.5 to    20.0
+	 *    88.0 to    90.0
+	 *   363.0 to   400.0 
+	 *  1155.0 to  1000.0
+	 *  1655.0 to  2000.0
+	 * 13455.0 to 10000.0
+	 * 17755.0 to 20000.0
+	 * 
+ * + * @param val + * @return array containing the rounded value + */ + public static double prettyRound(double val) { + if (val < 1.0) + return Math.ceil(val); + int l10 = (int) Math.pow(10, Math.floor(Math.log10(val))); + double x = val / l10; + x = Math.round(x); + x = x * l10; + return x; + } + +} diff --git a/src/main/java/mobac/utilities/OSUtilities.java b/src/main/java/mobac/utilities/OSUtilities.java new file mode 100644 index 0000000..866395f --- /dev/null +++ b/src/main/java/mobac/utilities/OSUtilities.java @@ -0,0 +1,189 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FilenameFilter; +import java.util.regex.Pattern; + +import javax.naming.NameNotFoundException; +import javax.swing.JOptionPane; + +import org.apache.log4j.Logger; + +public class OSUtilities { + + static Logger log = Logger.getLogger(OSUtilities.class); + + public enum OperatingSystem { + Windows, Linux, MacOs, MacOsX, Solaris, Unknown + }; + + public enum DesktopType { + Windows, Gnome, Kde, Unknown { + @Override + public String toString() { + return super.toString() + " (" + System.getProperty("sun.desktop") + ")"; + } + } + }; + + public static OperatingSystem detectOs() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.indexOf("windows") > -1) + return OperatingSystem.Windows; + if (osName.indexOf("linux") > -1) + return OperatingSystem.Linux; + if (osName.indexOf("mac os x") > -1) + return OperatingSystem.MacOsX; + if (osName.indexOf("mac os") > -1) + return OperatingSystem.MacOs; + if (osName.indexOf("sunos") > -1) + return OperatingSystem.Solaris; + + return OperatingSystem.Unknown; + } + + public static DesktopType detectDesktopType() { + String desktopName = System.getProperty("sun.desktop"); + if (desktopName == null) + return DesktopType.Unknown; + desktopName = desktopName.toLowerCase().trim(); + if (desktopName.startsWith("windows")) + return DesktopType.Windows; + if (desktopName.startsWith("gnome")) + return DesktopType.Gnome; + if (desktopName.startsWith("kde")) + return DesktopType.Kde; + log.error("Unknown desktop type: " + desktopName); + return DesktopType.Unknown; + } + + public static void openFolderBrowser(File directory) throws NameNotFoundException { + if (!directory.isDirectory()) + throw new NameNotFoundException("Directory does not exist or is not a directory"); + String[] strCmd = null; + try { + String dirPath = directory.getCanonicalPath(); + DesktopType dt = detectDesktopType(); + switch (dt) { + case Windows: + strCmd = new String[] { "rundll32.exe", "url.dll,FileProtocolHandler", "\"" + dirPath + "\"" }; + break; + case Gnome: + strCmd = new String[] { "nautilus", dirPath }; + break; + case Kde: + strCmd = new String[] { "konqueror", dirPath }; + break; + default: + OperatingSystem os = detectOs(); + switch (os) { + case MacOsX: + strCmd = new String[] { "/usr/bin/open", dirPath }; + break; + case Linux: + strCmd = new String[] { "/usr/bin/xdg-open", dirPath }; + break; + default: + JOptionPane.showMessageDialog(null, + String.format(I18nUtils.localizedStringForKey("msg_environment_not_support"), dt, os), + I18nUtils.localizedStringForKey("msg_environment_not_support_title"), + JOptionPane.ERROR_MESSAGE); + return; + } + } + for (String s : strCmd) + log.trace("exec/params: " + s); + Runtime.getRuntime().exec(strCmd); + } catch (Exception e) { + StringBuilder sb = new StringBuilder(512); + sb.append(I18nUtils.localizedStringForKey("msg_environment_failed_open_output")); + sb.append(e.getMessage()); + sb.append(String.format(I18nUtils.localizedStringForKey("msg_environment_use_commond"), strCmd[0])); + for (int i = 1; i < strCmd.length; i++) { + sb.append(String.format(I18nUtils.localizedStringForKey("msg_environment_commond_param"), i, strCmd[i])); + } + sb.append(""); + String msg = sb.toString(); + JOptionPane.showMessageDialog(null, msg, + I18nUtils.localizedStringForKey("msg_environment_failed_open_output_title"), + JOptionPane.ERROR_MESSAGE); + log.error(msg, e); + } + + } + + /** + * Reads the Linux distribution name (last line) from the first file that matches the pattern + * + *
+	 * /etc/*-release
+	 * 
+ * + * @return Linux distrbution name or null + */ + public static String getLinuxDistributionName() { + try { + File etcDir = new File("/etc"); + if (!etcDir.exists()) + return null; + File[] files = etcDir.listFiles(new FilenameFilter() { + + Pattern pattern = Pattern.compile(".*-release"); + + public boolean accept(File dir, String name) { + return pattern.matcher(name).matches(); + } + }); + if (files.length == 0) + return null; + BufferedReader br = new BufferedReader(new FileReader(files[0])); + String result = null; + String line = null; + line = br.readLine(); + while (line != null) { + result = line; + line = br.readLine(); + } + br.close(); + if (result != null) { + int index = result.indexOf('='); + if (index > 0) + result = result.substring(index + 1); + if (result.startsWith("\"") && result.endsWith("\"")) + result = result.substring(1, result.length() - 2); + } + return result; + } catch (Exception e) { + log.trace("", e); + return null; + } + } + + /** + * Replies true if we are currently running on OSX + * + * @return true if we are currently running on OSX + */ + public static boolean isPlatformOsx() { + String os = System.getProperty("os.name"); + return os != null && os.toLowerCase().startsWith("mac os x"); + } +} diff --git a/src/main/java/mobac/utilities/Utilities.java b/src/main/java/mobac/utilities/Utilities.java new file mode 100644 index 0000000..99f1162 --- /dev/null +++ b/src/main/java/mobac/utilities/Utilities.java @@ -0,0 +1,735 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JComponent; + +import mobac.Main; +import mobac.exceptions.MOBACOutOfMemoryException; +import mobac.program.Logging; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageType; +import mobac.utilities.file.DirectoryFileFilter; + +import org.apache.log4j.Logger; + +public class Utilities { + + public static final Color COLOR_TRANSPARENT = new Color(0, 0, 0, 0); + public static final DecimalFormatSymbols DFS_ENG = new DecimalFormatSymbols(Locale.ENGLISH); + public static final DecimalFormatSymbols DFS_LOCAL = new DecimalFormatSymbols(); + public static final DecimalFormat FORMAT_6_DEC = new DecimalFormat("#0.######"); + public static final DecimalFormat FORMAT_6_DEC_ENG = new DecimalFormat("#0.######", DFS_ENG); + public static final DecimalFormat FORMAT_2_DEC = new DecimalFormat("0.00"); + private static final DecimalFormat cDmsMinuteFormatter = new DecimalFormat("00"); + private static final DecimalFormat cDmsSecondFormatter = new DecimalFormat("00.0"); + + private static final Logger log = Logger.getLogger(Utilities.class); + + public static final long SECONDS_PER_HOUR = TimeUnit.HOURS.toSeconds(1); + public static final long SECONDS_PER_DAY = TimeUnit.DAYS.toSeconds(1); + + public static boolean testJaiColorQuantizerAvailable() { + try { + Class c = Class.forName("javax.media.jai.operator.ColorQuantizerDescriptor"); + if (c != null) + return true; + } catch (NoClassDefFoundError e) { + return false; + } catch (Throwable t) { + log.error("Error in testJaiColorQuantizerAvailable():", t); + return false; + } + return true; + } + + public static BufferedImage createEmptyTileImage(MapSource mapSource) { + int tileSize = mapSource.getMapSpace().getTileSize(); + Color color = mapSource.getBackgroundColor(); + + int imageType; + if (color.getAlpha() == 255) + imageType = BufferedImage.TYPE_INT_RGB; + else + imageType = BufferedImage.TYPE_INT_ARGB; + BufferedImage emptyImage = new BufferedImage(tileSize, tileSize, imageType); + Graphics2D g = emptyImage.createGraphics(); + try { + g.setColor(color); + g.fillRect(0, 0, tileSize, tileSize); + } finally { + g.dispose(); + } + return emptyImage; + } + + public static BufferedImage safeCreateBufferedImage(int width, int height, int imageType) { + try { + return new BufferedImage(width, height, imageType); + } catch (OutOfMemoryError e) { + int bytesPerPixel = getBytesPerPixel(imageType); + if (bytesPerPixel < 0) + throw e; + long requiredMemory = ((long) width) * ((long) height) * bytesPerPixel; + String message = String.format( + "Available free memory not sufficient for creating image of size %dx%d pixels", width, height); + throw new MOBACOutOfMemoryException(requiredMemory, message); + } + } + + /** + * + * @param imageType + * as used for {@link BufferedImage#BufferedImage(int, int, int)} + * @return + */ + public static int getBytesPerPixel(int bufferedImageType) { + switch (bufferedImageType) { + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + case BufferedImage.TYPE_INT_BGR: + case BufferedImage.TYPE_4BYTE_ABGR: + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + return 4; + case BufferedImage.TYPE_3BYTE_BGR: + return 3; + case BufferedImage.TYPE_USHORT_GRAY: + case BufferedImage.TYPE_USHORT_565_RGB: + case BufferedImage.TYPE_USHORT_555_RGB: + return 2; + case BufferedImage.TYPE_BYTE_GRAY: + case BufferedImage.TYPE_BYTE_BINARY: + case BufferedImage.TYPE_BYTE_INDEXED: + return 1; + } + return -1; + } + + public static byte[] createEmptyTileData(MapSource mapSource) { + BufferedImage emptyImage = createEmptyTileImage(mapSource); + ByteArrayOutputStream buf = new ByteArrayOutputStream(4096); + try { + ImageIO.write(emptyImage, mapSource.getTileImageType().getFileExt(), buf); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] emptyTileData = buf.toByteArray(); + return emptyTileData; + } + + private static final byte[] PNG = new byte[] { (byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }; + private static final byte[] JPG = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, (byte) 0x00, + 0x10, 'J', 'F', 'I', 'F' }; + private static final byte[] GIF_1 = "GIF87a".getBytes(); + private static final byte[] GIF_2 = "GIF89a".getBytes(); + + public static TileImageType getImageType(byte[] imageData) { + if (imageData == null) + return null; + if (startsWith(imageData, PNG)) + return TileImageType.PNG; + if (startsWith(imageData, JPG)) + return TileImageType.JPG; + if (startsWith(imageData, GIF_1) || startsWith(imageData, GIF_2)) + return TileImageType.GIF; + return null; + } + + public static boolean startsWith(byte[] data, byte[] startTest) { + if (data.length < startTest.length) + return false; + for (int i = 0; i < startTest.length; i++) + if (data[i] != startTest[i]) + return false; + return true; + } + + /** + * Checks if the available JAXB version is at least v2.1 + */ + public static boolean checkJAXBVersion() { + try { + // We are trying to load the class javax.xml.bind.JAXB which has + // been introduced with JAXB 2.1. Previous version do not contain + // this class and will therefore throw an exception. + Class c = Class.forName("javax.xml.bind.JAXB"); + return (c != null); + } catch (ClassNotFoundException e) { + return false; + } + } + + public static InputStream loadResourceAsStream(String resourcePath) throws IOException { + return Main.class.getResourceAsStream("resources/" + resourcePath); + } + + public static String loadTextResource(String resourcePath) throws IOException { + DataInputStream in = new DataInputStream(Main.class.getResourceAsStream("resources/" + resourcePath)); + byte[] buf; + buf = new byte[in.available()]; + in.readFully(buf); + in.close(); + String text = new String(buf, Charsets.UTF_8); + return text; + } + + /** + * + * @param imageName + * imagePath resource path relative to the class {@link Main} + * @return + */ + public static ImageIcon loadResourceImageIcon(String imageName) { + URL url = Main.class.getResource("resources/images/" + imageName); + return new ImageIcon(url); + } + + public static URL getResourceImageUrl(String imageName) { + return Main.class.getResource("resources/images/" + imageName); + } + + public static void loadProperties(Properties p, URL url) throws IOException { + InputStream propIn = url.openStream(); + try { + p.load(propIn); + } finally { + closeStream(propIn); + } + } + + public static void loadProperties(Properties p, File f) throws IOException { + InputStream propIn = new FileInputStream(f); + try { + p.load(propIn); + } finally { + closeStream(propIn); + } + } + + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Checks if the current {@link Thread} has been interrupted and if so a {@link InterruptedException}. Therefore it + * behaves similar to {@link Thread#sleep(long)} without actually slowing down anything by sleeping a certain amount + * of time. + * + * @throws InterruptedException + */ + public static void checkForInterruption() throws InterruptedException { + if (Thread.currentThread().isInterrupted()) + throw new InterruptedException(); + } + + /** + * Checks if the current {@link Thread} has been interrupted and if so a {@link RuntimeException} will be thrown. + * This method is useful for long lasting operations that do not allow to throw an {@link InterruptedException}. + * + * @throws RuntimeException + */ + public static void checkForInterruptionRt() throws RuntimeException { + if (Thread.currentThread().isInterrupted()) + throw new RuntimeException(new InterruptedException()); + } + + public static void closeFile(RandomAccessFile file) { + if (file == null) + return; + try { + file.close(); + } catch (IOException e) { + } + } + + public static void closeStream(InputStream in) { + if (in == null) + return; + try { + in.close(); + } catch (IOException e) { + } + } + + public static void close(Closeable c) { + if (c == null) + return; + try { + c.close(); + } catch (IOException e) { + } + } + + public static void closeStream(OutputStream out) { + if (out == null) + return; + try { + out.close(); + } catch (IOException e) { + } + } + + public static void closeWriter(Writer writer) { + if (writer == null) + return; + try { + writer.close(); + } catch (IOException e) { + } + } + + public static void closeReader(OutputStream reader) { + if (reader == null) + return; + try { + reader.close(); + } catch (IOException e) { + } + } + + public static void closeStatement(Statement statement) { + if (statement == null) + return; + try { + statement.close(); + } catch (SQLException e) { + } + } + + public static double parseLocaleDouble(String text) throws ParseException { + ParsePosition pos = new ParsePosition(0); + Number n = Utilities.FORMAT_6_DEC.parse(text, pos); + if (n == null) + throw new ParseException("Unknown error", 0); + if (pos.getIndex() != text.length()) + throw new ParseException("Text ends with unparsable characters", pos.getIndex()); + return n.doubleValue(); + } + + public static void showTooltipNow(JComponent c) { + Action toolTipAction = c.getActionMap().get("postTip"); + if (toolTipAction != null) { + ActionEvent postTip = new ActionEvent(c, ActionEvent.ACTION_PERFORMED, ""); + toolTipAction.actionPerformed(postTip); + } + } + + /** + * Formats a byte value depending on the size to "Bytes", "KiBytes", "MiByte" and "GiByte" + * + * @param bytes + * @return Formatted {@link String} + */ + public static String formatBytes(long bytes) { + if (bytes < 1000) + return Long.toString(bytes) + " " + I18nUtils.localizedStringForKey("Bytes"); + if (bytes < 1000000) + return FORMAT_2_DEC.format(bytes / 1024d) + " " + I18nUtils.localizedStringForKey("KiByte"); + if (bytes < 1000000000) + return FORMAT_2_DEC.format(bytes / 1048576d) + " " + I18nUtils.localizedStringForKey("MiByte"); + return FORMAT_2_DEC.format(bytes / 1073741824d) + " " + I18nUtils.localizedStringForKey("GiByte"); + } + + public static String formatDurationSeconds(long seconds) { + long x = seconds; + long days = x / SECONDS_PER_DAY; + x %= SECONDS_PER_DAY; + int years = (int) (days / 365); + days -= (years * 365); + + int months = (int) (days * 12d / 365d); + String m = (months == 1) ? "month" : "months"; + + if (years > 5) + return String.format("%d years", years); + if (years > 0) { + String y = (years == 1) ? "year" : "years"; + return String.format("%d %s %d %s", years, y, months, m); + } + String d = (days == 1) ? "day" : "days"; + if (months > 0) { + days -= months * (365d / 12d); + return String.format("%d %s %d %s", months, m, days, d); + } + long hours = TimeUnit.SECONDS.toHours(x); + String h = (hours == 1) ? "hour" : "hours"; + x -= hours * SECONDS_PER_HOUR; + if (days > 0) + return String.format("%d %s %d %s", days, d, hours, h); + long minutes = TimeUnit.SECONDS.toMinutes(x); + String min = (minutes == 1) ? "minute" : "minutes"; + if (hours > 0) + return String.format("%d %s %d %s", hours, h, minutes, min); + else + return String.format("%d %s", minutes, min); + } + + public static void mkDir(File dir) throws IOException { + if (dir.isDirectory()) + return; + if (!dir.mkdir()) + throw new IOException("Failed to create directory \"" + dir.getAbsolutePath() + "\""); + } + + public static void mkDirs(File dir) throws IOException { + if (dir.isDirectory()) + return; + if (dir.mkdirs()) + return; + + if (Logging.isCONFIGURED()) + Logging.LOG.error("mkDirs creation failed first time - one retry left"); + + // Wait some time and then retry it. + // See for details: + // http://javabyexample.wisdomplug.com/component/content/article/37-core-java/48-is-mkdirs-thread-safe.html + // Hopefully this will fix the different bugs reported for this method + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + if (dir.mkdirs()) + return; + + throw new IOException("Failed to create directory \"" + dir.getAbsolutePath() + "\""); + } + + public static void fileCopy(File sourceFile, File destFile) throws IOException { + + FileChannel source = null; + FileChannel destination = null; + FileInputStream fis = null; + FileOutputStream fos = null; + + try { + fis = new FileInputStream(sourceFile); + fos = new FileOutputStream(destFile); + + source = fis.getChannel(); + destination = fos.getChannel(); + + destination.transferFrom(source, 0, source.size()); + } finally { + fis.close(); + fos.close(); + + if (source != null) { + source.close(); + } + if (destination != null) { + destination.close(); + } + } + } + + public static byte[] getFileBytes(File file) throws IOException { + int size = (int) file.length(); + byte[] buffer = new byte[size]; + DataInputStream in = new DataInputStream(new FileInputStream(file)); + try { + in.readFully(buffer); + return buffer; + } finally { + closeStream(in); + } + } + + /** + * Fully reads data from in to an internal buffer until the end of in has been reached. Then the buffer is + * returned. + * + * @param in + * data source to be read + * @return buffer all data available in in + * @throws IOException + */ + public static byte[] getInputBytes(InputStream in) throws IOException { + int initialBufferSize = in.available(); + if (initialBufferSize <= 0) + initialBufferSize = 32768; + ByteArrayOutputStream buffer = new ByteArrayOutputStream(initialBufferSize); + byte[] b = new byte[1024]; + int ret = 0; + while ((ret = in.read(b)) >= 0) { + buffer.write(b, 0, ret); + } + return buffer.toByteArray(); + } + + /** + * Fully reads data from in the read data is discarded. + * + * @param in + * @throws IOException + */ + public static void readFully(InputStream in) throws IOException { + byte[] b = new byte[4096]; + while ((in.read(b)) >= 0) { + } + } + + /** + * Lists all direct sub directories of dir + * + * @param dir + * @return list of directories + */ + public static File[] listSubDirectories(File dir) { + return dir.listFiles(new DirectoryFileFilter()); + } + + public static List listSubDirectoriesRec(File dir, int maxDepth) { + List dirList = new LinkedList(); + addSubDirectories(dirList, dir, maxDepth); + return dirList; + } + + public static void addSubDirectories(List dirList, File dir, int maxDepth) { + File[] subDirs = dir.listFiles(new DirectoryFileFilter()); + for (File f : subDirs) { + dirList.add(f); + if (maxDepth > 0) + addSubDirectories(dirList, f, maxDepth - 1); + } + } + + public static String prettyPrintLatLon(double coord, boolean isCoordKindLat) { + boolean neg = coord < 0.0; + String c; + if (isCoordKindLat) { + c = (neg ? "S" : "N"); + } else { + c = (neg ? "W" : "E"); + } + double tAbsCoord = Math.abs(coord); + int tDegree = (int) tAbsCoord; + double tTmpMinutes = (tAbsCoord - tDegree) * 60; + int tMinutes = (int) tTmpMinutes; + double tSeconds = (tTmpMinutes - tMinutes) * 60; + return c + tDegree + "\u00B0" + cDmsMinuteFormatter.format(tMinutes) + "\'" + + cDmsSecondFormatter.format(tSeconds) + "\""; + } + + public static void setHttpProxyHost(String host) { + if (host != null && host.length() > 0) + System.setProperty("http.proxyHost", host); + else + System.getProperties().remove("http.proxyHost"); + } + + public static void setHttpProxyPort(String port) { + if (port != null && port.length() > 0) + System.setProperty("http.proxyPort", port); + else + System.getProperties().remove("http.proxyPort"); + } + + /** + * Returns the file path for the selected class. If the class is located inside a JAR file the return value contains + * the directory that contains the JAR file. If the class file is executed outside of an JAR the root directory + * holding the class/package structure is returned. + * + * @param mainClass + * @return + * @throws URISyntaxException + */ + public static File getClassLocation(Class mainClass) { + ProtectionDomain pDomain = mainClass.getProtectionDomain(); + CodeSource cSource = pDomain.getCodeSource(); + File f; + try { + URL loc = cSource.getLocation(); // file:/c:/almanac14/examples/ + f = new File(loc.toURI()); + } catch (Exception e) { + throw new RuntimeException("Unable to determine program directory: ", e); + } + if (f.isDirectory()) { + // Class is executed from class/package structure from file system + return f; + } else { + // Class is executed from inside of a JAR -> f references the JAR + // file + return f.getParentFile(); + } + } + + /** + * Saves data to the file specified by filename. + * + * @param filename + * @param data + * @throws IOException + */ + public static void saveBytes(String filename, byte[] data) throws IOException { + FileOutputStream fo = null; + try { + fo = new FileOutputStream(filename); + fo.write(data); + } finally { + closeStream(fo); + } + } + + /** + * Saves data to the file specified by filename. + * + * @param filename + * @param data + * @return Data has been saved successfully? + */ + public static boolean saveBytesEx(String filename, byte[] data) { + FileOutputStream fo = null; + try { + fo = new FileOutputStream(filename); + fo.write(data); + return true; + } catch (IOException e) { + return false; + } finally { + closeStream(fo); + } + } + + /** + * Tries to delete a file or directory and throws an {@link IOException} if that fails. + * + * @param fileToDelete + * @throws IOException + * Thrown if fileToDelete can not be deleted. + */ + public static void deleteFile(File fileToDelete) throws IOException { + if (!fileToDelete.delete()) + throw new IOException("Deleting of \"" + fileToDelete + "\" failed."); + } + + public static void renameFile(File oldFile, File newFile) throws IOException { + if (!oldFile.renameTo(newFile)) + throw new IOException("Failed to rename file: " + oldFile + " to " + newFile); + } + + public static int getJavaMaxHeapMB() { + try { + return (int) (Runtime.getRuntime().maxMemory() / 1048576l); + } catch (Exception e) { + return -1; + } + } + + public static byte[] downloadHttpFile(String url) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) + throw new IOException("Invalid HTTP response: " + responseCode + " for url " + conn.getURL()); + InputStream in = conn.getInputStream(); + try { + return Utilities.getInputBytes(in); + } finally { + in.close(); + } + } + + public static void copyFile(File source, File target) throws IOException { + FileChannel in = null; + FileChannel out = null; + try { + in = (new FileInputStream(source)).getChannel(); + out = (new FileOutputStream(target)).getChannel(); + in.transferTo(0, source.length(), out); + } finally { + close(in); + close(out); + } + } + + /** + * + * @param value + * positive value + * @return 0 if no bit is set else the highest bit that is one in value + */ + public static int getHighestBitSet(int value) { + int bit = 0x40000000; + for (int i = 31; i > 0; i--) { + int test = bit & value; + if (test != 0) + return i; + bit >>= 1; + } + return 0; + } + + /** + * + * @param revsision + * SVN revision string like "1223", "1224M" or "1616:1622M" + * @return parsed svn revision + */ + public static int parseSVNRevision(String revision) { + revision = revision.trim(); + int index = revision.lastIndexOf(':'); + if (index >= 0) { + revision = revision.substring(index + 1).trim(); + } + Matcher m = Pattern.compile("(\\d+)[^\\d]*").matcher(revision); + if (!m.matches()) + return -1; + return Integer.parseInt(m.group(1)); + } + +} diff --git a/src/main/java/mobac/utilities/beanshell/Tools.java b/src/main/java/mobac/utilities/beanshell/Tools.java new file mode 100644 index 0000000..ff2a15c --- /dev/null +++ b/src/main/java/mobac/utilities/beanshell/Tools.java @@ -0,0 +1,80 @@ +package mobac.utilities.beanshell; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.security.SecureRandom; + +import mobac.mapsources.MapSourceTools; +import mobac.mapsources.mapspace.MercatorPower2MapSpace; +import mobac.program.interfaces.MapSpace; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; + +public class Tools { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface MethodDescription { + String value(); + } + + public static final SecureRandom RND = new SecureRandom(); + public static final MapSpace OSM_MERCATOR = MercatorPower2MapSpace.INSTANCE_256; + + @MethodDescription("Converts an horizontal tile number on a certain zoom level " + + "into the corespondent longitude") + public static double xTileToLon(int x, int zoom) { + return OSM_MERCATOR.cXToLon(x, zoom); + } + + @MethodDescription("Converts an vertical tile number on a certain zoom level " + + "into the corespondent latitude") + public static double yTileToLat(int y, int zoom) { + return OSM_MERCATOR.cYToLat(y, zoom); + } + + @MethodDescription("Returns a random value. Range [0..max]") + public int getRandomInt(int max) { + return RND.nextInt(max + 1); + } + + @MethodDescription("Converts a tile numer on a certain zoom level into a quad tree coordinate") + public static String encodeQuadTree(int zoom, int tilex, int tiley) { + return MapSourceTools.encodeQuadTree(zoom, tilex, tiley); + } + + @MethodDescription("Returns a byte array of length length filled with random data.") + public byte[] getRandomByteArray(int length) { + byte[] buf = new byte[length]; + RND.nextBytes(buf); + return buf; + } + + @MethodDescription("Encodes the binaryData byte array to a " + + "base64 String without line breaks") + public static String encodeBase64(byte[] binaryData) { + return new String(Base64.encodeBase64(binaryData)); + } + + @MethodDescription("Decodes an base64 encoded String to a byte array") + public static byte[] decodeBase64(String base64String) { + return Base64.decodeBase64(base64String); + } + + @MethodDescription("Encodes the binaryData byte array to a hexadecimal String " + + "without line breaks, leading 0x and spaces") + public static String encodeHex(byte[] binaryData) throws DecoderException { + return Hex.encodeHexString(binaryData); + } + + @MethodDescription("Decodes an hexadecimal encoded String to a byte array. The string have to " + + "contain only the hexadecimal encoded nibbles.") + public static byte[] decodeHex(String hexString) throws DecoderException { + return Hex.decodeHex(hexString.toCharArray()); + } + +} diff --git a/src/main/java/mobac/utilities/collections/SoftHashMap.java b/src/main/java/mobac/utilities/collections/SoftHashMap.java new file mode 100644 index 0000000..048f379 --- /dev/null +++ b/src/main/java/mobac/utilities/collections/SoftHashMap.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.collections; + +import java.lang.ref.SoftReference; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A HashMap that uses internally SoftReferences for the value (if the app is + * running out of memory the values stored in a {@link SoftReference} will be + * automatically freed. + * + * Therefore it may happen that this map "looses" values. + * + * @param + * @param + */ +public class SoftHashMap implements Map { + + HashMap> map; + + public SoftHashMap(int initialCapacity) { + map = new HashMap>(initialCapacity); + } + + public V get(Object key) { + SoftReference ref = map.get(key); + return (ref != null) ? ref.get() : null; + } + + public V put(K key, V value) { + SoftReference ref = map.put(key, new SoftReference(value)); + return (ref != null) ? ref.get() : null; + } + + public V remove(Object key) { + SoftReference ref = map.remove(key); + return (ref != null) ? ref.get() : null; + } + + public void clear() { + map.clear(); + } + + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + public Set keySet() { + return map.keySet(); + } + + public int size() { + return map.size(); + } + + public Set> entrySet() { + throw new RuntimeException("Not implemented"); + } + + public void putAll(Map m) { + throw new RuntimeException("Not implemented"); + } + + public Collection values() { + throw new RuntimeException("Not implemented"); + } +} diff --git a/src/main/java/mobac/utilities/debug/MySocketImplFactory.java b/src/main/java/mobac/utilities/debug/MySocketImplFactory.java new file mode 100644 index 0000000..312a9da --- /dev/null +++ b/src/main/java/mobac/utilities/debug/MySocketImplFactory.java @@ -0,0 +1,313 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.debug; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketImpl; +import java.net.SocketImplFactory; +import java.net.URL; + +import mobac.program.Logging; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +/** + * Proxies the default {@link SocketImpl} using reflection. This allows to trace + * all calls into the log file. + * + * Requires SunJRE/SunJDK! + */ +public class MySocketImplFactory implements SocketImplFactory { + + private static Logger log = Logger.getLogger(MySocketImplFactory.class); + + /** + * @param args + */ + public static void main(String[] args) { + + try { + install(); + Logging.configureConsoleLogging(Level.TRACE); + + HttpURLConnection conn = (HttpURLConnection) new URL("http://google.de") + .openConnection(); + conn.connect(); + byte[] data = new byte[1024]; + new DataInputStream(conn.getInputStream()).readFully(data); + System.out.println(new String(data)); + Thread.sleep(1000); + System.gc(); + Thread.sleep(1000); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public static void install() throws IOException { + try { + Socket.setSocketImplFactory(new MySocketImplFactory()); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException("Unable to install " + MySocketImplFactory.class.getSimpleName(), + e); + } + } + + private final Constructor constructor; + private final Method accept; + private final Method bind; + private final Method available; + private final Method create; + private final Method connect1; + private final Method connect2; + private final Method connect3; + private final Method close; + private final Method getInputStream; + private final Method getOutputStream; + private final Method sendUrgentData; + private final Method listen; + + public MySocketImplFactory() throws ClassNotFoundException, SecurityException, + NoSuchMethodException { + super(); + Class c = Class.forName("java.net.PlainSocketImpl"); + constructor = c.getDeclaredConstructor(); + constructor.setAccessible(true); + accept = c.getDeclaredMethod("accept", SocketImpl.class); + accept.setAccessible(true); + bind = c.getDeclaredMethod("bind", InetAddress.class, Integer.TYPE); + bind.setAccessible(true); + available = c.getDeclaredMethod("available"); + available.setAccessible(true); + create = c.getDeclaredMethod("create", Boolean.TYPE); + create.setAccessible(true); + connect1 = c.getDeclaredMethod("connect", InetAddress.class, Integer.TYPE); + connect1.setAccessible(true); + connect2 = c.getDeclaredMethod("connect", SocketAddress.class, Integer.TYPE); + connect2.setAccessible(true); + connect3 = c.getDeclaredMethod("connect", String.class, Integer.TYPE); + connect3.setAccessible(true); + getInputStream = c.getDeclaredMethod("getInputStream"); + getInputStream.setAccessible(true); + getOutputStream = c.getDeclaredMethod("getOutputStream"); + getOutputStream.setAccessible(true); + close = c.getDeclaredMethod("close"); + close.setAccessible(true); + sendUrgentData = c.getDeclaredMethod("sendUrgentData", Integer.TYPE); + sendUrgentData.setAccessible(true); + listen = c.getDeclaredMethod("listen", Integer.TYPE); + listen.setAccessible(true); + } + + public SocketImpl createSocketImpl() { + try { + return new MySocketImpl(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private class MySocketImpl extends SocketImpl { + + private final SocketImpl si; + + private final int socketId; + + private MySocketImpl() throws IllegalArgumentException, InstantiationException, + IllegalAccessException, InvocationTargetException { + si = (SocketImpl) constructor.newInstance(); + socketId = si.hashCode(); + log.trace("[" + socketId + "] new SocketImpl created"); + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + + @Override + protected void accept(SocketImpl s) throws IOException { + log.trace("[" + socketId + "] accept(...)"); + try { + accept.invoke(si, s); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected int available() throws IOException { + // log.trace("[" + socketId + "] available()"); + try { + return ((Integer) bind.invoke(si)).intValue(); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void bind(InetAddress host, int port) throws IOException { + log.trace("[" + socketId + "] bind()"); + try { + bind.invoke(si, host, port); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void close() throws IOException { + log.trace("[" + socketId + "] close()"); + try { + close.invoke(si); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void connect(InetAddress address, int port) throws IOException { + log.trace("[" + socketId + "] connect1(..)"); + try { + connect1.invoke(si, address, port); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void connect(SocketAddress address, int timeout) throws IOException { + log.trace("[" + socketId + "] connect2(..)"); + try { + connect2.invoke(si, address, timeout); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void connect(String host, int port) throws IOException { + log.trace("[" + socketId + "] connect3(..)"); + try { + connect3.invoke(si, host, port); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void create(boolean stream) throws IOException { + log.trace("[" + socketId + "] create(..)"); + try { + create.invoke(si, stream); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected InputStream getInputStream() throws IOException { + // log.trace("[" + socketId + "] getInputStream()"); + try { + return (InputStream) getInputStream.invoke(si); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + public Object getOption(int optID) throws SocketException { + // log.trace("[" + socketId + "] getOption(..)"); + return si.getOption(optID); + } + + @Override + protected OutputStream getOutputStream() throws IOException { + // log.trace("[" + socketId + "] getOutputStream()"); + try { + return (OutputStream) getOutputStream.invoke(si); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void listen(int backlog) throws IOException { + log.trace("[" + socketId + "] listen(..)"); + try { + listen.invoke(si, backlog); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + @Override + protected void sendUrgentData(int data) throws IOException { + // log.trace("[" + socketId + "] sendUrgentData"); + try { + sendUrgentData.invoke(si, data); + } catch (Exception e) { + if (e instanceof IOException) + throw (IOException) e; + throw new RuntimeException(e); + } + } + + public void setOption(int optID, Object value) throws SocketException { + // log.trace("[" + socketId + "] setOption"); + si.setOption(optID, value); + } + + } +} diff --git a/src/main/java/mobac/utilities/file/DeleteFileFilter.java b/src/main/java/mobac/utilities/file/DeleteFileFilter.java new file mode 100644 index 0000000..2f07552 --- /dev/null +++ b/src/main/java/mobac/utilities/file/DeleteFileFilter.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; +import java.io.FileFilter; + +/** + * A {@link FileFilter} that deletes every file in the directory specified by + * the {@link File} on which {@link File#listFiles(FileFilter)} using + * {@link DeleteFileFilter} is executed. Therefore the {@link FileFilter} + * concept is abused as {@link File} enumerator. + *

+ * Example: new File("C:/Temp").listFiles(new DeleteFileFilter()); + * deletes all files but no directories in the directory C:\Temp. + *

+ */ +public class DeleteFileFilter implements FileFilter { + + int countSuccess = 0; + int countFailed = 0; + int countError = 0; + + public boolean accept(File file) { + try { + if (file.isDirectory()) + // We only delete files + return false; + boolean success = file.delete(); + if (success) + countSuccess++; + else + countFailed++; + } catch (Exception e) { + countError++; + } + // We don't care about the filter result + return false; + } + + public int getCountSuccess() { + return countSuccess; + } + + public int getCountFailed() { + return countFailed; + } + + public int getCountError() { + return countError; + } + + @Override + public String toString() { + return "Delete file filter status (success, failed, error): " + countSuccess + " / " + + countFailed + " / " + countError + " files"; + } + +} diff --git a/src/main/java/mobac/utilities/file/DirInfoFileFilter.java b/src/main/java/mobac/utilities/file/DirInfoFileFilter.java new file mode 100644 index 0000000..547aa7d --- /dev/null +++ b/src/main/java/mobac/utilities/file/DirInfoFileFilter.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; +import java.io.FileFilter; + +import mobac.utilities.Utilities; + + +public class DirInfoFileFilter implements FileFilter { + + long dirSize = 0; + int fileCount = 0; + + public DirInfoFileFilter() { + } + + public boolean accept(File f) { + if (f.isDirectory()) + return false; + Utilities.checkForInterruptionRt(); + dirSize += f.length(); + fileCount++; + return false; + } + + public long getDirSize() { + return dirSize; + } + + public int getFileCount() { + return fileCount; + } +} diff --git a/src/main/java/mobac/utilities/file/DirectoryFileFilter.java b/src/main/java/mobac/utilities/file/DirectoryFileFilter.java new file mode 100644 index 0000000..5a34a96 --- /dev/null +++ b/src/main/java/mobac/utilities/file/DirectoryFileFilter.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; +import java.io.FileFilter; + +/** + * A {@link FileFilter} only returning directories. + */ +public class DirectoryFileFilter implements FileFilter { + + public boolean accept(File f) { + return f.isDirectory(); + } +} diff --git a/src/main/java/mobac/utilities/file/FileExtFilter.java b/src/main/java/mobac/utilities/file/FileExtFilter.java new file mode 100644 index 0000000..5a0b04b --- /dev/null +++ b/src/main/java/mobac/utilities/file/FileExtFilter.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; +import java.io.FileFilter; + +public class FileExtFilter implements FileFilter { + + private final String acceptedFileExt; + + public FileExtFilter(String acceptedFileExt) { + this.acceptedFileExt = acceptedFileExt; + } + + public boolean accept(File pathname) { + return pathname.getName().endsWith(acceptedFileExt); + } + +} diff --git a/src/main/java/mobac/utilities/file/GpxFileFilter.java b/src/main/java/mobac/utilities/file/GpxFileFilter.java new file mode 100644 index 0000000..ba0f9ce --- /dev/null +++ b/src/main/java/mobac/utilities/file/GpxFileFilter.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; + +import javax.swing.filechooser.FileFilter; + +public class GpxFileFilter extends FileFilter { + + private boolean onlyGpx11; + + public GpxFileFilter(boolean onlyGpx11) { + this.onlyGpx11 = onlyGpx11; + } + + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().endsWith(".gpx"); + } + + @Override + public String getDescription() { + if (onlyGpx11) + return "GPX 1.1 files (*.gpx)"; + else + return "GPX 1.0/1.1 files (*.gpx)"; + + } + +} diff --git a/src/main/java/mobac/utilities/file/NamePatternFileFilter.java b/src/main/java/mobac/utilities/file/NamePatternFileFilter.java new file mode 100644 index 0000000..7db2d8d --- /dev/null +++ b/src/main/java/mobac/utilities/file/NamePatternFileFilter.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; +import java.io.FileFilter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NamePatternFileFilter implements FileFilter { + + protected final Pattern pattern; + + public NamePatternFileFilter(Pattern pattern) { + this.pattern = pattern; + } + + public NamePatternFileFilter(String regex) { + this.pattern = Pattern.compile(regex); + } + + public boolean accept(File pathname) { + Matcher m = pattern.matcher(pathname.getName()); + return m.matches(); + } + +} diff --git a/src/main/java/mobac/utilities/file/RegexFileFilter.java b/src/main/java/mobac/utilities/file/RegexFileFilter.java new file mode 100644 index 0000000..86ad546 --- /dev/null +++ b/src/main/java/mobac/utilities/file/RegexFileFilter.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.file; + +import java.io.File; +import java.io.FileFilter; +import java.util.regex.Pattern; + +public class RegexFileFilter implements FileFilter { + + private Pattern p; + + public RegexFileFilter(String regex) { + p = Pattern.compile(regex); + } + + public boolean accept(File pathname) { + return p.matcher(pathname.getName()).matches(); + } + +} diff --git a/src/main/java/mobac/utilities/geo/CoordinateDm2Format.java b/src/main/java/mobac/utilities/geo/CoordinateDm2Format.java new file mode 100644 index 0000000..dc2b552 --- /dev/null +++ b/src/main/java/mobac/utilities/geo/CoordinateDm2Format.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.geo; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; + +import org.apache.log4j.Logger; + +public class CoordinateDm2Format extends NumberFormat { + + protected static Logger log = Logger.getLogger(CoordinateDms2Format.class); + + NumberFormat degFmt; + NumberFormat minFmt; + NumberFormat minFmtParser; + + public CoordinateDm2Format(DecimalFormatSymbols dfs) { + degFmt = new DecimalFormat("00°", dfs); + minFmt = new DecimalFormat("00.00''", dfs); + minFmt.setRoundingMode(RoundingMode.FLOOR); + minFmtParser = new DecimalFormat("##.##", dfs); + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + int degrees; + if (number >= 0) + degrees = (int) Math.floor(number); + else + degrees = (int) Math.ceil(number); + double minutes = Math.abs((number - degrees) * 60); + if (number < 0 && degrees == 0) + toAppendTo.append("-"); + toAppendTo.append(degFmt.format(degrees) + " "); + toAppendTo.append(minFmt.format(minutes)); + return toAppendTo; + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Number parse(String source) throws ParseException { + return parse(source, new ParsePosition(0)); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + String[] tokens = source.trim().split("[°\\']"); + if (tokens.length != 2) + return null; + try { + String degStr = tokens[0].trim(); + int deg = Integer.parseInt(degStr); + double min = minFmtParser.parse(tokens[1].trim()).doubleValue(); + double coord; + if (degStr.startsWith("-")) + coord = deg - min / 60.0; + else + coord = deg + min / 60.0; + return new Double(coord); + } catch (Exception e) { + parsePosition.setErrorIndex(0); + log.error("e"); + return null; + } + } + +} diff --git a/src/main/java/mobac/utilities/geo/CoordinateDms2Format.java b/src/main/java/mobac/utilities/geo/CoordinateDms2Format.java new file mode 100644 index 0000000..69c83ab --- /dev/null +++ b/src/main/java/mobac/utilities/geo/CoordinateDms2Format.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.geo; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; + +import org.apache.log4j.Logger; + +public class CoordinateDms2Format extends NumberFormat { + + protected static Logger log = Logger.getLogger(CoordinateDms2Format.class); + + NumberFormat degFmt; + NumberFormat minFmt; + NumberFormat secFmt; + NumberFormat secFmtParser; + + public CoordinateDms2Format(DecimalFormatSymbols dfs) { + degFmt = new DecimalFormat("00°", dfs); + minFmt = new DecimalFormat("00''", dfs); + minFmt.setRoundingMode(RoundingMode.FLOOR); + secFmt = new DecimalFormat("00.00\"", dfs); + secFmt.setRoundingMode(RoundingMode.FLOOR); + secFmtParser = new DecimalFormat("##.##", dfs); + } + + @Override + public StringBuffer format(double numberOrg, StringBuffer toAppendTo, FieldPosition pos) { + double number = numberOrg; + int degrees; + int minutes; + double seconds; + if (number >= 0) + degrees = (int) Math.floor(number); + else + degrees = (int) Math.ceil(number); + number = Math.abs((number - degrees) * 60); + minutes = (int) Math.floor(number); + seconds = (number - minutes) * 60; + if (numberOrg < 0 && degrees == 0) + toAppendTo.append("-"); + toAppendTo.append(degFmt.format(degrees) + " "); + toAppendTo.append(minFmt.format(minutes) + " "); + toAppendTo.append(secFmt.format(seconds)); + return toAppendTo; + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Number parse(String source) throws ParseException { + return parse(source, new ParsePosition(0)); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + String[] tokens = source.trim().split("[°\\'\\\"]"); + if (tokens.length != 3) + return null; + try { + String degStr = tokens[0].trim(); + int deg = Integer.parseInt(degStr); + int min = Integer.parseInt(tokens[1].trim()); + double sec = secFmtParser.parse(tokens[2].trim()).doubleValue(); + double coord; + if (degStr.startsWith("-")) + coord = deg - sec / 3600 - min / 60.0; + else + coord = deg + sec / 3600 + min / 60.0; + return new Double(coord); + } catch (Exception e) { + parsePosition.setErrorIndex(0); + log.error("e"); + return null; + } + } + +} diff --git a/src/main/java/mobac/utilities/geo/CoordinateTileFormat.java b/src/main/java/mobac/utilities/geo/CoordinateTileFormat.java new file mode 100644 index 0000000..7eed311 --- /dev/null +++ b/src/main/java/mobac/utilities/geo/CoordinateTileFormat.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.geo; + +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParsePosition; + +import mobac.gui.MainGUI; +import mobac.program.interfaces.MapSpace; + +import org.apache.log4j.Logger; + +public class CoordinateTileFormat extends NumberFormat { + + protected static Logger log = Logger.getLogger(CoordinateTileFormat.class); + + private final boolean isLongitude; + + public CoordinateTileFormat(boolean isLongitude) { + this.isLongitude = isLongitude; + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + MainGUI gui = MainGUI.getMainGUI(); + MapSpace mapSpace = gui.getSelectedMapSource().getMapSpace(); + int zoom = gui.previewMap.getZoom(); + int tileNum = 0; + if (isLongitude) + tileNum = mapSpace.cLonToX(number, zoom); + else + tileNum = mapSpace.cLatToY(number, zoom); + toAppendTo.append(String.format("%d / z%d ", tileNum / mapSpace.getTileSize(), zoom)); + return toAppendTo; + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + MainGUI gui = MainGUI.getMainGUI(); + MapSpace mapSpace = gui.getSelectedMapSource().getMapSpace(); + try { + String[] tokens = source.trim().split("/"); + int zoom = 0; + int tileNum = 0; + if (tokens.length == 2) { + String s = tokens[1].trim(); + if (s.startsWith("z")) + s = s.substring(1); + zoom = Integer.parseInt(s); + } else { + zoom = gui.previewMap.getZoom(); + } + if (tokens.length > 0) { + String s = tokens[0]; + s = s.trim(); + if ((s.indexOf('.') < 0) && (s.indexOf(',') < 0)) { + tileNum = Integer.parseInt(s); + tileNum *= mapSpace.getTileSize(); + } else { + double num = Double.parseDouble(s); + tileNum = (int) (num * mapSpace.getTileSize()); + } + } + parsePosition.setIndex(source.length()); + if (isLongitude) + return mapSpace.cXToLon(tileNum, zoom); + return mapSpace.cYToLat(tileNum, zoom); + } catch (Exception e) { + parsePosition.setErrorIndex(0); + log.error("e"); + return null; + } + } +} diff --git a/src/main/java/mobac/utilities/geo/GeoUtils.java b/src/main/java/mobac/utilities/geo/GeoUtils.java new file mode 100644 index 0000000..423a11a --- /dev/null +++ b/src/main/java/mobac/utilities/geo/GeoUtils.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.geo; + +import java.util.Locale; + +public class GeoUtils { + + public static String getDegMinFormat(double coord, boolean isLatitude) { + + boolean neg = (coord < 0.0); + coord = Math.abs(coord); + int deg = (int) coord; + double min = (coord - deg) * 60.0; + + String degMinFormat = "%d, %3.6f, %c"; + + char dirC; + if (isLatitude) + dirC = (neg ? 'S' : 'N'); + else + dirC = (neg ? 'W' : 'E'); + + return String.format(Locale.ENGLISH, degMinFormat, deg, min, dirC); + } + +} diff --git a/src/main/java/mobac/utilities/imageio/Png4BitWriter.java b/src/main/java/mobac/utilities/imageio/Png4BitWriter.java new file mode 100644 index 0000000..0f93533 --- /dev/null +++ b/src/main/java/mobac/utilities/imageio/Png4BitWriter.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2007 Matthias Mann - www.matthiasmann.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package mobac.utilities.imageio; + +import static mobac.utilities.imageio.PngConstants.COLOR_PALETTE; +import static mobac.utilities.imageio.PngConstants.COMPRESSION_DEFLATE; +import static mobac.utilities.imageio.PngConstants.FILTER_SET_1; +import static mobac.utilities.imageio.PngConstants.FILTER_TYPE_NONE; +import static mobac.utilities.imageio.PngConstants.IDAT; +import static mobac.utilities.imageio.PngConstants.IEND; +import static mobac.utilities.imageio.PngConstants.IHDR; +import static mobac.utilities.imageio.PngConstants.INTERLACE_NONE; +import static mobac.utilities.imageio.PngConstants.PLTE; +import static mobac.utilities.imageio.PngConstants.SIGNATURE; +import static mobac.utilities.imageio.PngConstants.TEXT; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import mobac.utilities.MyMath; + +/** + * 4 Bit PNG Writer + *

+ * Writes a color png image with pallette containing 16 colors. Currently the image data is saved without any PNG + * filtering. + *

+ * + * Bases on the PNGWriter written by Matthias Mann - www.matthiasmann.de + * + * @author r_x + */ +public class Png4BitWriter { + + public static void writeImage(File file, BufferedImage image) throws IOException { + FileOutputStream out = new FileOutputStream(file); + try { + writeImage(out, image); + } finally { + out.close(); + } + } + + /** + * + * @param out + * @param image + * Must be an image with {@link ColorModel} {@link IndexColorModel} + * @throws IOException + */ + public static void writeImage(OutputStream out, BufferedImage image) throws IOException { + writeImage(out, image, Deflater.BEST_COMPRESSION); + } + + /** + * + * @param out + * @param image + * Must be an image with {@link ColorModel} {@link IndexColorModel} + * @param compression + * deflater method used for compression - possible values are for example + * {@link Deflater#BEST_COMPRESSION}, {@link Deflater#BEST_SPEED},{@link Deflater#NO_COMPRESSION} + * @throws IOException + */ + public static void writeImage(OutputStream out, BufferedImage image, int compression) throws IOException { + writeImage(out, image, compression, null); + } + + /** + * + * @param out + * Must be an image with {@link ColorModel} {@link IndexColorModel} + * @param image + * deflater method used for compression - possible values are for example + * {@link Deflater#BEST_COMPRESSION}, {@link Deflater#BEST_SPEED},{@link Deflater#NO_COMPRESSION} + * @param description + * PNG comment text (meta info) + * @throws IOException + */ + public static void writeImage(OutputStream out, BufferedImage image, int compression, String description) + throws IOException { + DataOutputStream dos = new DataOutputStream(out); + + int width = image.getWidth(); + int height = image.getHeight(); + + ColorModel cm = image.getColorModel(); + + if (!(cm instanceof IndexColorModel)) + throw new UnsupportedOperationException("Image format not compatible"); + + IndexColorModel palette = (IndexColorModel) cm; + + dos.write(SIGNATURE); + PngChunk cIHDR = new PngChunk(IHDR); + cIHDR.writeInt(width); + cIHDR.writeInt(height); + cIHDR.writeByte(4); // 4 bit per component + cIHDR.writeByte(COLOR_PALETTE); + cIHDR.writeByte(COMPRESSION_DEFLATE); + cIHDR.writeByte(FILTER_SET_1); + cIHDR.writeByte(INTERLACE_NONE); + cIHDR.writeTo(dos); + + if (description != null) { + PngChunk cTxT = new PngChunk(TEXT); + cTxT.write("Description".getBytes()); + cTxT.write(0); + cTxT.write(description.getBytes()); + cTxT.writeTo(dos); + } + + PngChunk cPLTE = new PngChunk(PLTE); + int paletteEntries = palette.getMapSize(); + byte[] r = new byte[paletteEntries]; + byte[] g = new byte[paletteEntries]; + byte[] b = new byte[paletteEntries]; + palette.getReds(r); + palette.getGreens(g); + palette.getBlues(b); + int colorCount = Math.min(paletteEntries, 16); + for (int i = 0; i < colorCount; i++) { + cPLTE.writeByte(r[i]); + cPLTE.writeByte(g[i]); + cPLTE.writeByte(b[i]); + } + cPLTE.writeTo(dos); + + PngChunk cIDAT = new PngChunk(IDAT); + DeflaterOutputStream dfos = new DeflaterOutputStream(cIDAT, new Deflater(compression)); + + int lineLen = MyMath.divCeil(width, 2); + byte[] lineOut = new byte[lineLen]; + int[] samples = null; + + for (int line = 0; line < height; line++) { + dfos.write(FILTER_TYPE_NONE); + + // Get the samples for the next line - each byte is one sample/pixel + samples = image.getRaster().getPixels(0, line, width, 1, samples); + int sx = 0; + int iMax = samples.length - 2; + for (int i = 0; i < samples.length; i += 2) { + // Now we are packing two samples of 4 bit into one byte + int sample1 = samples[i]; + int sample2 = (i <= iMax) ? samples[i + 1] : 0; + int s1 = sample1 & 0x0F; + int s2 = sample2 & 0x0F; + if ((s1 != sample1) || (s2 != sample2)) + throw new RuntimeException("sample has more than 4 bit!"); + lineOut[sx++] = (byte) ((s1 << 4) | s2); + } + dfos.write(lineOut); + } + + dfos.finish(); + cIDAT.writeTo(dos); + + PngChunk cIEND = new PngChunk(IEND); + cIEND.writeTo(dos); + cIEND.close(); + + dos.flush(); + } + + protected static void writeColor(DataOutputStream dos, Color c) throws IOException { + dos.writeByte(c.getRed()); + dos.writeByte(c.getGreen()); + dos.writeByte(c.getBlue()); + } + +} diff --git a/src/main/java/mobac/utilities/imageio/PngChunk.java b/src/main/java/mobac/utilities/imageio/PngChunk.java new file mode 100644 index 0000000..81e0560 --- /dev/null +++ b/src/main/java/mobac/utilities/imageio/PngChunk.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.imageio; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.zip.CRC32; +import java.util.zip.CheckedOutputStream; + +class PngChunk extends DataOutputStream { + + final CRC32 crc; + final ByteArrayOutputStream baos; + + PngChunk(int chunkType) throws IOException { + this(chunkType, new ByteArrayOutputStream(), new CRC32()); + } + + private PngChunk(int chunkType, ByteArrayOutputStream baos, CRC32 crc) throws IOException { + super(new CheckedOutputStream(baos, crc)); + this.crc = crc; + this.baos = baos; + + writeInt(chunkType); + } + + public void writeTo(DataOutputStream out) throws IOException { + flush(); + out.writeInt(baos.size() - 4); + baos.writeTo(out); + out.writeInt((int) crc.getValue()); + } +} diff --git a/src/main/java/mobac/utilities/imageio/PngConstants.java b/src/main/java/mobac/utilities/imageio/PngConstants.java new file mode 100644 index 0000000..3dc3176 --- /dev/null +++ b/src/main/java/mobac/utilities/imageio/PngConstants.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.imageio; + +/** + * Common constants used in the PNG file format. http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.IHDR + */ +public class PngConstants { + + /** + * PNG magic file bytes + */ + public static final byte[] SIGNATURE = { (byte) 137, 80, 78, 71, 13, 10, 26, 10 }; + + /** + * Chunk header identifiers + */ + public static final int IHDR = (int) 0x49484452; // png header + public static final int TEXT = (int) 0x74455874; // textual information + public static final int PLTE = (int) 0x504C5445; // color palette + public static final int IDAT = (int) 0x49444154; // image data + public static final int IEND = (int) 0x49454E44; // end of file + + /** + * Each pixel is a grayscale sample. Allowed bit depths: 1,2,4,8,16 + */ + public static final byte COLOR_GRAYSCALE = 0; + + /** + * Each pixel is an R,G,B triple. Allowed bit depths: 8,16 + */ + public static final byte COLOR_TRUECOLOR = 2; + + /** + * Each pixel is a palette index; a PLTE chunk must appear. Allowed bit depths: 1,2,4,8 + */ + public static final byte COLOR_PALETTE = 3; + + /** + * Each pixel is a grayscale sample, followed by an alpha sample. Allowed bit depths: 8,16 + */ + public static final byte COLOR_GRAYSCALE_ALPHA = 4; + + /** + * Each pixel is an R,G,B triple, followed by an alpha sample. Allowed bit depths: 6 8,16 + */ + public static final byte COLOR_TRUECOLOR_ALPHA = 6; + + public static final byte COMPRESSION_DEFLATE = 0; + + public static final byte FILTER_SET_1 = 0; + + public static final byte INTERLACE_NONE = 0; + + public static final byte FILTER_TYPE_NONE = 0; + public static final byte FILTER_TYPE_SUB = 1; + public static final byte FILTER_TYPE_UP = 2; + public static final byte FILTER_TYPE_AVERAGE = 0; + public static final byte FILTER_TYPE_PAETH = 4; + +} diff --git a/src/main/java/mobac/utilities/imageio/PngXxlWriter.java b/src/main/java/mobac/utilities/imageio/PngXxlWriter.java new file mode 100644 index 0000000..93846e1 --- /dev/null +++ b/src/main/java/mobac/utilities/imageio/PngXxlWriter.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.imageio; + +/* + * PNGWriter.java + * + * Copyright (c) 2007 Matthias Mann - www.matthiasmann.de + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import static mobac.utilities.imageio.PngConstants.*; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.awt.image.DirectColorModel; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +import javax.activation.UnsupportedDataTypeException; + +/** + * A PNG writer that is able to write extra large PNG images using incremental + * writing. + *

+ * The image is processed incremental in "tile lines" - e.g. an PNG image of + * 30000 x 20000 pixels (width x height) can be written by 200 "tile lines" of + * size 30000 x 100 pixels. Each tile line can be written via the method + * {@link #writeTileLine(BufferedImage)}. After writing the last line you have + * to call {@link #finish()} which will write the final PNG structure + * information into the {@link OutputStream}. + *

+ *

+ * Please note that this writer creates 24bit/truecolor PNGs. Transparency and + * alpha masks are not supported. + *

+ * Bases on the PNGWriter written by Matthias Mann - www.matthiasmann.de + * + * @author r_x + */ +public class PngXxlWriter { + + private static final int BUFFER_SIZE = 128 * 1024; + + private int width; + private int height; + private DataOutputStream dos; + + ImageDataChunkWriter imageDataChunkWriter; + + /** + * Creates an PNG writer instance for an image with the specified width and + * height. + * + * @param width + * width of the PNG image to be written + * @param height + * height of the PNG image to be written + * @param os + * destination to write the PNG image data to + * @throws IOException + */ + public PngXxlWriter(int width, int height, OutputStream os) throws IOException { + this.width = width; + this.height = height; + this.dos = new DataOutputStream(os); + + dos.write(SIGNATURE); + + PngChunk cIHDR = new PngChunk(IHDR); + cIHDR.writeInt(this.width); + cIHDR.writeInt(this.height); + cIHDR.writeByte(8); // 8 bit per component + cIHDR.writeByte(COLOR_TRUECOLOR); + cIHDR.writeByte(COMPRESSION_DEFLATE); + cIHDR.writeByte(FILTER_SET_1); + cIHDR.writeByte(INTERLACE_NONE); + cIHDR.writeTo(dos); + imageDataChunkWriter = new ImageDataChunkWriter(dos); + } + + /** + * + * @param tileLineImage + * @throws IOException + */ + public void writeTileLine(BufferedImage tileLineImage) throws IOException { + + int tileLineHeight = tileLineImage.getHeight(); + int tileLineWidth = tileLineImage.getWidth(); + + if (width != tileLineWidth) + throw new RuntimeException("Invalid width"); + + ColorModel cm = tileLineImage.getColorModel(); + + if (!(cm instanceof DirectColorModel)) + throw new UnsupportedDataTypeException( + "Image uses wrong color model. Only DirectColorModel is supported!"); + + // We process the image line by line, from head to bottom + Rectangle rect = new Rectangle(0, 0, tileLineWidth, 1); + + DataOutputStream imageDataStream = imageDataChunkWriter.getStream(); + + byte[] curLine = new byte[width * 3]; + for (int line = 0; line < tileLineHeight; line++) { + rect.y = line; + DataBuffer db = tileLineImage.getData(rect).getDataBuffer(); + if (db.getNumBanks() > 1) + throw new UnsupportedDataTypeException("Image data has more than one data bank"); + if (db instanceof DataBufferByte) + curLine = ((DataBufferByte) db).getData(); + else if (db instanceof DataBufferInt) { + int[] intLine = ((DataBufferInt) db).getData(); + int c = 0; + for (int i = 0; i < intLine.length; i++) { + int pixel = intLine[i]; + curLine[c++] = (byte) (pixel >> 16 & 0xFF); + curLine[c++] = (byte) (pixel >> 8 & 0xFF); + curLine[c++] = (byte) (pixel & 0xFF); + } + } else + throw new UnsupportedDataTypeException(db.getClass().getName()); + + imageDataStream.write(FILTER_TYPE_NONE); + imageDataStream.write(curLine); + } + } + + public void finish() throws IOException { + imageDataChunkWriter.finish(); + PngChunk cIEND = new PngChunk(IEND); + cIEND.writeTo(dos); + cIEND.close(); + dos.flush(); + } + + static class ImageDataChunkWriter extends OutputStream { + + DeflaterOutputStream dfos; + DataOutputStream stream; + DataOutputStream out; + CRC32 crc = new CRC32(); + + public ImageDataChunkWriter(DataOutputStream out) throws IOException { + this.out = out; + dfos = new DeflaterOutputStream(new BufferedOutputStream(this, BUFFER_SIZE), + new Deflater(Deflater.BEST_COMPRESSION)); + stream = new DataOutputStream(dfos); + } + + public DataOutputStream getStream() { + return stream; + } + + public void finish() throws IOException { + stream.flush(); + stream.close(); + dfos.finish(); + dfos = null; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + crc.reset(); + out.writeInt(len); + out.writeInt(IDAT); + out.write(b, off, len); + crc.update("IDAT".getBytes()); + crc.update(b, off, len); + out.writeInt((int) crc.getValue()); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(int b) throws IOException { + throw new IOException("Simgle byte writing not supported"); + } + } +} diff --git a/src/main/java/mobac/utilities/jdbc/DriverProxy.java b/src/main/java/mobac/utilities/jdbc/DriverProxy.java new file mode 100644 index 0000000..6f779cd --- /dev/null +++ b/src/main/java/mobac/utilities/jdbc/DriverProxy.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.jdbc; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Properties; + +import org.apache.log4j.Logger; + +/** + * Proxies all calls to {@link Driver} that has been loaded using a custom {@link ClassLoader}. This is necessary as the + * SQL {@link DriverManager} does only accept drivers loaded by the SystemClassLoader. + */ +public class DriverProxy implements Driver { + + private static Logger log = Logger.getLogger(DriverProxy.class); + + private final Driver driver; + + @SuppressWarnings("unchecked") + public DriverProxy(String className, ClassLoader classLoader) throws ClassNotFoundException, + InstantiationException, IllegalAccessException { + Class c = (Class) classLoader.loadClass(className); + driver = c.newInstance(); + log.info("SQL driver loaded: v" + driver.getMajorVersion() + "." + driver.getMinorVersion() + " [" + + driver.getClass().getName() + "]"); + } + + public static void loadSQLDriver(String className, ClassLoader classLoader) throws ClassNotFoundException, + InstantiationException, IllegalAccessException, SQLException { + DriverProxy driver = new DriverProxy(className, classLoader); + DriverManager.registerDriver(driver); + } + + public boolean acceptsURL(String url) throws SQLException { + return driver.acceptsURL(url); + } + + public Connection connect(String url, Properties info) throws SQLException { + return driver.connect(url, info); + } + + public int getMajorVersion() { + return driver.getMajorVersion(); + } + + public int getMinorVersion() { + return driver.getMinorVersion(); + } + + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return driver.getPropertyInfo(url, info); + } + + public boolean jdbcCompliant() { + return driver.jdbcCompliant(); + } + + /** + * Required for Java 7 + */ + public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + +} diff --git a/src/main/java/mobac/utilities/jdbc/SQLiteLoader.java b/src/main/java/mobac/utilities/jdbc/SQLiteLoader.java new file mode 100644 index 0000000..6cf870a --- /dev/null +++ b/src/main/java/mobac/utilities/jdbc/SQLiteLoader.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.swing.JOptionPane; + +import mobac.utilities.I18nUtils; + +import org.apache.log4j.Logger; + +/** + * Dynamic loading of SqliteJDBC http://www.zentus.com/sqlitejdbc/ + */ +public class SQLiteLoader { + + private static final Logger log = Logger.getLogger(SQLiteLoader.class); + + private static boolean SQLITE_LOADED = false; + + private static final String SQLITE_DRIVERNAME1 = "SQLite.JDBCDriver"; + private static final String SQLITE_DRIVERNAME2 = "org.sqlite.JDBC"; + + public static boolean loadSQLiteOrShowError() { + try { + SQLiteLoader.loadSQLite(); + return true; + } catch (Exception e) { + JOptionPane.showMessageDialog(null, SQLiteLoader.getMsgSqliteMissing(), + I18nUtils.localizedStringForKey("msg_environment_slqite_lib_missing_title"), + JOptionPane.ERROR_MESSAGE); + return false; + } + } + + public static String getMsgSqliteMissing() { + return I18nUtils.localizedStringForKey("msg_environment_slqite_lib_missing"); + } + + public static synchronized void loadSQLite() throws SQLException { + try { + SQLiteLoader.loadSQLite(SQLITE_DRIVERNAME1); + } catch (Exception e) { + } + SQLiteLoader.loadSQLite(SQLITE_DRIVERNAME2); + } + + protected static synchronized void loadSQLite(String driverClassName) throws SQLException { + if (SQLITE_LOADED) + return; + try { + // Load the sqlite library + Class.forName(driverClassName); + SQLITE_LOADED = true; + log.debug("SQLite library loaded. Driver class name: " + driverClassName); + return; + } catch (Throwable t) { + SQLException e = new SQLException("Loading of SQLite library failed (" + driverClassName + "): " + + t.getMessage(), t); + log.error(e.getMessage()); + throw e; + } + } + + public static void closeConnection(Connection conn) { + if (conn == null) + return; + try { + conn.close(); + } catch (Exception e) { + log.error("Failed to close SQL connection: " + e.getMessage()); + } + } + +} diff --git a/src/main/java/mobac/utilities/stream/ArrayOutputStream.java b/src/main/java/mobac/utilities/stream/ArrayOutputStream.java new file mode 100644 index 0000000..fb67a71 --- /dev/null +++ b/src/main/java/mobac/utilities/stream/ArrayOutputStream.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.stream; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple fixed-size version of {@link ByteArrayOutputStream}. + */ +public class ArrayOutputStream extends OutputStream { + + protected byte[] buf; + + protected int pos = 0; + + /** + * @param size + * Size of the buffer available for writing. + */ + public ArrayOutputStream(int size) { + buf = new byte[size]; + } + + /** + * @param array + * Byte array used for writing to + */ + public ArrayOutputStream(byte[] array) { + buf = array; + } + + /** + * + * @param array + * Byte array used for writing to + * @param off + * offset in array + */ + public ArrayOutputStream(byte[] array, int off) { + buf = array; + pos = off; + } + + public byte[] toByteArray() { + byte[] data = new byte[pos]; + System.arraycopy(buf, 0, data, 0, pos); + return data; + } + + public void reset() { + pos = 0; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + int newPos = pos + len; + if (newPos > buf.length) + throw new IOException("End of buffer reached"); + System.arraycopy(b, off, buf, pos, len); + pos = newPos; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte) b }, 0, 1); + } + +} diff --git a/src/main/java/mobac/utilities/stream/CountingOutputStream.java b/src/main/java/mobac/utilities/stream/CountingOutputStream.java new file mode 100644 index 0000000..000c347 --- /dev/null +++ b/src/main/java/mobac/utilities/stream/CountingOutputStream.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.stream; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class CountingOutputStream extends FilterOutputStream { + + private long bytesWritten = 0; + + public CountingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + out.write(b); + bytesWritten++; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + bytesWritten += len; + } + + @Override + public void write(byte[] b) throws IOException { + out.write(b); + bytesWritten += b.length; + } + + public long getBytesWritten() { + return bytesWritten; + } + +} diff --git a/src/main/java/mobac/utilities/stream/LittleEndianOutputStream.java b/src/main/java/mobac/utilities/stream/LittleEndianOutputStream.java new file mode 100644 index 0000000..436ba62 --- /dev/null +++ b/src/main/java/mobac/utilities/stream/LittleEndianOutputStream.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.stream; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class LittleEndianOutputStream extends FilterOutputStream { + + public LittleEndianOutputStream(OutputStream out) { + super(out); + } + + /** + * Write an int, 32-bits. Like DataOutputStream.writeInt. + * + * @param v + * the int to write + * + * @throws IOException + * if write fails. + */ + public final void writeInt(int v) throws IOException { + byte[] work = new byte[4]; + work[0] = (byte) (v & 0xFF); + work[1] = (byte) ((v >> 8) & 0xFF); + work[2] = (byte) ((v >> 16) & 0xFF); + work[3] = (byte) ((v >> 24) & 0xFF); + out.write(work, 0, 4); + } + + /** + * Write a double. + * + * @param v + * the double to write. Like DataOutputStream.writeDouble. + * + * @throws IOException + * if write fails. + */ + public final void writeDouble(double v) throws IOException { + writeLong(Double.doubleToLongBits(v)); + } + + /** + * Write a long, 64-bits. like DataOutputStream.writeLong. + * + * @param v + * the long to write + * + * @throws IOException + * if write fails. + */ + public final void writeLong(long v) throws IOException { + byte[] work = new byte[8]; + work[0] = (byte) v; + work[1] = (byte) (v >> 8); + work[2] = (byte) (v >> 16); + work[3] = (byte) (v >> 24); + work[4] = (byte) (v >> 32); + work[5] = (byte) (v >> 40); + work[6] = (byte) (v >> 48); + work[7] = (byte) (v >> 56); + out.write(work); + } + +} diff --git a/src/main/java/mobac/utilities/stream/RandomAccessFileOutputStream.java b/src/main/java/mobac/utilities/stream/RandomAccessFileOutputStream.java new file mode 100644 index 0000000..0634b59 --- /dev/null +++ b/src/main/java/mobac/utilities/stream/RandomAccessFileOutputStream.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.stream; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * Allows to write to an {@link RandomAccessFile} through an + * {@link OutputStream}. + * + *

+ * Notes: + *

    + *
  • Closing this stream does not have any effect on the underlying + * {@link RandomAccessFile}.
  • + *
  • Seeking or changing the {@link RandomAccessFile} file position directly + * affects {@link RandomAccessFileOutputStream}.
  • + *
+ * + */ +public class RandomAccessFileOutputStream extends OutputStream { + + private final RandomAccessFile file; + + public RandomAccessFileOutputStream(RandomAccessFile f) { + this.file = f; + } + + @Override + public void write(int b) throws IOException { + file.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + file.write(b, off, len); + } + + @Override + public void write(byte[] b) throws IOException { + file.write(b); + } + + public RandomAccessFile getFile() { + return file; + } + +} diff --git a/src/main/java/mobac/utilities/stream/ThrottleSupport.java b/src/main/java/mobac/utilities/stream/ThrottleSupport.java new file mode 100644 index 0000000..9d6ed27 --- /dev/null +++ b/src/main/java/mobac/utilities/stream/ThrottleSupport.java @@ -0,0 +1,118 @@ +/* + * XNap - A P2P framework and client. + * + * See the file AUTHORS for copyright information. + * + * 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. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package mobac.utilities.stream; + +import org.apache.log4j.Logger; + +/** + * Provides a throttle implementation based on time ticks. Clients can allocate a limited amount of bytes for each tick. + * If all bandwidth has been allocated clients are stalled until the next tick. + */ +public class ThrottleSupport { + + // --- Constant(s) --- + + /** + * The default length of a tick. + */ + public static final long TICK_LENGTH = 512; // = 2^9 + + // --- Data field(s) --- + + private static Logger logger = Logger.getLogger(ThrottleSupport.class); + + /** + * bytes per tick + */ + protected long bandwidth = 0; + protected long allocated = 0; + protected long tick = 0; + protected Object lock = new Object(); + + // --- Constructor(s) --- + + public ThrottleSupport() { + } + + /** + * Sets the maximum bandwidth. + * + * @param bandwidth + * byte / s + */ + public void setBandwidth(long bandwidth) { + synchronized (lock) { + this.bandwidth = (bandwidth * TICK_LENGTH) / 1024; + } + } + + /** + * Returns the number of bytes that the calling thread is allowed to send. Blocks until at least one byte could be + * allocated. + * + * @return -1, if interrupted; + */ + public int allocate(int bytes) { + if (bytes == 0) + return 0; + synchronized (lock) { + while (true) { + if (bandwidth == 0) { + // no limit + return bytes; + } + long currentTick = System.currentTimeMillis() >> 9; + if (currentTick > tick) { + logger.debug("* new tick: " + bandwidth + " to allocate *"); + tick = currentTick; + allocated = 0; + lock.notifyAll(); + } + if (bytes < bandwidth - allocated) { + // we still have some bandwidth left + allocated += bytes; + logger.debug("returning " + bytes + " allocated now " + allocated); + return bytes; + } + if (bandwidth - allocated > 0) { + // don't have enough, but return all we have left + bytes = (int) (bandwidth - allocated); + allocated = bandwidth; + logger.debug("returning " + bytes + " allocated now " + allocated); + return bytes; + } + + // we could not allocate any bandwidth, wait until the next + // tick is started + + // this is a bit too long + long t = TICK_LENGTH - (System.currentTimeMillis() % TICK_LENGTH); + if (t > 0) { + try { + logger.debug("waiting for " + t); + lock.wait(t); + } catch (InterruptedException e) { + return -1; + } + } + } + } + } +} diff --git a/src/main/java/mobac/utilities/stream/ThrottledInputStream.java b/src/main/java/mobac/utilities/stream/ThrottledInputStream.java new file mode 100644 index 0000000..611ae6b --- /dev/null +++ b/src/main/java/mobac/utilities/stream/ThrottledInputStream.java @@ -0,0 +1,61 @@ +/* + * XNap - A P2P framework and client. + * + * See the file AUTHORS for copyright information. + * + * 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. + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package mobac.utilities.stream; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * The global throtteled InputStream. All downloads should pipe their data through this stream. + */ +public class ThrottledInputStream extends FilterInputStream { + + // --- Data Field(s) --- + + private static ThrottleSupport ts = new ThrottleSupport(); + private int unused = 0; + + // --- Constructor(s) --- + + /** + * @param in + * {@link InputStream} with implemented/working {@link InputStream#available()} method. + */ + public ThrottledInputStream(InputStream in) { + super(in); + } + + // --- Method(s) --- + + public static void setBandwidth(long newValue) { + ts.setBandwidth(newValue); + } + + public int read(byte[] b, int off, int len) throws IOException { + int allowedlen = ts.allocate(len); + if (allowedlen < len) { + allowedlen = Math.min(allowedlen + unused, len); + } + int read = in.read(b, off, allowedlen); + unused = len - read; + return read; + } +} diff --git a/src/main/java/mobac/utilities/stream/ZipStoreOutputStream.java b/src/main/java/mobac/utilities/stream/ZipStoreOutputStream.java new file mode 100644 index 0000000..c2df4e6 --- /dev/null +++ b/src/main/java/mobac/utilities/stream/ZipStoreOutputStream.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.stream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ZipStoreOutputStream extends ZipOutputStream { + + private CRC32 crc = new CRC32(); + + public ZipStoreOutputStream(OutputStream out) { + super(out); + } + + public ZipStoreOutputStream(File f) throws FileNotFoundException { + super(new FileOutputStream(f)); + } + + /** + * + * Warning this method is not thread safe! + * + * @param name file name including path in the zip + * @param data + * @throws IOException + */ + public void writeStoredEntry(String name, byte[] data) throws IOException { + ZipEntry ze = new ZipEntry(name); + ze.setMethod(ZipEntry.STORED); + ze.setCompressedSize(data.length); + ze.setSize(data.length); + crc.reset(); + crc.update(data); + ze.setCrc(crc.getValue()); + putNextEntry(ze); + write(data); + closeEntry(); + } +} diff --git a/src/main/java/mobac/utilities/tar/TarArchive.java b/src/main/java/mobac/utilities/tar/TarArchive.java new file mode 100644 index 0000000..e6f3437 --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarArchive.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import mobac.utilities.stream.CountingOutputStream; + + +/** + * Creates a new tar file and allows to add files from the file system or + * "virtual files" that only exist in memory as byte[]. + */ +public class TarArchive { + + protected CountingOutputStream tarFileStream; + protected File tarFile; + protected File baseDir; + + /** + * + * @param tarFile + * @param baseDir + * root directory used for getting the relative path when adding + * a file from the file system. If only in memory files are added + * this parameter can be null + * @throws FileNotFoundException + */ + public TarArchive(File tarFile, File baseDir) throws FileNotFoundException { + this.tarFile = tarFile; + this.tarFileStream = new CountingOutputStream(new BufferedOutputStream( + new FileOutputStream(tarFile, false))); + this.baseDir = baseDir; + } + + public long getTarFilePos() { + return tarFileStream.getBytesWritten(); + } + + public boolean writeContentFromDir(File dirToAdd) throws IOException { + if (!dirToAdd.isDirectory()) + return false; + TarHeader th = new TarHeader(dirToAdd, baseDir); + writeTarHeader(th); + File[] files = dirToAdd.listFiles(); + Arrays.sort(files); + for (File f : files) { + if (!f.isDirectory()) + writeFile(f); + else + writeContentFromDir(f); + } + return true; + } + + public void writeFile(File fileOrDirToAdd) throws IOException { + TarHeader th = new TarHeader(fileOrDirToAdd, baseDir); + writeTarHeader(th); + + if (!fileOrDirToAdd.isDirectory()) { + TarRecord tr = new TarRecord(fileOrDirToAdd); + tarFileStream.write(tr.getRecordContent()); + } + } + + public void writeDirectory(String dirName) throws IOException { + TarHeader th = new TarHeader(dirName, 0, true); + writeTarHeader(th); + } + + /** + * Writes a "file" into tar archive that does only exists in memory + * + * @param fileName + * @param data + * @throws IOException + */ + public void writeFileFromData(String fileName, byte[] data) throws IOException { + writeFileFromData(fileName, data, 0, data.length); + } + + /** + * Writes a "file" into tar archive that does only exists in memory + * + * @param fileName + * @param data + * @param off + * @param len + * @throws IOException + */ + public void writeFileFromData(String fileName, byte[] data, int off, int len) + throws IOException { + TarHeader th = new TarHeader(fileName, len, false); + writeTarHeader(th); + TarRecord tr = new TarRecord(data, off, len); + tarFileStream.write(tr.getRecordContent()); + } + + protected void writeTarHeader(TarHeader th) throws IOException { + tarFileStream.write(th.getBytes()); + } + + public void writeEndofArchive() throws IOException { + byte[] endOfArchive = new byte[1024]; + tarFileStream.write(endOfArchive); + tarFileStream.flush(); + } + + public void close() { + try { + tarFileStream.close(); + } catch (Exception e) { + } + tarFileStream = null; + } + + public File getTarFile() { + return tarFile; + } + +} diff --git a/src/main/java/mobac/utilities/tar/TarHeader.java b/src/main/java/mobac/utilities/tar/TarHeader.java new file mode 100644 index 0000000..8ad80a8 --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarHeader.java @@ -0,0 +1,290 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.io.File; +import java.io.UnsupportedEncodingException; + +public class TarHeader { + + // private static Logger log = Logger.getLogger(TarHeader.class); + + private File baseFilePath; + + private int fileNameLength; + private final char[] fileName = new char[100]; + private final char[] fileMode = new char[8]; + private final char[] fileOwnerUserID = new char[8]; + private final char[] fileOwnerGroupID = new char[8]; + private final char[] fileSize = new char[12]; + private final char[] lastModificationTime = new char[12]; + private final char[] linkIndicator = new char[1]; + private final char[] nameOfLinkedFile = new char[100]; + private static final char[] padding = new char[255]; + + public TarHeader() { + } + + public TarHeader(File theFile, File theBaseFilePath) { + this(); + baseFilePath = theBaseFilePath; + + this.setFileName(theFile, baseFilePath); + this.setFileMode(); + this.setFileOwnerUserID(); + this.setFileOwnerGroupID(); + this.setFileSize(theFile); + this.setLastModificationTime(theFile); + this.setLinkIndicator(theFile); + } + + public TarHeader(String fileName, int fileSize, boolean isDirectory) { + this(); + this.setFileName(fileName); + this.setFileMode(); + this.setFileOwnerUserID(); + this.setFileOwnerGroupID(); + this.setFileSize(fileSize); + this.setLastModificationTime(System.currentTimeMillis()); + this.setLinkIndicator(isDirectory); + } + + public void read(byte[] buffer) { + String fn = new String(buffer, 0, 512); + fn.getChars(0, 100, fileName, 0); + fileNameLength = fn.indexOf((char) 0); + fn.getChars(100, 108, fileMode, 0); + fn.getChars(108, 116, fileOwnerUserID, 0); + fn.getChars(116, 124, fileOwnerGroupID, 0); + fn.getChars(124, 136, fileSize, 0); + fn.getChars(136, 148, lastModificationTime, 0); + // fn.getChars(148, 156, checksum, 0); we ignore the checksum + fn.getChars(156, 157, linkIndicator, 0); + fn.getChars(157, 257, nameOfLinkedFile, 0); + } + + // S E T - Methods + public void setFileName(File theFile, File theBaseFilePath) { + + String filePath = theFile.getAbsolutePath(); + String basePath = theBaseFilePath.getAbsolutePath(); + if (!filePath.startsWith(basePath)) + throw new RuntimeException("File \"" + filePath + + "\" is outside of archive base path \"" + basePath + "\"!"); + + String tarFileName = filePath.substring(basePath.length(), filePath.length()); + + tarFileName = tarFileName.replace('\\', '/'); + + if (tarFileName.startsWith("/")) + tarFileName = tarFileName.substring(1, tarFileName.length()); + + if (theFile.isDirectory()) + tarFileName = tarFileName + "/"; + setFileName(tarFileName); + } + + public void setFileName(String newFileName) { + char[] theFileName = newFileName.toCharArray(); + + fileNameLength = newFileName.length(); + for (int i = 0; i < fileName.length; i++) { + if (i < theFileName.length) { + fileName[i] = theFileName[i]; + } else { + fileName[i] = 0; + } + } + } + + public void setFileMode() { + " 777 ".getChars(0, 7, fileMode, 0); + } + + public void setFileOwnerUserID() { + " 0 ".getChars(0, 7, fileOwnerUserID, 0); + } + + public void setFileOwnerGroupID() { + " 0 ".getChars(0, 7, fileOwnerGroupID, 0); + } + + public void setFileSize(File theFile) { + long fileSizeLong = 0; + if (!theFile.isDirectory()) { + fileSizeLong = theFile.length(); + } + setFileSize(fileSizeLong); + } + + public void setFileSize(long fileSize) { + char[] fileSizeCharArray = Long.toString(fileSize, 8).toCharArray(); + + int offset = 11 - fileSizeCharArray.length; + + for (int i = 0; i < 12; i++) { + if (i < offset) { + this.fileSize[i] = ' '; + } else if (i == 11) { + this.fileSize[i] = ' '; + } else { + this.fileSize[i] = fileSizeCharArray[i - offset]; + } + } + } + + public void setLastModificationTime(File theFile) { + + setLastModificationTime(theFile.lastModified()); + } + + public void setLastModificationTime(long lastModifiedTime) { + lastModifiedTime /= 1000; + + char[] fileLastModifiedTimeCharArray = Long.toString(lastModifiedTime, 8).toCharArray(); + + for (int i = 0; i < fileLastModifiedTimeCharArray.length; i++) { + lastModificationTime[i] = fileLastModifiedTimeCharArray[i]; + } + + if (fileLastModifiedTimeCharArray.length < 12) { + for (int i = fileLastModifiedTimeCharArray.length; i < 12; i++) { + lastModificationTime[i] = ' '; + } + } + } + + public void setLinkIndicator(File theFile) { + setLinkIndicator(theFile.isDirectory()); + } + + public void setLinkIndicator(boolean isDirectory) { + if (isDirectory) { + linkIndicator[0] = '5'; + } else { + linkIndicator[0] = '0'; + } + } + + // G E T - Methods + public String getFileName() { + return new String(fileName, 0, fileNameLength); + } + + public int getFileNameLength() { + return fileNameLength; + } + + public char[] getFileMode() { + return fileMode; + } + + public char[] getFileOwnerUserID() { + return fileOwnerUserID; + } + + public char[] getFileOwnerGroupID() { + return fileOwnerGroupID; + } + + public char[] getFileSize() { + return fileSize; + } + + public int getFileSizeInt() { + return Integer.parseInt(new String(fileSize).trim(), 8); + } + + public char[] getLastModificationTime() { + return lastModificationTime; + } + + public char[] getLinkIndicator() { + return linkIndicator; + } + + public char[] getNameOfLinkedFile() { + return nameOfLinkedFile; + } + + public char[] getPadding() { + return padding; + } + + /** + *

+ * Checksum field content:
+ * Header checksum, stored as an octal number in ASCII. To compute the + * checksum, set the checksum field to all spaces, then sum all bytes in the + * header using unsigned arithmetic. This field should be stored as six + * octal digits followed by a null and a space character. Note that many + * early implementations of tar used signed arithmetic for the checksum + * field, which can cause inter- operability problems when transferring + * archives between systems. Modern robust readers compute the checksum both + * ways and accept the header if either computation matches.
+ * definition source + *

+ * + * @param header + * array containing a tar header at offset 0 (512 bytes of size) + * with prepared checksum field (filled with spaces) + */ + public void correctCheckSum(byte[] header) { + // Compute the checksum + // theoretical max = 512 bytes * 255 = 130560 = o377000 + int checksum = 0; + for (int i = 0; i < 512; i++) { + // compute the checksum with unsigned arithmetic + checksum = checksum + (header[i] & 0xFF); + } + String s = Integer.toOctalString(checksum); + while (s.length() < 6) + s = '0' + s; + byte[] checksumBin = (s).getBytes(); + System.arraycopy(checksumBin, 0, header, 148, 6); + header[154] = 0; + } + + public byte[] getBytes() { + + StringBuffer sb = new StringBuffer(512); + + sb.append(fileName); + sb.append(fileMode); + sb.append(fileOwnerUserID); + sb.append(fileOwnerGroupID); + sb.append(fileSize); + sb.append(lastModificationTime); + sb.append(" "); // empty/prepared checksum + sb.append(linkIndicator); + sb.append(nameOfLinkedFile); + sb.append(padding); + + byte[] result; + try { + result = sb.toString().getBytes("US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); // should never happen + } + if (result.length != 512) + throw new RuntimeException("Invalid tar header size: " + result.length); + correctCheckSum(result); + + return result; + } +} diff --git a/src/main/java/mobac/utilities/tar/TarIndex.java b/src/main/java/mobac/utilities/tar/TarIndex.java new file mode 100644 index 0000000..55bef01 --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarIndex.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +import org.apache.log4j.Logger; + +public class TarIndex { + + private static final Logger log = Logger.getLogger(TarIndex.class); + private File tarFile; + private RandomAccessFile tarRAFile; + + private TarIndexTable tarIndex; + + public TarIndex(File tarFile, TarIndexTable tarIndex) throws FileNotFoundException { + super(); + this.tarFile = tarFile; + this.tarIndex = tarIndex; + tarRAFile = new RandomAccessFile(tarFile, "r"); + } + + public byte[] getEntryContent(String entryName) throws IOException { + long off = tarIndex.getEntryOffset(entryName); + if (off < 0) + return null; + tarRAFile.seek(off); + byte[] buf = new byte[512]; + tarRAFile.readFully(buf); + TarHeader th = new TarHeader(); + th.read(buf); + int fileSize = th.getFileSizeInt(); + log.trace("reading file " + entryName + " off=" + off + " size=" + fileSize); + byte[] data = new byte[fileSize]; + tarRAFile.readFully(data); + return data; + } + + public int size() { + return tarIndex.size(); + } + + public void close() { + try { + tarRAFile.close(); + } catch (IOException e) { + } + } + + public void closeAndDelete() { + close(); + tarFile.deleteOnExit(); + tarFile.delete(); + } +} diff --git a/src/main/java/mobac/utilities/tar/TarIndexTable.java b/src/main/java/mobac/utilities/tar/TarIndexTable.java new file mode 100644 index 0000000..c0781cc --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarIndexTable.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.util.Hashtable; + +public class TarIndexTable { + + /** + * Maps tile name to TAR block index (each block has 512 bytes). + */ + private Hashtable hashTable; + + public TarIndexTable(int initialCapacity) { + hashTable = new Hashtable(initialCapacity); + } + + public void addTarEntry(String filename, long streamPos) { + assert ((streamPos & 0x1F) == 0); + int tarBlockIndex = (int) (streamPos >> 9); + hashTable.put(filename, new Integer(tarBlockIndex)); + } + + public long getEntryOffset(String filename) { + Integer tarBlockIndex = hashTable.get(filename); + if (tarBlockIndex == null) + return -1; + long offset = ((long) (tarBlockIndex)) << 9; + return offset; + } + + public int size() { + return hashTable.size(); + } +} diff --git a/src/main/java/mobac/utilities/tar/TarIndexedArchive.java b/src/main/java/mobac/utilities/tar/TarIndexedArchive.java new file mode 100644 index 0000000..88e804b --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarIndexedArchive.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Hashtable; + +/** + * Extended version of {@link TarArchive} that automatically creates + * {@link Hashtable} with the starting offsets of every archived file. + */ +public class TarIndexedArchive extends TarArchive { + + private TarIndexTable tarIndex; + + public TarIndexedArchive(File tarFile, int approxFileCount) throws IOException { + super(tarFile, null); + tarIndex = new TarIndexTable(approxFileCount); + } + + @Override + protected void writeTarHeader(TarHeader th) throws IOException { + long streamPos = getTarFilePos(); + tarIndex.addTarEntry(th.getFileName(), streamPos); + super.writeTarHeader(th); + } + + public void delete() { + if (tarFile != null) { + boolean b = tarFile.delete(); + if (!b && tarFile.isFile()) + tarFile.deleteOnExit(); + } + } + + public TarIndex getTarIndex() { + try { + return new TarIndex(tarFile, tarIndex); + } catch (FileNotFoundException e) { + // should never happen + return null; + } + } + +} diff --git a/src/main/java/mobac/utilities/tar/TarRecord.java b/src/main/java/mobac/utilities/tar/TarRecord.java new file mode 100644 index 0000000..d84b94d --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarRecord.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import mobac.utilities.Utilities; + + +/** + * A tar record contains the data of the file to be stored. The data size has to + * be dividable by 512 (internal tar block length). + * + */ +public class TarRecord { + + private byte[] fileData; + + public TarRecord(File theFile) throws IOException { + fileData = new byte[calculateFileSizeInTar(theFile)]; + this.setRecordContent(theFile); + } + + public TarRecord(byte[] data) throws IOException { + this(data, 0, data.length); + } + + public TarRecord(byte[] data, int off, int len) throws IOException { + fileData = new byte[calculateFileSizeInTar(len)]; + System.arraycopy(data, off, fileData, 0, len); + } + + public static int calculateFileSizeInTar(File theFile) { + long fl = theFile.length(); + if (fl > Integer.MAX_VALUE) + throw new RuntimeException("File size too large"); + return calculateFileSizeInTar((int) fl); + } + + public static int calculateFileSizeInTar(int fileLength) { + if (fileLength < 512) { + return 512; + } else { + int mod = fileLength % 512; + // align buffer size on 512 byte block length + if (mod != 0) + fileLength += 512 - mod; + return fileLength; + } + } + + public void setRecordContent(File theFile) throws IOException { + + FileInputStream inputFile = null; + DataInputStream dIn; + try { + inputFile = new FileInputStream(theFile); + dIn = new DataInputStream(inputFile); + dIn.readFully(fileData, 0, (int) theFile.length()); + dIn.close(); + } finally { + Utilities.closeStream(inputFile); + } + } + + public byte[] getRecordContent() { + return fileData; + } +} diff --git a/src/main/java/mobac/utilities/tar/TarTmiArchive.java b/src/main/java/mobac/utilities/tar/TarTmiArchive.java new file mode 100644 index 0000000..0fd6145 --- /dev/null +++ b/src/main/java/mobac/utilities/tar/TarTmiArchive.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.tar; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import mobac.program.atlascreators.AtlasCreator; +import mobac.utilities.Utilities; + + +/** + * + * Extended version of {@link TarArchive} that automatically creates the + * TrekBuddy tmi-file while writing the archive entries. + * + * @author r_x + * + * @see + * TrekBuddy tmi map tar index file description< /a> + */ +public class TarTmiArchive extends TarArchive { + + Writer tmiWriter; + + public TarTmiArchive(File tarFile, File baseDir) throws IOException { + super(tarFile, baseDir); + String tmiFilename = tarFile.getAbsolutePath(); + if (tmiFilename.toLowerCase().endsWith(".tar")) + tmiFilename = tmiFilename.substring(0, tmiFilename.length() - 4); + + tmiWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(tmiFilename + + ".tmi"), AtlasCreator.TEXT_FILE_CHARSET)); + } + + @Override + public void writeEndofArchive() throws IOException { + super.writeEndofArchive(); + tmiWriter.flush(); + } + + @Override + public void close() { + super.close(); + Utilities.closeWriter(tmiWriter); + } + + @Override + protected void writeTarHeader(TarHeader th) throws IOException { + long streamPos = getTarFilePos(); + int block = (int) (streamPos >> 9); + String line = String.format("block %10d: %s\n", new Object[] { block, th.getFileName() }); + tmiWriter.write(line); + super.writeTarHeader(th); + } + +} diff --git a/src/main/java/mobac/utilities/writer/NullPrintWriter.java b/src/main/java/mobac/utilities/writer/NullPrintWriter.java new file mode 100644 index 0000000..36009fc --- /dev/null +++ b/src/main/java/mobac/utilities/writer/NullPrintWriter.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.writer; + +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +public class NullPrintWriter extends PrintWriter { + + public NullPrintWriter() throws FileNotFoundException { + super(new NullWriter()); + } +} diff --git a/src/main/java/mobac/utilities/writer/NullWriter.java b/src/main/java/mobac/utilities/writer/NullWriter.java new file mode 100644 index 0000000..f63f0af --- /dev/null +++ b/src/main/java/mobac/utilities/writer/NullWriter.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.utilities.writer; + +import java.io.IOException; +import java.io.Writer; + +public class NullWriter extends Writer { + + @Override + public void close() throws IOException { + } + + @Override + public void flush() throws IOException { + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + } +} diff --git a/src/main/resources/mobac/mobac-rev.properties b/src/main/resources/mobac/mobac-rev.properties new file mode 100644 index 0000000..a8ce857 --- /dev/null +++ b/src/main/resources/mobac/mobac-rev.properties @@ -0,0 +1,3 @@ +#Wed, 03 Feb 2021 16:47:12 -0700 + +mobac.revision=Unversioned directory diff --git a/src/main/resources/mobac/mobac.properties b/src/main/resources/mobac/mobac.properties new file mode 100644 index 0000000..d0f17f8 --- /dev/null +++ b/src/main/resources/mobac/mobac.properties @@ -0,0 +1,6 @@ +mobac.version=1.9.16 +mobac.revision.fallback=$Revision: 1.9.16 $ +mobac.revision.hide=true +mobac.updateurl=https://mobac.sourceforge.io/mapsources-update/v2/mappacks-md5.txt +mobac.updatebaseurl=http://mobac.sourceforge.net/mapsources-update/v2/ +mobac.mappackversion=1 diff --git a/src/main/resources/mobac/resources/cert/MapPack.cer b/src/main/resources/mobac/resources/cert/MapPack.cer new file mode 100644 index 0000000000000000000000000000000000000000..4dc4fc150506a19d3b150c17ffd695e857fa4153 GIT binary patch literal 441 zcmXqLV%%!b#3;9bnTe5!iN)v7Aua=6HcqWJkGAi;jEt{h1dQkVV8LcZDcP4ko8GbM2K_BckSIoeGI@j6Zi zrB?sVZcPkUYSCMi(d#-h%kC{(c>3nc2K!#$;GVSFz->dh;H0y*=GT7k20SwIU4abHFBY`!p4bhiyHC-$s5tk-ek%;dxB@5JXAl)6ki nzv9i!^keIdPKMNpNtTDR^t8=4yO!wkaF*f`vE-9WU;P3A=4q;B literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/Splash.jpg b/src/main/resources/mobac/resources/images/Splash.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7933d736eb8d08d56627be9a6e702ed3bce6ed49 GIT binary patch literal 41570 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 R04o3h002ovPDHLkV1f@GjF12T literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/arrow_blue_left.png b/src/main/resources/mobac/resources/images/arrow_blue_left.png new file mode 100644 index 0000000000000000000000000000000000000000..99057afed4553f5dc4203c304c77da770d6c20c3 GIT binary patch literal 1702 zcmV;X23h%uP)Hq)$8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H102OpaSaeirbZlh+L2`0$cOXJ+b!8w-WoC3>HVvBq000fFDZ*Bl;WnpeE zc4=dDZ*neTVQgt`Kxb`XX>2ZIZ*ADx=TQIv02p*dSad^jWnpw_Z*Cw|X>DZyGB7YT zEio`HGch>Nq09gP07-O3Sad^gaCvfRXJ~W)P<3K#X=5NnZ*5^|ZXjrMbZ|N^FJp3L zVRUJBWn*t`ZEtRKE^l&YFKlUJWo~n2b1!gpVr*$+WN&R@X>KoB!~V(u00lBhL_t(Y ziM^K3Z(P+C$3N%Z`+m&W9@~?|jwvoNEs1GRKy5@9NC=5Ww5o&vRsAdY18BPnltmX^ zbWy2Yu;~Jcs^W)5R5d#m6bT`9kyZ(53!zAHVmsIqd*;piaqsEk&3I-=;x-_-($O1T zy*Hoxz2|$+xuUAv%{ck}H78Vo1>ghkmX!w&P?%AouX_UL$sV@XR{=HWdCA=<4^z_Wa42{H~`V}jF;JbfFA-pMZ z{NvW*JP(~{v(QfAODdLw%=N8DUTKKfw*hFnNR6uCGY?BQ+9U>L<}Mp zVepi(Cl({Ooqy+!;FX7e-xRTH8c#hrvv`zK$J-og#Y{CFX=2#z7I+_MB#vfkXe5?2 zF{H62iVTqz@>0U6N3-m7} zvyVly-(czR6voKJpHG00vBe0%D_dI`At=@wB4=o(jx;tnYp_PJ28@-F;-v>)o)zqQ zP3IqVt$R7#Zq)f?^3Y7!P_>>=P_Jxm=U5|Xq=cY&ujGX%1PDRv*A+yy&hg@jv-2YM zylEaf9krHe&o(e-Qrqw&_MbY=W#dLhRRyL}OIaE6G9UtFr6{$riZW1E^@6lktZ$me zk<)JGK4zv<%;!pqjw>60h9^)Jws#6TH*=b)rLclA9t6BstPQB@zN?GUQ}_U?xMAs2 z>Mi*E2ns=!po)e%hI2*f`Sfa+G%;8!`_2tPDO{abRq#P6t3X}^@-ozl6@8}aT|JNL z>0MTEu0iC+Wb0_F>jl)ipaK4KwM$m`k=qB5s#2T{`#uQ+a9Jt+yxs+~dHJPnRDa;J zYwwqxD{OcAl;vat@S!%$&?r?`l<8LjU@^<<3W}Z*=MB)hO42eZ|)|`)uvxWQ8ZoL#?#$=?>NQ z{3N(HT2PkPif?HWizm)bi1e*OwvB%=2lp} ze~wH4?hsTl-F2kuWA1PPxMgngvhcxyV1T97ms1h*h9vDDn$~>`TZP&FC!bt+UJm^D zfbl=?Tk`2YX_ literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/arrow_blue_right.png b/src/main/resources/mobac/resources/images/arrow_blue_right.png new file mode 100644 index 0000000000000000000000000000000000000000..b1f2ba3d30be09d751ba69968a4cfc2adf5b3a62 GIT binary patch literal 1714 zcmV;j22J^iP)`D<#0000dbVXQnL}hbha%pgMX>V=-YGq+= zE_P{SbZ>GlVqt7)Za`;kVQFkGV{dKP+UHRK000V>IRB3Hx05UKz zH7zkPEi*AV&!Nl!002pJMObu0Z*X~XX=iA308n*eY-wX4L~m_jX>K5BbaZe!FE3+q zWnpw_c4cF4ZEbIEb1rXkXD@7NV`Xl0WpgiZbz*F3V`Oh_VQFqJS;PLy000F&NklvO|9fcagv(ahCG-{q&?1&2E2dm^Si5tC)c+Iot~a5x;`+^DeMhl6LeIO61qDQZVPjK!0mm-L&5 zp7=@oHn1e($We4Rh?s1}v?n4anh`Q%b2B3kD!;xTqY8v`Rb3Y6AdWpJ=GxR| z=1{%!S)tdCf9%Jd+bs5xBk~Y=M;v*YbuG(%j=eoG!(Mxe!WNmK)xKNjMxuFw)$}3sD4PkOY$4=yd8MZZ&DD zi62h=-aBTdTWskBId;zW)_y;G`o#+|5?L^^JRCr?+;Gkjg2;+;DP_5l=SHueQv_%< z;?lY?2XHU}9fCur-DF1(BiY%oxAICX%%!;zr-ljgLPWk4OfXRtrPXOx)>&@cTpsYy z@ir!50#)HC`{ljEmn2jLL9%ikC#m6Gpa@1S_IO`PMJ9^M>MX00NM;PPz>Vbrv&UMf z0@;{OI0P<&(ceuddW#gpmA8bRiNRcw1SC<5J#l0suu5=Il<>3yr&*xe&uBGcT;wnl zW~DI0A#?NG*zP6dy(PkM^)E;lHm?1k7fWI<4FrieGNQ=g-3S~iMdxEK-%p=c7kivK zIgJTI_3n6lvple|y+__%Bn(&oBB;E9(RD?sY6#|0C#2>(^fF?>~^y-U$u#L}%xLQk(QeEZG^f%`e$K78hQ)5$%K!iX07*qo IM6N<$g4BO0-~a#s literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/arrow_blue_up.png b/src/main/resources/mobac/resources/images/arrow_blue_up.png new file mode 100644 index 0000000000000000000000000000000000000000..53c1ba48ad916d9007c986f92ecbbc3211f19888 GIT binary patch literal 1737 zcmV;)1~&PLP)GXLSbxaZtO}COaK4?Cv-(vbVOxyV{&P5bZKvH0BU7nZZ39d zV{~tFE@EM9X>LGgZDDC_E@N+P*xKh&0000ObVXQnLvm$dbZKvHAXI5>WdJfTFf}bP zFfB7MPsov40000b7gZcaCKsAX=7w>ZDDC{FImI>$^ZZbS4l)cR7i=f zlv{6HR~d$%Z>_aYGh>gPU^{ji)vl-$t3pVqLINa&1f>-frL9medch4BTyTK|7hE7H zXZjNmKLK$lMXg#Dq>2a?LJE{NO;XeVt&5$;u@iedvk&XQ#hS!Pm;r`%OIu6Yvt~W( zec$i>P#q*EtO!9x31USF?jV>a;GOeY%0sP?7X%YFv#@Vl3 zeb<5WtB*~K$aA_l@M?{`c`NBt%nH6TcIYui0yoc?p1KjR9R9Am1 z$bE0T@}=JY4O}?>?Y4+Kt@62#CNp<1H`^jf)J=Q54{U7@Xs*76)O)`Xkx%{oFJIiB z7f=RH#d4TS2j`2;ManD`t?75e(+y`izmL9Bl4um4&P&DmRY&2 zOS|~buxpQXo(pg6Vs|g0o9a0c^}t{M__=Z{aB=PXnIKO{HhZ6$S>ev*8K&C_f*=CO zaAN>JCi=Q@oV#!Zzq^BR)iax$C`$IMmHh0!qn{ zB|}~y1PCGEd|YpQ`jzFC^EYA!g@&z`2{Dd;NyfXSOi?eT;KYBI>hePd(ho;@aazkRPkG`w&yN zlV^q^*Bo1DbK6pjZYM>A_zn;Af$Ivrk1CxHIOm5x#GM;k1OD*x7DZi=c>i-mJ))=Q zKKUa_ryrH6Ws2O;E;OxN6A0|}8tTT;$~DuSgf!8li6TuTu11k0io^(s5mKWtI>Od3 z_F3QDr`NYwWiZH^MvfX0eN5~C**Q-sLWQF4b~$|FPO?-Zz~$|dva}psXwxc8wA6zS zA=(c(^XQkx({dxtisi9%%b z7#sss6nQM3B8yP1W9>N?sI5XP$L?Ouxz`)+TJ4e;jSsP1QX>cxDs1JNc3~(oO`a-@ ziKE6kkEvt%J0IxvEie4(G9UiH98+yg2#^>Vt8nBsL=<@%)wK&vr)8LGC3ITR>O4EJ zIx&jIdQQIhHpU3;mc|%`jLC2`5KWpY@-$l8E)1QPp;HWV9rmEng4VHucYBs+PhO;* zYm{Qr3^j=sl0=au>Ux&e1$?k1Mo=ogaSFwJFRb@?`s60Uc!gt&Myq&pqvIrL7n*+Q zP!c`3vB3gyj)nlwpSr|;<&e=wkIV4Dju7Gm*)JVcK&~pmH=eV4bI|9d+YrtjF58$GL#@H;*?nX@-pr)QzL8gb+YUR8>}vvhq~5 zkKpU@g9}(2$R|>WqG}wb62Q_}g^1vMpl&>a(lMwVWgTs<8&BPY8&vzrtF){<;9{d| zK}4_)>e@4CEM@8F4?O*WXHa>nYpQLaX~M)+diobXUB4R<1QkRCZdnBtZvH(BA~^*r fwBq;QjXL{ZvwXJtdFP*J00000NkvXXu0mjf4~`v! literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/arrow_closed.png b/src/main/resources/mobac/resources/images/arrow_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..48b4a46083ef25b3bb4bf178b06226cf1bdd2d5b GIT binary patch literal 616 zcmV-u0+;=XP)Px#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vGbVE_OJVF8uHxV8WQ02p*dSaefwW^{L9a%BKeVQFr3 zE>1;MAa*k@Gd8K`;w1n80j)_yK~y+TmC`#bLtz*O;1Zj;lf`WyB&39hgn^2X>flb@ zf<#;fVo-xqLQp~LT2$he6cK|aDrIORl`t@1X10n^Uhl`zR1|TNH|L!1|DXSMPJ|}p ziY*y8@YTgcht}2S7v5_hkIn8dZ5j>na%za$sct{!0|s zEixLG1UvqP1rBUpLuXq2A69k=x31B{Agyj* z@Uz-)I=FlmklXG=Y5zVuJ0u6aeUe728^FO%`56X=}tV^H8a;c@_NHa{AyXF-_Pvx7LU zQu>GDC>G9>%V*HN7$Cero1Jj#3{~8Q%vi`RC6-wQo--vnS7cuWJY9h~9UB5A%CAtx zk?iPAFj}E&Sw*~IQlOWK{1dAKXxaHnol)-EaPSQfML!dQ6S#l?0000N2bPDNB8 zb~7$BHmT?0B>(^cS4l)cR5(v#V4yk(deF}h^x!`be+A-BAZ8)nj-UtkxeMR^S8D$G zpCJrv$YCIs#xx-4fiF;EIuOr5!k=YpfBp~K_W!@_y#N0hq8@<^cm>3+s0IW*_`(qX z^gny%D->)n;Xhcj|N8&`?dSjh&yx8TWWZk_t_NZ!h#^3~X?H;sBIA%v|Nr}}`~Tl* z)&Kuii~s*O0vfM&^C~bFpVrFUw|=yW;0mhYKVPx#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vGe6aWAU6an>XTjT%$02p*dSaefwW^{L9a%BKeVQFr3 zE>1;MAa*k@Gd8K`;w1n8142neK~y+TU6XA{Q(+v%uc^r(*wnBxNe%7AE!#@8q!KJj zP0eKzp}i2Lt(y6Q8D@PD_d!2Y2F^KAw^{T;i-Mw-wW+o176w5@D@dfbPf6C)R@{J0y<&69K@qN3sra&x;-RMd;DTW@0hdV?Y0A}J|d+0by? zZ#0gO>Vnbul>#fpk4d|LBS+jQES%a$uxpo-k;buYo5*29eyLQh6l!WN>1%7PBoE@m z2|LN(=){etrb&uibTfg{(&rR@#GXCFq~FJuEqzE&zlxL;i(Y7GuzPemH}v{Xv}p^7 zi;H{c!j0|Qdsv{0wjUrX%ZmJbk@+2T^pz1FAvLwvBWN@xUs;(2r%pYgi}wMtM&rWC zlM@sVLaQCefdivFg&{`pV&g_9hYVxIiY_c$*5MOsYwfx&-Mh`q|C-130(Es0%sNdjEEGA&%l!j$ zKSNH=7}l&2k(Txn2??E8w5Zi16cx4VwOTPCE-0{2>|)*?id@LZu<(75g*#*vw6BCVU$cO(u`JVpKjrE-9UIhgw{7A$CoOr~{E_y2vEx*#gm zX>(dy`!9t;$E?NxSuVeh_;>>g>%_u^mS1z{Hg^lcjLY<|l{s?~R8r|mePrY&Ph@1? r_lO9!PbO1)B$8BpNXY!@%>S(4VbAOqZC;-t00000NkvXXu0mjf>e$ox literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/compass.png b/src/main/resources/mobac/resources/images/compass.png new file mode 100644 index 0000000000000000000000000000000000000000..7f452fc9e3fc4db642c206b50fe64aed50114430 GIT binary patch literal 16875 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 E1GWnM_5c6? literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/layer.png b/src/main/resources/mobac/resources/images/layer.png new file mode 100644 index 0000000000000000000000000000000000000000..939df50e5b10aec774c25f471e052eb686adf668 GIT binary patch literal 551 zcmV+?0@(eDP)E@$c_?5o(7hmtNCD5;=k(CaJn5i*PN? z=+HsjocsfV1qZ2vWCX%KD_WEBD4?OI1YtJu=W|AUIDa@y?@bI5(+a=`vxK$TM&A_2rR#YdqW*DjGcu1 zW7uq=)V%P(5q1zhMuoF?1XcSEub;qZV(%E!)3CJ-A`Hw456`sFPsDs>ial$Ksw?67 zDIxtrpz6ciZ8&{Uhox}_KO{K6=5XP?hJwDuK2mQ|*uD+GMsb5;uE^J=5tzzo%$$I7 zsiC+|j#2K@j~!izl6;%QuC>tL5fREq%B+m6P(NJX;w~2z(3NpASB;o)(>h?8`&3(k zo#4m%=K9O(YV~qgBogV4^~Z*LdwMgWP^kNVSl4td910%|g5Z$){Wa3*bRr&)-!e_} zoUZG7C)jK@w|(D#=6T+v>$=N-@NTl%?2u(yPx#1ZP1_K>z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy7<5HgbW?9;ba!ELWdKlNX>N2bPDNB8b~7$BHmT?0B>(^f6iGxu zR5(v9RQpexWf=Wn1C2UkqKS!dE|C_n6e!Traw&H(nq^T2j>{aEF$Bh7vx8 z7!?@MLI;$VOIIiz5H2&(Xkzrg&|h@PHjR-nr9Gav^Gn|3?f1RUdCxh|dFp4(4a$4% z#H*oy9C{zW^G3j9kJpPMkN3di!M`3zf!zNV=qLShbmTb*Jm&w_pNF0&Z@wN7D*5|s zUJ39&@bmL~@#NV-Bo6p^oomKt@Ql_njm1`Vt=F^6;2 z(ev|FQ7~7iEQ>X2@g3GWE$-K9FjlO@Xr5dtjTePA+`SZqoTO8*HYZ~FdJ@L3#KPXH zhlBg&)#5K7>&9d1-dRoV(2S1b_X)CL%_MlcVMS{sNh8-6HFo zX>fPwpP9-c&icSCm%`7iwZvm@AOkyOVr|i5Nq9w>?%p&!B(psN3BWyq?+;~R^JY3W z`QA!k=XE{it5g`tm7S8NOO=Hy^lY^~nMGM}kdg6nEGBB%Edmcx&O;Uppl^pi#y-!8 zOD7FF^cJgNx~j!ut@^LAf{+iS$&#>79ZhjGIojvb6c62@+ zc|x*S;-WD|Xr9gK%~$;B^URnZ{N7x3jLXg3?9w+kix&wh3U^r~8X-D2ZHQwe^w{ms z#CmrsEDiCPW208v5*ZEE-b=7NxqRVD%-`eA0e1(i${Jq0WxjOVe zxZ3r<+r^0eKW4LR80Quf=R@M9uk++uOL-)0hS*>2^;(##)tIT#V2K@eGJp2ESTEOs ztyT?7rD~Q#|DBUj5+f)gwa7mL%6vNdg{)fY`{aGaP)<<#oxI?aqXi+li3^I<>9X+8 zXUoFN%@>tX2pX;{WgnKIK@7@9Ja{h@5|^vSpX Y17ov^__+c9EC2ui07*qoM6N<$g3w7mfB*mh literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/minus.png b/src/main/resources/mobac/resources/images/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..4093e64277e30f6d908045cfea3eec9d1dfdcc3b GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G}c0*}aI z1_mZ85N7;mrWptnlqhkHC<)F_D=AMbN@WO0%*-p%^K%VRC^ObGHZ*T8Zv(2)^mK6y z(FjgXkdRA|ydZt#(NYEd|LTnYmAwyFF3?N*XgPuRjLM^fV#N&CbTnJb?k1lGYGUwo L^>bP0l+XkK3w|$q literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/mobac16.png b/src/main/resources/mobac/resources/images/mobac16.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf88f6d1cc4e59ceab82f9f8adbc3a17f8f1b2b GIT binary patch literal 941 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 IM6N<$f>5&sng9R* literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/mobac48.png b/src/main/resources/mobac/resources/images/mobac48.png new file mode 100644 index 0000000000000000000000000000000000000000..b7024b1a01a522d0baed4a4543ed54f703c64302 GIT binary patch literal 5035 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 BeP93p literal 0 HcmV?d00001 diff --git a/src/main/resources/mobac/resources/images/plus.png b/src/main/resources/mobac/resources/images/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..6d7df3cc54b7b332f277fb68d03e5080906d3510 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kmUKs7M+SzC{oH>NS%G}c0*}aI z1_mZ85N7;mrWptnlqhkHC<)F_D=AMbN@WO0%*-p%^K%VRC^ObGHZ*T8Zv(1{_jGX# z(FjgXaA56mcqgN{8T2ka?MFwtR->h6hGuzYDb>rT*w^n`b+ zZU5Y-wS9JoJ@LbM(%ShKo}6pUNpL%~#^IZt3h(ZQ95EA9jY1=iM~n<5=iEwd3jclw PTE*b$>gTe~DWM4fE@?NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fHRz`)E9 z;1l8sRG^*Fu8}=Wzi75u=_K>=ndTMKEK4V;<}Xn$*`{21RHgQuYTX61rgaV*ZaQpw z=(zb!=juJ}yKhcCcBA|3o8C)5CY*dY>D-g4w|;Ja_-@PN-@9LYy?O7&-3PD!{re9z zd=!j^z+eu6PR=AbV4h+q3Gxg6Ke7Qs@OO4qU|dO*xJHx&=ckpFCl;kL1SDqWmFW4o zhA5O7>lvG^eEtZerr*=WF+?NS_WVtuW(6MB0LLQ?T45=HxBvW)4+?wFY;4(*B9X+o z?1}NQh1ZOeg&*B`czXWp%4^J03Oo1Q|5Dk_kD+3Spej~qi`1kPQHJ0naqljEV4u(a zF#rqSpYVO3FvbAz^2ry+fs^HOdAU4Vp0Kpeb~+uSEvsGMt#8+AwKKrC=g(dE6+pc7 z$_XKaIKA-P!rJW2jHp(toIiH~5J)L8#&F=^epXgjxOwA-xpe9B`~80Z9SOX-w7j(T z?7}l5=V$rm>$BYZ{T{PVPBVY>DWnp7@%dNu2R)8FJug;It)5m&t?8+$srM?CeIn^~ zNskd5evWpW3=! zufN-9wLZDCwZ)IuuTvN*P%4!fE*23&5XCWJc%LYYa2y8^Y;SLWAdE4ro_eD*K0fX| z_V^)kxg2SlGU)d)fKn2zHQjE)*5)RgTeq8gdyTvXC=Lzx{CvT=a^*5<+GAv-%3>JMoNkA`@Hu0tJG>Ws@08)fF}UUvh>{H z`NMA?Ju-hHU&vd&?~}=9(aNHqrZk&Pf}oaMz53nRAHKi#C2&m`V-P}Upa7I6CLTOE yIyO4v`Q9NjFrv|D{2Tc6eqqaGdg|%0000 + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/src/main/resources/mobac/resources/text/apache-2.0.txt b/src/main/resources/mobac/resources/text/apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/src/main/resources/mobac/resources/text/apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/src/main/resources/mobac/resources/text/gpl.txt b/src/main/resources/mobac/resources/text/gpl.txt new file mode 100644 index 0000000..f32f9fe --- /dev/null +++ b/src/main/resources/mobac/resources/text/gpl.txt @@ -0,0 +1,339 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/src/main/resources/mobac/resources/text/help_dialog.html b/src/main/resources/mobac/resources/text/help_dialog.html new file mode 100644 index 0000000..48f09bf --- /dev/null +++ b/src/main/resources/mobac/resources/text/help_dialog.html @@ -0,0 +1,53 @@ + +

Keyboard & Mouse commands

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
F5Refresh map display (reloads missing tiles)
<any cursor key>move map
[right mouse drag]move map
<ctrl> + <left mousebutton>move map (OS X only)
<ctrl> + <cursor up>zoom in
[left mouse double click]zoom in
[mouse scroll up]zoom in
<ctrl> + <cursor down>zoom out
[mouse scroll down]zoom out
<ctrl> + <cursor left>previous map source
<ctrl> + <cursor right>next map source
[left mouse drag]select area for download
+ \ No newline at end of file diff --git a/src/main/resources/mobac/resources/text/help_dialog_fr.html b/src/main/resources/mobac/resources/text/help_dialog_fr.html new file mode 100644 index 0000000..1a5edea --- /dev/null +++ b/src/main/resources/mobac/resources/text/help_dialog_fr.html @@ -0,0 +1,53 @@ + +

Commandes clavier & souris

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
F5Rafraîchit l'affichage (recharge les dalles manquantes)
<flèches de direction>Déplace la carte
[Déplacement de la souris avec bouton gauche appuyé]Déplace la carte
<Ctrl> + <bouton gauche de la souris>Déplace la carte (OS X seulement)
<Ctrl> + <flèche vers le haut>Zoom +
[Double clic du bouton gauche de la souris]Zoom +
[Molette de la souris vers le haut]Zoom +
<Ctrl> + <flèche vers le bas>Zoom -
[Molette de la souris vers le bas]Zoom -
<Ctrl> + <flèche gauche>Source de carte précédente
<Ctrl> + <flèche droite>Source de carte suivante
[Déplacement de la souris avec bouton gauche appuyé]Sélectionne la zone à télécharger
+ diff --git a/src/main/resources/mobac/resources/text/help_dialog_ja.html b/src/main/resources/mobac/resources/text/help_dialog_ja.html new file mode 100644 index 0000000..97420ea --- /dev/null +++ b/src/main/resources/mobac/resources/text/help_dialog_ja.html @@ -0,0 +1,53 @@ + +

キーボードとマウスによる操作

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
F5地図の再表示 (タイルの再読込)
<カーソルキー>地図の移動
[マウス 右ボタン ドラッグ]地図の移動
<ctrl> + <マウス 左ボタン>地図の移動 (MacOS X のみ)
<ctrl> + <カーソル 上>ズームイン
[マウス 左ボタン ダブルクリック]ズームイン
[マウス スクロールアップ]ズームイン
<ctrl> + <カーソル 下>ズームアウト
[マウス スクロールダウン]ズームアウト
<ctrl> + <カーソル 左>前の地図の取得元
<ctrl> + <カーソル 右>次の地図の取得元
[マウス 左ボタン ドラッグ]タイルの矩形選択
+ diff --git a/src/main/resources/mobac/resources/text/help_dialog_zh.html b/src/main/resources/mobac/resources/text/help_dialog_zh.html new file mode 100644 index 0000000..ad43d9a --- /dev/null +++ b/src/main/resources/mobac/resources/text/help_dialog_zh.html @@ -0,0 +1,53 @@ + +

快捷键 & 鼠标操作

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
F5刷新地图显示 (重新载入缺失的地图块)
<光标箭头(上下左右)>移动地图
[拖动鼠标右键]移动地图
<ctrl> + <鼠标左键>移动地图 (仅限OSX系统)
<ctrl> + <光标上箭头>放大
[双击鼠标左键]放大
[鼠标滚轮 上]放大
<ctrl> + <光标下箭头>缩小
[鼠标滚轮 下]缩小
<ctrl> + <光标左箭头>上一个地图源
<ctrl> + <光标右箭头>下一个地图源
[拖动鼠标左键]选择下载区域
+ \ No newline at end of file diff --git a/src/main/resources/mobac/resources/text/lgpl-3.0.txt b/src/main/resources/mobac/resources/text/lgpl-3.0.txt new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/src/main/resources/mobac/resources/text/lgpl-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/src/main/resources/mobac/resources/text/license-dbd-je.txt b/src/main/resources/mobac/resources/text/license-dbd-je.txt new file mode 100644 index 0000000..df8fe6f --- /dev/null +++ b/src/main/resources/mobac/resources/text/license-dbd-je.txt @@ -0,0 +1,75 @@ +/*- + * $Id: LICENSE,v 1.12.2.2 2010/01/04 15:30:18 cwl Exp $ + */ + +The following is the license that applies to this copy of the Berkeley +DB Java Edition software. For a license to use the Berkeley DB Java +Edition software under conditions other than those described here, or +to purchase support for this software, please contact Oracle at +berkeleydb-info_us@oracle.com. + +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +/* + * Copyright (c) 2002,2010 Oracle. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Redistributions in any form must be accompanied by information on + * how to obtain complete source code for the DB software and any + * accompanying software that uses the DB software. The source code + * must either be included in the distribution or be available for no + * more than the cost of distribution plus a nominal fee, and must be + * freely redistributable under reasonable conditions. For an + * executable file, complete source code means the source code for all + * modules it contains. It does not include source code for modules or + * files that typically accompany the major components of the operating + * system on which the executable file runs. + * + * THIS SOFTWARE IS PROVIDED BY ORACLE ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR + * NON-INFRINGEMENT, ARE DISCLAIMED. IN NO EVENT SHALL ORACLE 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. + */ +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +/*** + * ASM: a very small and fast Java bytecode manipulation framework + * Copyright (c) 2000-2005 INRIA, France Telecom + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * 3. Neither the name of the copyright holders 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 THE COPYRIGHT OWNER OR CONTRIBUTORS 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. + */ diff --git a/src/main/resources/mobac/resources/text/localize.properties b/src/main/resources/mobac/resources/text/localize.properties new file mode 100644 index 0000000..580f3d2 --- /dev/null +++ b/src/main/resources/mobac/resources/text/localize.properties @@ -0,0 +1,662 @@ +#---------- Version ------------------------- +mobac_version_subfix=International Edition +mobac_version_subfix_dev= +#---------- Basic ------------------------- +OK=OK +Cancel=Cancel +Close=Close +Error=Error +Unnamed=Unnamed +Warning=Warning +minute=minute +minutes=minutes +second=second +seconds=seconds +Information=Information +Continue=Continue +Abort=Abort +Skip=Skip +Retry=Retry +Undefined=Unknown +Exit=Exit +Bytes=Bytes +KiByte=KiByte +MiByte=MiByte +GiByte=GiByte + +#---------- On map controls ------------------------- +map_ctrl_zoom_level_title= Zoom: +map_ctrl_zoom_level_title_tips=The current zoom level +map_ctrl_zoom_grid_tips=Projects a grid of the specified zoom level over the map +map_ctrl_zoom_grid_prefix_fmt=Grid Zoom %d +map_ctrl_zoom_grid_disable=Grid disabled +map_ctrl_wgs_grid_title=WGS 84 Grid +map_ctrl_wgs_grid_tips=Projects WGS 84 Grid on map preview (not included in atlas) +map_ctrl_wgs_grid_density_tips=Specifies density for WGS 84 Grid +map_ctrl_wgs_grid_density_prefix=Every +map_ctrl_wgs_grid_density_degree=Degree +map_ctrl_wgs_grid_density_degrees=Degrees +map_ctrl_wgs_grid_density_minute=Minute +map_ctrl_wgs_grid_density_minutes=Minutes +map_ctrl_wgs_grid_density_seconds=Second +map_ctrl_wgs_grid_density_second=Seconds +map_loading_wait=Please wait - loading map data + +#---------- Left Panel ------------------------- +#coordinate +lp_coords_title=Selection coordinates (min/max) +lp_coords_select_btn_title=Select entered coordinates +lp_coords_label_N=N +lp_coords_label_E=E +lp_coords_label_W=W +lp_coords_label_S=S +lp_coords_fmt_list_title=Fmt +lp_coords_fmt_degree_eng=Degree (eng) +lp_coords_fmt_degree_local=Degree (local) +lp_coords_fmt_degree_min_eng=Deg Min (eng) +lp_coords_fmt_degree_min_local=Deg Min (local) +lp_coords_fmt_degree_min_sec_eng=Deg Min Sec (eng) +lp_coords_fmt_degree_min_sec_local=Deg Min Sec (local) +lp_coords_fmt_tile=Tile (x or y / zoom) +lp_coords_invalid_text=Invalid coordinate!
Please enter a number between %s and %s +#map source +lp_map_source_title=Map Source +lp_map_source_combo_tips=Select map source +#zoom level +lp_zoom_title=Zoom Levels +lp_zoom_total_tile_count_tips=Total amount of tiles to download +lp_zoom_number_tips=Select zoom level %d for atlas +lp_zoom_total_tile_title=%s tiles +lp_zoom_total_tile_hint_head=Total amount of tiles to download: +lp_zoom_total_tile_hint_row=
Level %d: %d (%dx%d) +#Tile parameters settings +lp_tile_param_title=Layer settings: custom tile processing +lp_tile_param_recreate_checkbox_title=Recreate/adjust map tiles (CPU intensive) +lp_tile_param_recreate_checkbox_tips=If this option is disabled, each map tile (size: 256x256) is used exactly as downloaded from the server (faster).
\ +Otherwise each tile is newly created which allows to use custom tile size (slower / CPU intensive). +lp_tile_param_width_title=Width: +lp_tile_param_width_tips=Tile width +lp_tile_param_height_title=Height: +lp_tile_param_height_tips=Tile height +lp_tile_param_image_fmt_title=Tile format: +lp_tile_param_image_fmt_png=PNG +lp_tile_param_image_fmt_png_8bit=PNG 256 colors (8 bit) +lp_tile_param_image_fmt_png_4bit=PNG 16 colors (4 bit) +lp_tile_param_image_fmt_jpg_q100=JPEG - quality 100 +lp_tile_param_image_fmt_jpg_q99=JPEG - quality 99 +lp_tile_param_image_fmt_jpg_q95=JPEG - quality 95 +lp_tile_param_image_fmt_jpg_q90=JPEG - quality 90 +lp_tile_param_image_fmt_jpg_q85=JPEG - quality 85 +lp_tile_param_image_fmt_jpg_q80=JPEG - quality 80 +lp_tile_param_image_fmt_jpg_q70=JPEG - quality 70 +lp_tile_param_image_fmt_jpg_q60=JPEG - quality 60 +lp_tile_param_image_fmt_jpg_q50=JPEG - quality 50 +lp_tile_param_msg_valid_height=Value of "Tile Size Height" must be between %d and %d.\n +lp_tile_param_msg_valid_width=Value of "Tile Size Width" must be between %d and %d.\n +#atlas content +lp_atlas_title=Atlas Content +lp_atlas_new_btn_title=New +lp_atlas_add_selection_btn_title=Add selection +lp_atlas_name_label_title=Name: +lp_atlas_name_field_tips=Enter a name for the atlas here +# this one only support English +#lp_atlas_element_default_name=Layer name +#atlas content detail - Atlas +lp_atlas_info_atlas_title=Atlas
+lp_atlas_info_atlas_name=Name: %s
+lp_atlas_info_atlas_layer=Layer count: %d
+lp_atlas_info_atlas_format=Atlas format: %s
+lp_atlas_info_max_tile=Maximum tiles to download: %d
+lp_atlas_info_area_start=Area start: %s %s
+lp_atlas_info_area_end=Area end: %s %s
+#atlas content detail - Layer +lp_atlas_info_layer_title=Layer
+lp_atlas_info_layer_map_count=Map count: %d
+#atlas content detail - Map +lp_atlas_info_map_title=Map
+lp_atlas_info_polygon_map_title=Polygonal Map
+lp_atlas_info_polygon_map_point=Polygon points:
+lp_atlas_info_map_source_short=Map source: %s
+lp_atlas_info_map_source=Map source: %s (%s)
+lp_atlas_info_map_zoom_lv=Zoom Level: %d
+lp_atlas_info_map_area_start=Area start: %s (%d / %d)
+lp_atlas_info_map_area_end=Area end: %s (%d / %d)
+lp_atlas_info_map_size=Map size: %d x %d pixel
+lp_atlas_info_tile_size=Tile size: %d x %d
+lp_atlas_info_tile_format=Tile format: %s
+lp_atlas_info_tile_format_origin=Tile format: 256x256 (no processing)
+#atlas content node pop menu +lp_atlas_pop_menu_show_detail=Show item details +lp_atlas_pop_menu_display_select_area=Display selected areas +lp_atlas_pop_menu_select_map_box=Select map bounding box +lp_atlas_pop_menu_zoom_to_map_box=Zoom to map bounding box +lp_atlas_pop_menu_zoom_to=Zoom to +lp_atlas_pop_menu_rename=Rename +lp_atlas_pop_menu_apply_tile_process=Apply tile processing options +lp_atlas_pop_menu_clear_atals=Clear atlas +lp_atlas_pop_menu_delete_node=Delete +lp_atlas_default_tip=Use context menu of the entries to see all available commands. +#Profile +lp_atlas_profile_title=Saved profiles +lp_atlas_profile_combo_tips=Select an atlas creation profile\n or enter a name for a new profile +lp_atlas_profile_delete_btn_title=Delete +lp_atlas_profile_delete_btn_tips=Delete atlas profile from list +lp_atlas_profile_save_btn_title=Save +lp_atlas_profile_save_btn_tips=Save atlas profile +lp_atlas_profile_load_btn_title=Load +lp_atlas_profile_load_btn_tips=Load the selected profile +lp_atlas_profile_refresh_btn_tips=reload the profiles list +lp_atlas_profile_msg_ask_name=Please enter a profile name +lp_atlas_profile_msg_overwrite_confirm_title=Please confirm +lp_atlas_profile_msg_overwrite_confirm=Profile %s already exists. Overwrite? +#main button +lp_mian_create_btn_title=Create Atlas +lp_main_create_btn_tips=Create the atlas +lp_main_setting_button_title=Settings +lp_main_setting_button_tips=Open the preferences dialogue panel +#tile store coverage +lp_tile_store_title=Map source tile store coverage +lp_tile_store_title_tips=Displays the regions for the currently selected map source that has been
\ +downloaded and which are therefore offline available in the tile store (tile cache) +lp_tile_store_show_coverage_btn_title=Show coverage +lp_tile_store_show_coverage_btn_tips=Display tile store coverage for the current map source,
\ +the selected zoom level and the current visible map region.
\ +Green regions are present in the cache, gray regions are not covered. +lp_tile_store_hide_coverage_btn_title=Hide coverage +lp_tile_store_zoom_combo_tips=Select the zoom level you wish to display tile store coverage +lp_tile_store_zoom_title=Zoom level: +lp_tile_store_layer_title=Layer: + +#---------- GPX Panel ------------------------- +rp_gpx_new_gpx=New Gpx +rp_gpx_load_gpx=Load Gpx +rp_gpx_save_gpx=Save Gpx +rp_gpx_clear_gpx=Clear List +rp_gpx_add_wpt=Add wpt +rp_gpx_default_node_name=loaded gpx files... +rp_gpx_pop_menu_delete_element=delete element +rp_gpx_pop_menu_rename_element=rename element +rp_gpx_menu_rename=rename +rp_gpx_menu_delete=delete +rp_gpx_msg_confim_delete_title=Delete node +rp_gpx_msg_confim_delete=Do you really want to delete this node? +rp_gpx_rename_element_title=Please input the name: +rp_gpx_msg_can_not_rename_track=Track segments cannot be named. +rp_gpx_msg_ask_create_new=No GPX file selected.\nDo you want to create a new GPX file? +rp_gpx_msg_ask_create_new_title=Add point to new GPX file? +rp_gpx_msg_add_point_failed=Way points can only be added to the gpx file, routes or track segments +rp_gpx_root_default_name_nofile=unnamed (new gpx) +rp_gpx_root_default_name_hasfile=unnamed (file %s) +rp_gpx_unname_route_name=unnamed route +rp_gpx_unname_track_name=unnamed track +rp_gpx_unname_wpt_name=unnamed waypoint +rp_gpx_msg_confirm_reopen_file=One or more Gpx files are already opened.\nDo you want to open new instances of these files? +rp_gpx_msg_error_save_gpx_file=Error saving Gpx file +rp_gpx_msg_no_select_file=No Gpx file selected +rp_gpx_node_seg_name=segment %s + +#---------- Main Menu ------------------------- +#Atlas +menu_atlas=Atlas +menu_atlas_new=New Atlas +menu_atlas_convert_format=Convert Atlas Format +menu_atlas_create=Create Atlas +#Maps +menu_maps=Maps +menu_maps_selection=Selection Mode +menu_maps_selection_rect=Rectangle +menu_maps_selection_polygon=Polygon +menu_maps_selection_circle=Circle +menu_maps_selection_add=Add selection +menu_maps_selection_add_around_gpx=Add selection around GPX track +menu_maps_selection_add_by_gpx=Add selection by GPX track +#Bookmark +menu_bookmark=Bookmarks +menu_bookmark_save=Save current view +menu_bookmark_manage=Manage Bookmarks +#Panels +menu_panels=Panels +menu_show_hide_left_panel=Show/hide left panel +menu_show_hide_gpx_panel=Show/hide GPX panel +#Debug +menu_debug=Debug +menu_debug_show_hide_tile_border=Show/Hide map tile borders +menu_debug_show_all_map_source=Show all map source names +menu_debug_refresh_map_source=Refresh custom map sources +menu_debug_show_log_file=Show log file +menu_debug_log_level=General log level +menu_debug_system_report=Generate system report +#help +menu_help=Help +menu_help_readme=Show Readme +menu_help_how_to_preview=How to use preview map +menu_help_licenses=Licenses +menu_help_about=About +#tools +menu_tool=Tools + +#---------- Dialog ------------------------- +#About +dlg_about_title=About +dlg_about_version=Version: +dlg_about_program_version=Program revision: +dlg_about_download_mapplus=
Download Map Plus +dlg_about_download_mapplus_url=http://itunes.apple.com/us/app/map-plus/id438868200?mt=8&ls=1 +#Font choose +dlg_font_choose_title=Choose font +dlg_font_choose_preview=Preview +dlg_font_choose_name=Name +dlg_font_choose_style=Style +dlg_font_choose_size=Size +#Licenses +dlg_license_title=Licenses +#Help +dlg_help_title=Help +#Manager Bookmark +dlg_mgn_bookmark_title=Manage Bookmarks +dlg_mgn_bookmark_delete=Delete Bookmark +#Bookmark Add +dlg_add_bookmark_msg=please select a name for the new bookmark +#New Atlas +dlg_new_atlas_title=Settings for new Atlas +dlg_new_atlas_name_title=Name of the new atlas: +dlg_new_atlas_default_atlas_name=Unnamed atlas +dlg_new_atlas_select_format_title=Please select the desired atlas format +#Working Progress +dlg_progress_title=Progress +dlg_progress_about_btn=About +dlg_progress_count=Count: 0 +dlg_progress_count_i=Count: %d +#Select GPX Track +dlg_gpx_track_select_title=Selected distance around track +dlg_gpx_track_select_distance=Selected distance around track: %d %s +dlg_gpx_track_select_preview=Preview selection +#Add GPS point +dlg_gpx_inpu_point_name=Please input a name for the new point: +#Show All Map Source +dlg_show_source_title=Map source names +dlg_show_source_column_name=Name +dlg_show_source_column_display_text=Display text +dlg_show_source_column_rev=Revision +dlg_show_source_column_type=Type +#Select Dir +dlg_select_dir_title=Select Directory +dlg_select_dir_description=Directories +#Atlas download progress +dlg_download_title=Atlas creation in progress +dlg_download_zoom_level_progress=Collecting tiles for zoom level +dlg_download_map_progress=Processing maps of atlas: +dlg_download_atlas_progress=%d%% done - processing atlas %s of type %s +dlg_download_done_percent=%d%% done +dlg_download_done_tenthpercent=%.1f%% done +dlg_download_window_title=

ATLAS CREATION IN PROGRESS...

+dlg_download_map_info_label_default=Processing map ABCDEFGHIJKLMNOPQRSTUVWXYZ-nn of layer ABCDEFGHIJKLMNOPQRSTUVWXYZ from map source ABCDEFGHIJKLMNOPQRSTUVWXYZ +dlg_download_map_info_label=Processing map %s of layer %s from map source %s +dlg_download_map_done_count_default=000 of 000 done +dlg_download_map_done_count=%d of %d done +dlg_download_remain_time_default=Time remaining: 00000 minutes 00 seconds +dlg_download_remain_time=Time remaining: %s +dlg_download_tile_done_count_default=1000000 of 1000000 tiles done +dlg_download_tile_done_count=%d of %d tiles done +dlg_download_map_create_title=Map Creation +dlg_download_avg_speed=Average download speed +dlg_download_avg_speed_value=: %s / second +dlg_download_total_bytes=Downloaded +dlg_download_bytes_from_cache=Loaded from tile store +dlg_download_thread_count=Active tile fetcher threads +dlg_download_retry_count=Transient download errors +dlg_download_retry_count_value=: current map: %d, total: %d +dlg_download_failed_count=Unrecoverable download errors +dlg_download_failed_count_value=: current map: %d, total: %d +dlg_download_error_tips=

Download errors for the current map and for the total atlas (transient/unrecoverable)

\ +

Mobile Atlas Creator retries failed tile downloads up to two times.
\ +If the tile downloads fails the second time the tile will be counted as
\ +unrecoverable error and not tried again during the current map creation run.

+dlg_download_total_time=Total creation time +dlg_download_checkbox_ignore_error=Ignore download errors and continue automatically +dlg_download_status_title=Status: +dlg_download_status_running=RUNNING +dlg_download_status_aborted=ABORTED +dlg_download_status_finished=FINISHED +dlg_download_status_paused=PAUSED +dlg_download_btn_abort=Abort creation +dlg_download_btn_abort_tips=Abort current Atlas download +dlg_download_btn_close_win=Close Window +dlg_download_btn_close_win_tips_disable=Atlas creation in progress... +dlg_download_btn_close_win_tips_enable=Close atlas creation progress window +dlg_download_btn_open_folder=Open Atlas Folder +dlg_download_btn_open_folder_tips_disabled=Atlas creation in progress... +dlg_download_btn_open_folder_tips_enabled=Open folder where atlas output folder +dlg_download_btn_pause_resume=Pause/Resume +dlg_download_time_unknown=unknown +dlg_download_abort_title=Atlas creation aborted +dlg_download_abort_window_title=

ATLAS CREATION HAS BEEN ABORTED BY USER

+dlg_download_succeed_title=Atlas creation finished successfully +dlg_download_succeed_window_title=

ATLAS CREATION FINISHED SUCCESSFULLY

+dlg_download_errors_todo=Multiple download errors - how to proceed? +dlg_download_errors_todo_msg=Multiple tile downloads have failed. \ +Something may be wrong with your connection to the download server or your selected area. \ +
Do you want to:

\ +Continue map download and ignore the errors? (results in blank/missing tiles)
\ +Retry to download this map, by starting over?
\ +Skip the current map and continue to process other maps in the atlas?
\ +Abort the current map and atlas creation process?
+dlg_download_errors_missing_tile=Error - tiles are missing - do you want to continue anyway? +dlg_download_errors_missing_tile_msg=Something is wrong with download of atlas tiles.\n\ +The amount of downloaded tiles is not as high as it was calculated.\nTherefore tiles \ +will be missing in the created atlas.\n %d tiles are missing.\n\n\ +Are you sure you want to continue and create the atlas anyway? +dlg_download_show_error_report=Show error report +dlg_download_erro_head=An error occured: +#Splash Frame +splash_title=Mobile Atlas Creator is starting up... + +#---------- Setting ------------------------- +set_title=Settings +#Display +set_display_title=Display +set_display_unit_system_title=Unit System +set_display_unit_system_metric=Metric +set_display_unit_system_imperial=Imperial +set_display_unit_system_scale_bar=Unit system for map scale bar: +set_display_language=Language +set_display_language_choose=Choose Language: +set_display_language_choose_tips=Choose a language and restart MOBAC to take effect. +set_display_language_restart_desc=You need to restart MOBAC after language changes. +set_display_language_msg_title=Restart Required +set_display_language_msg=Please restart MOBAC to make the new language take effect. +set_display_grid=WGS 84 Grid +set_display_grid_compress=Compress labels +set_display_grid_compress_tips=If selected, the coordinate labels will not contain explicit information. +set_display_grid_title=Choose grid color +set_display_grid_width=Width: +set_display_grid_width_tips=The width of the lines. +set_display_grid_color=Color: +set_display_grid_color_tips=The color of the lines and coordinate labels. +set_display_grid_font=Font: +set_display_grid_font_tips=Chooses the font to be used for the coordinate labels. +#Map sources config +set_mapsrc_config_title=Map sources config +set_mapsrc_config_online_update=Map packs online update +set_mapsrc_config_online_update_btn=Perform online update +set_mapsrc_config_online_update_no_update=No updates available +set_mapsrc_config_online_update_done=Updates has been downloaded +set_mapsrc_config_online_update_failed=Update failed +set_mapsrc_config_online_update_msg_outdate=This version of MOBAC is no longer supported for online updates.\ +Please upgrade to the most recent version. +set_mapsrc_config_online_update_msg_noneed=You have already the latest map packs installed. +set_mapsrc_config_online_update_msg_done=%d map packs has been updated.\n\ +Please restart MOBAC for installing the updates. +set_mapsrc_config_osmhiking=Reit- und Wanderkarte ($Abo) +set_mapsrc_config_osmhiking_purchased=Purchased ticket ID: +set_mapsrc_config_osmhiking_howto=How to get a ticket (German) +set_mapsrc_config_osmhiking_howto_url=http://www.wanderreitkarte.de/shop_abo_de.php +#Map sources +set_mapsrc_mgr_title=Map sources +set_mapsrc_mgr_title_enabled=Enabled Map Sources +set_mapsrc_mgr_title_disabled=Disabled Map Sources +set_mapsrc_mgr_move_up_tips=Move selected enabled map
source(s) one position up +set_mapsrc_mgr_move_down_tips=Move selected enabled map
source(s) one position down +set_mapsrc_mgr_move_left_tips=Enable the selected disabled map source(s) +set_mapsrc_mgr_move_right_tips=Disable the selected enabled map source(s) +#Tile Update +set_tile_update_title=Tile update +set_tile_update_desc=Tiles are updated automatically base on the settings below. \ +Each map tile has
an expiry date that is sometimes provided by \ +the server. If the server does
not provide one, the default expiration time is used. +set_tile_update_default_expiration=Default expiration time +set_tile_update_default_expiration_desc=The default expiration time is used for map sources that do not
\ +provide an expiration time for each map tile. +set_tile_update_max_expiration=Maximum expiration time +set_tile_update_min_expiration=Minimum expiration time +#Tile Store +set_tile_store_title=Tile store +set_tile_store_enable_checkbox=Enable tile store for map preview and atlas download +set_tile_store_settings=Tile store settings +set_tile_store_information=Information +set_tile_store_info_mapsrc=Map source +set_tile_store_info_tiles=Tiles +set_tile_store_info_size=Size +set_tile_store_info_disabled_subfix= (unused) +set_tile_store_info_delete_tips=Delete all stored %s tiles. +set_tile_store_info_deleteing_tips=Deleting in progress - please wait +set_tile_store_info_total=Total +#Map Size +set_map_size_title=Map size +set_map_size_settings=Map size settings +set_map_size_max_size_of_rect=Maximum size (width & height) of rectangular maps: +set_map_size_overlap_tiles=Number of tiles to overlap maps regions: +set_map_size_desc=If the area of the selected rectangular region to download\ +is larger in height or width than
the map size it will be splitted into \ +several maps when adding the map selection.
\ +Each map is no larger than the specified maximum map size.
\ +Note that polygonal maps are not affected by this setting!
\ +You can see the number of maps and their region in the atlas content tree.
\ +Changing the maximum map size after an area has been added the atlas has no effect on the atlas.

\ +Note for TrekBuddy users:
TrekBuddy versions before v0.9.88 \ +do not support map sizes larger than 32767.
\ +Newer versions can handle maps up to a size of 1048575. +#Directory +set_directory_title=Directories +set_directory_output=Atlas output directory +set_directory_output_tips=If empty the default directory is used:
%s +set_directory_output_select=Select +set_directory_output_select_dlg_title=Select Directory +#Network +set_net_title=Network +set_net_connection=Network connections +set_net_connection_desc=Number of parallel network connections for tile downloading +set_net_bandwidth_desc=Bandwidth limitation for tile downloading +set_net_bandwidth_unlimited=Unlimited +set_net_proxy=HTTP Proxy +set_net_proxy_settings=Proxy settings: +set_net_proxy_settings_java=Use standard Java proxy settings +set_net_proxy_settings_application=Use application properties +set_net_proxy_settings_custom=Use custom proxy (user defined) +set_net_proxy_settings_custom_auth=Use custom proxy with Authentication +set_net_proxy_host=Proxy host name: +set_net_proxy_port=Proxy port: +set_net_proxy_username=Proxy user: +set_net_proxy_password=Proxy password: +set_net_default=Default +set_net_default_ignore_error=Ignore download errors and continue automatically +#Paper Atlas +set_paper_title=Paper Atlas +#Paper Atlas - Size +set_paper_size=Size +set_paper_size_default=Default +set_paper_size_default_tips=If selected, paper size will be determined by the specifed format. +set_paper_size_default_combo_tips=Chooses a default page format. +set_paper_size_default_landscape=Landscape +set_paper_size_default_landscape_tips=If selected, the paper format will be positioned landscape. +set_paper_size_custom=Custom +set_paper_size_custom_tips=If selected, user can specify the paper size manualy. +set_paper_size_custom_width=Width: +set_paper_size_custom_width_tips=Specified the page width. +set_paper_size_custom_height=Height: +set_paper_size_custom_height_tips=Specified the page height. +set_paper_size_selection=Selection +set_paper_size_selection_tips=If selected, paper size will be determined by the map size. +#Paper Atlas - Additions +set_paper_additions=Additions +set_paper_wgs_grid=WGS 84 Grid +set_paper_wgs_grid_tips=If selected, WGS-84 grid will be painted on the map. +set_paper_wgs_grid_density_tips=Chooses the density for the WGS-84 Grid. +set_paper_paper_nubmer=Page numbers +set_paper_paper_nubmer_tips=If selected, page numbers will be painted on each page. +set_paper_scale_bar=Scale bar +set_paper_scale_tips=If selected, scale bar will be painted on the map. +set_paper_compass=Compass +set_paper_compass_tips=If selected, compass will be painted on the map. +#Paper Atlas - Margins +set_paper_margins=Margins +set_paper_margins_tips=Specifies the margin size. +set_paper_margins_top=Top: +set_paper_margins_left=Left: +set_paper_margins_bottom=Bottom: +set_paper_margins_right=Right: +#Paper Atlas - Advanced +set_paper_advanced=Advanced +set_paper_advanced_compression=Compression: +set_paper_advanced_compression_tips=Specifies the compression level used for PDF document.
\ +Zero means no compression and 9 means best compression.
\ +The higher number the better compression. The default value is 6. +set_paper_advanced_dpi=Resolution +set_paper_advanced_dpi_tips=Specifies the resolution (DPI) to be used for images.
\ +Generally higher values means better quality,
but smaller pixel based objects
\ +( such as scale bar, WGS-84 grid, compass and page numbers).
\ +Default value is 96, better quality can be achieved width 120.
\ +Notice: Make sure, your PDF viewer displays the page with the
\ +selected resolution. +set_paper_advanced_overlap=Overlap: +set_paper_advanced_overlap_tips=Specifies the size of overlap for two neighboring pages. +set_paper_advanced_crop=Crop: +set_paper_advanced_crop_tips=Specifies the number of crop percentage.
\ +If any edge page is covered less than the crop percentage,
\ +it will not be processed. +#Paper Atlas - Actions +set_paper_actions=Actions +set_paper_actions_import_xml=Import from XML +set_paper_actions_import_xml_tip=Imports all Paper Atlas settings from a XML file. +set_paper_actions_export_xml=Export to XML +set_paper_actions_export_xml_tip=Exports all Paper Atlas settings to a XML file. +set_paper_actions_restore_default=Reset to defaults +set_paper_actions_restore_default_tips=Resets Paper Atlas settings to default.
\ +All previous changes will be lost. +set_paper_actions_error_import=Unable to import Paper Atlas settings from file: +set_paper_actions_error_export=Unable to export Paper Atlas settings to file: +set_paper_actions_error_reason=Reason: +set_paper_actions_error_title=An error occured! +set_paper_actions_xml_filter=XML Files (*.xml) +#Error +set_error_saving_title=Error saving settings to file +set_error_saving_msg=Error saving settings to file:\n ( %s ) + +#---------- Message ------------------------- +#common +msg_no_zoom_level_selected=No zoom level selected +msg_no_select_area=Please select an area +#Add GPX Track Polygon +msg_add_gpx_polygon_too_many_track=The select gpx file contains more than one track.\nPlease select the desired track or track segment. +msg_add_gpx_polygon_too_many_segment=The select track contains more than one track segment.\nPlease select the desired track segment. +msg_add_gpx_polygon_no_select=No GPX track segment selected +x=Maximum map size violated +msg_add_gpx_polygon_maxsize=At least one map that has been added violates the maximum map size.\n\ +Splitting a polygonal map into smaller pieces is not yet supported.\n\nDo you want to proceed anyway? +#Convert Atlas +msg_convert_atlas_format=Convert atlas format +msg_convert_incompatible_format=Map incompatible with atlas format +#Log file +msg_no_log_file_config=No file logger configured +msg_no_log_file=Log file does not exists:

%s +#Setting +msg_settings_write_error=Error on writing program settings to "settings.xml" +#Refresh +msg_refresh_all_map_source_done=%d custom file based map sources have been refreshed +#Help +msg_no_found_readme_file=Unable to find README.HTM +#Merge Layer +msg_confirm_merge_layer_title=Confirm layer merging +msg_confirm_merge_layer=Are you sure you want to merge the maps of layer\n %s into layer %s? +msg_merge_layer_failed=Layer merging failed +#Tile Size select +msg_invalid_tile_size=Invalid tile size!
Please enter a number between %d and %d +#Tile Store Coverage +msg_tile_store_failed_retrieve_coverage=Failed to retrieve tile store coverage data.\n\ +May be the selected area and zoom is too large. +#Atlas download thread +msg_out_of_memory_title=Out of memory. +msg_out_of_memory_head=Mobile Atlas Creator has run out of memory. +msg_out_of_memory_detail=\nCurrent maximum memory associated to MOBAC %d MB +msg_atlas_download_abort=Atlas download aborted +msg_too_many_tiles_title=Atlas download prohibited +msg_too_many_tiles_msg=Mobile Atlas Creator has detected that you are trying to\n\ +download an extra ordinary large atlas with a very high number of tiles.\n\ +Please reduce the selected areas on high zoom levels and try again.\n\ +The maximum allowed amount of tiles per atlas is %d\n\ +The number of tile in the currently selected atlas is: %d +msg_atlas_version_mismatch=The loaded atlas belongs to an older version Mobile Atlas Creator.This old version\n\ +used a somehow different atlas profile format which is incompatible to this version.\n\n\ +It is recommended to clear the loaded atlas and delete the affected profile.\n\ +Otherwise various exceptions may be thrown while working with this atlas. +msg_atlas_data_check_failed=At least one problem was detected while loading the saved atlas profile.\n\ +Usually this indicates that the profile file is inconsistent or the file format has changed.\n\n\ +It is recommended to clear the loaded atlas and delete the affected profile.\n\ +Otherwise various exceptions may be thrown while working with this atlas. +#Settings +msg_setting_file_is_changed_by_other_title=Overwrite changes? +msg_setting_file_is_changed_by_other=The settings.xml files has been changed by another application.\n\ +Do you want to overwrite these changes?\n\ +All changes made by the other application will be lost! +msg_settings_file_can_not_parse=Could not read file settings.xml program will exit. +#Others +msg_atlas_is_empty=Atlas is empty - please add at least one selection to atlas content. +msg_environment_not_support_title=Unsupported environment +msg_environment_not_support=Your environment is not supported.
Desktop: %s
Operating system: %s +msg_environment_failed_open_output_title=Failed Open Atlas Directory +msg_environment_failed_open_output=Failed to open atlas output directory.
+msg_environment_use_commond=
Used command: %s
+msg_environment_commond_param=Parameter: %d : %s +msg_environment_lack_memory_title=Warning: low memory +msg_environment_lack_memory=WARNING: Mobile Atlas Creator has been started \ +with a very small amount of memory assigned.
\ +The current maximum usable amount of memory to Mobile Atlas Creator is %s.

\ +Please make sure to start Mobile Atlas Creator in \ +the future via the provided start scripts Mobile Atlas Creator.exe
\ +on Windows or start.sh on Linux/Unix/OSX or add the \ +parameter -Xmx 512M to your startup command.

\ +Example: java -Xmx512M -jar Mobile_Atlas_Creator.jar
\ +
Press OK to continue and start Mobile Atlas Creator
+msg_environment_error_create_dir=Error while creating %s directory %s\n Program will exit. +msg_environment_error_write_file=Unable to write to %s \n %s \n directory\n Please correct file permissions and restart MOBAC. +msg_environment_error_create_setting=Could not create file settings.xml - program will exit. +msg_environment_error_init_mapsrc_dir=Error on initializing map sources directory:\n +msg_environment_error_load_cloudmade=Unable to load custom CloudMade map source. Please make sure the openstreetmap map source bundle is available. +msg_environment_mapsrc_dir_not_exist=Map sources directory does not exist - path:\n %s \n\ +Please make sure you extracted the release zip file\n\ +of MOBAC correctly including all subdirectories! +msg_environment_unable_to_start=Unable to start Mobile Atlas Creator: \n +msg_environment_jre_bellow=The used Java Runtime Environment does not meet the minimum requirements.\n\ +Mobile Atlas Creator requires at least Java 6 (1.6) or higher.\n\ +Please update your Java Runtime before starting Mobile Atlas Creator.\n\n\ +Detected Java Runtime Version: %@ +msg_environment_jre_bellow_title=Java Runtime version problem detected +msg_environment_invalid_source_folder=The specified source folder does not exist:\nMap name: %s\ +\nSource folder: %s +msg_environment_invalid_source_folder_zoom=No zoom directories found:\nMap name: %s\ +\nSource folder: %s +msg_environment_invalid_source_folder_title=Invalid source folder +msg_tools_exec_command_ask=Command to be executed:
%s +msg_tools_exec_command_ask_title=Do you want to execute the command? +msg_tools_exec_error_selected_area=This tool requires an selected area. +msg_custom_map_invalid_source_file_title=Invalid source file +msg_custom_map_invalid_source_file=No database file specified.\nMap name: %s \nFilename: %s +msg_custom_map_invalid_source_zip_title=Invalid source zip +msg_custom_map_invalid_source_zip=The specified source zip does not exist:\nMap name: %s \nZip file: %s +msg_custom_map_failed_open_source_zip_title=Error reading zip file +msg_custom_map_failed_open_source_zip=The specified source zip can not be read:\nMap name: %s \nZip file: %s +msg_custom_map_invalid_source_sqlitedb=The specified source SQLite database does not exist:\nMap name: %s \nFilename: %s +msg_custom_map_source_failed_load_sqlitedb_title=Error loading database +msg_custom_map_source_failed_load_sqlitedb=The specified source SQLite database could not be loaded:\nMap name: %s\ +\nFilename: %s \nError: %s +msg_update_map_pack_error_title=Error loading updated map package +msg_update_map_pack_error=An error occured while installing the updated map package.Please restart MOBAC for restoring the previous version. +msg_error_load_atlas_profile_title=Error loading atlas - continue loading? +msg_error_load_atlas_profile=Error loading atlas:
 %s 
\ +
file: %s line/column: %d / %d
\ +Do you want to continue loading the current atlas?
\ +Continue loading may result in an incomplete or non-working atlas.
+msg_environment_slqite_lib_missing_title="Error - SQLite not available" +msg_environment_slqite_lib_missing=Unable to find the SQLite libraries. \ +These are required for the currently selected atlas format.
Please read the README.HTM \ +section "SQLite atlas formats". " +msg_environment_image_format_not_available_title=Image format not available - libraries missing +msg_environment_image_format_not_available=This image format is requires additional libraries to be installed:
\ +Java Advanced Image library (jai_core.jar & jai_codec.jar)
\ +For more details please see the file README.HTM \ +in section Requirements. +msg_tile_store_access_conflict_title=Multiple instances of Mobile Atlas Creator running +msg_tile_store_access_conflict=Multiple instances of Mobile Atlas Creator are trying \ +to access the same tile store.\n\ +The tile store can only be used by used by one instance at a time.\n\ +Please close the other instance and try again. diff --git a/src/main/resources/mobac/resources/text/localize_fr_FR.properties b/src/main/resources/mobac/resources/text/localize_fr_FR.properties new file mode 100644 index 0000000..1be4321 --- /dev/null +++ b/src/main/resources/mobac/resources/text/localize_fr_FR.properties @@ -0,0 +1,657 @@ +#---------- Version ------------------------- +mobac_version_subfix=International Edition +mobac_version_subfix_dev= +#---------- Basic ------------------------- +OK=OK +Cancel=Annuler +Close=Fermer +Error=Erreur +Unnamed=Sans nom +Warning=Attention +minute=minute +minutes=minutes +second=seconde +seconds=secondes +Information=Information +Continue=Continuer +Abort=Interrompre +Skip=Passer +Retry=Réessayer +Undefined=Indéfini +Exit=Quitter +Bytes=Octets +KiByte=KiOctet +MiByte=MiOctet +GiByte=GiOctet + +#---------- On map controls ------------------------- +map_ctrl_zoom_level_title= Zoom : +map_ctrl_zoom_level_title_tips=Niveau de zoom actuel +map_ctrl_zoom_grid_tips=Affiche une grille du niveau de zoom demandé sur la carte +map_ctrl_zoom_grid_prefix_fmt=Niveau de zoom %d +map_ctrl_zoom_grid_disable=Grille désactivée +map_ctrl_wgs_grid_title=Grille WGS 84 +map_ctrl_wgs_grid_tips=Affiche une grille WGS 84 sur l'aperçu de la carte (non incluse dans l'atlas) +map_ctrl_wgs_grid_density_tips=Indique le pas de la grille WGS 84 +map_ctrl_wgs_grid_density_prefix=Pas de +map_ctrl_wgs_grid_density_degree=degré +map_ctrl_wgs_grid_density_degrees=degrés +map_ctrl_wgs_grid_density_minute=minute +map_ctrl_wgs_grid_density_minutes=minutes +map_ctrl_wgs_grid_density_seconds=secondes +map_ctrl_wgs_grid_density_second=seconde +map_loading_wait=Patientez SVP - chargement des cartes en cours + +#---------- Left Panel ------------------------- +#coordinate +lp_coords_title=Coordonnées de la sélection (min/max) +lp_coords_select_btn_title=Valider les coordonnées +lp_coords_label_N=N +lp_coords_label_E=E +lp_coords_label_W=O +lp_coords_label_S=S +lp_coords_fmt_list_title=Fmt +lp_coords_fmt_degree_eng=Degrés (anglais) +lp_coords_fmt_degree_local=Degrés (local) +lp_coords_fmt_degree_min_eng=Deg Min (anglais) +lp_coords_fmt_degree_min_local=Deg Min (local) +lp_coords_fmt_degree_min_sec_eng=Deg Min Sec (anglais) +lp_coords_fmt_degree_min_sec_local=Deg Min Sec (local) +lp_coords_fmt_tile=Dalle (x ou y / zoom) +lp_coords_invalid_text=Coordonnée invalide !
Tapez un nombre entre %s et %s +#map source +lp_map_source_title=Source de carte +lp_map_source_combo_tips=Sélectionne une source de carte +#zoom level +lp_zoom_title=Niveaux de zoom +lp_zoom_total_tile_count_tips=Nombre total de dalles à télécharger +lp_zoom_number_tips=Sélectionne le niveau de zoom %d de l'atlas +lp_zoom_total_tile_title=%s dalles +lp_zoom_total_tile_hint_head=Nombre total de dalles à télécharger: +lp_zoom_total_tile_hint_row=
Niveau %d : %d (%dx%d) +#Tile parameters settings +lp_tile_param_title=Paramètres de la couche : modification des dalles +lp_tile_param_recreate_checkbox_title=Recréer/ajuster les dalles (calculs intensifs) +lp_tile_param_recreate_checkbox_tips=Si cette option est inactive, chaque dalle (taille : 256x256) est utilisée exactement comme téléchargée (plus rapide).
\ +Sinon chaque nouvelle dalle est recréée, ce qui permet d'utiliser une taille de dalle personnalisée (plus lent / calculs intensifs). +lp_tile_param_width_title=Largeur : +lp_tile_param_width_tips=Largeur des dalles +lp_tile_param_height_title=Hauteur : +lp_tile_param_height_tips=Hauteur des dalles +lp_tile_param_image_fmt_title=Format des dalles : +lp_tile_param_image_fmt_png=PNG +lp_tile_param_image_fmt_png_8bit=PNG 256 couleurs (8 bits) +lp_tile_param_image_fmt_png_4bit=PNG 16 couleurs (4 bits) +lp_tile_param_image_fmt_jpg_q100=JPEG - qualité 100 +lp_tile_param_image_fmt_jpg_q99=JPEG - qualité 99 +lp_tile_param_image_fmt_jpg_q95=JPEG - qualité 95 +lp_tile_param_image_fmt_jpg_q90=JPEG - qualité 90 +lp_tile_param_image_fmt_jpg_q85=JPEG - qualité 85 +lp_tile_param_image_fmt_jpg_q80=JPEG - qualité 80 +lp_tile_param_image_fmt_jpg_q70=JPEG - qualité 70 +lp_tile_param_image_fmt_jpg_q60=JPEG - qualité 60 +lp_tile_param_image_fmt_jpg_q50=JPEG - qualité 50 +lp_tile_param_msg_valid_height=La hauteur de la dalle doit être comprise entre %d et %d.\n +lp_tile_param_msg_valid_width=La largeur de la dalle doit être comprise entre %d et %d.\n +#atlas content +lp_atlas_title=Contenu de l'atlas +lp_atlas_new_btn_title=Nouveau +lp_atlas_add_selection_btn_title=Ajouter la sélection +lp_atlas_name_label_title=Nom : +lp_atlas_name_field_tips=Tapez le nom de l'atlas ici +# this one only support English +#lp_atlas_element_default_name=Layer name +#atlas content detail - Atlas +lp_atlas_info_atlas_title=Atlas
+lp_atlas_info_atlas_name=Nom : %s
+lp_atlas_info_atlas_layer=Nombre de couches : %d
+lp_atlas_info_atlas_format=Format de l'atlas : %s
+lp_atlas_info_max_tile=Nombre maximal de dalles à télécharger : %d
+lp_atlas_info_area_start=Début de la zone : %s %s
+lp_atlas_info_area_end=Fin de la zone : %s %s
+#atlas content detail - Layer +lp_atlas_info_layer_title=Couche
+lp_atlas_info_layer_map_count=Nombre de couches de cartes : %d
+#atlas content detail - Map +lp_atlas_info_map_title=Carte
+lp_atlas_info_polygon_map_title=Sélection polygonale
+lp_atlas_info_polygon_map_point=Points du polygone :
+lp_atlas_info_map_source_short=Source de la carte : %s
+lp_atlas_info_map_source=Source de la carte : %s (%s)
+lp_atlas_info_map_zoom_lv=Niveau de zoom : %d
+lp_atlas_info_map_area_start=Début de la zone : %s (%d / %d)
+lp_atlas_info_map_area_end=Fin de la zone : %s (%d / %d)
+lp_atlas_info_map_size=Taille de la carte : %d x %d pixels
+lp_atlas_info_tile_size=Taille des dalles : %d x %d
+lp_atlas_info_tile_format=Format des dalles : %s
+lp_atlas_info_tile_format_origin=Format des dalles : 256x256 (pas de traitement)
+#atlas content node pop menu +lp_atlas_pop_menu_show_detail=Afficher les détails +lp_atlas_pop_menu_display_select_area=Afficher les zones sélectionnées +lp_atlas_pop_menu_select_map_box=Choisir l'étendue de la carte +lp_atlas_pop_menu_zoom_to_map_box=Zoomer sur l'étendue de la carte +lp_atlas_pop_menu_zoom_to=Zoomer sur +lp_atlas_pop_menu_rename=Renommer +lp_atlas_pop_menu_apply_tile_process=Valider les options de traitement des dalles +lp_atlas_pop_menu_clear_atals=Effacer l'atlas +lp_atlas_pop_menu_delete_node=Supprimer +lp_atlas_default_tip=Utiliser le menu contextuel des options pour voir toutes les commandes disponibles. +#Profile +lp_atlas_profile_title=Profils sauvegardés +lp_atlas_profile_combo_tips=Sélectionnez un profil de création de l'atlas\n ou tapez le nom du nouveau profil +lp_atlas_profile_delete_btn_title=Supprimer +lp_atlas_profile_delete_btn_tips=Supprime un profil d'atlas +lp_atlas_profile_save_btn_title=Enregistrer +lp_atlas_profile_save_btn_tips=Enregistre un profil d'atlas +lp_atlas_profile_load_btn_title=Charger +lp_atlas_profile_load_btn_tips=Charge le profil sélectionné +lp_atlas_profile_refresh_btn_tips=Recharge la liste des profils +lp_atlas_profile_msg_ask_name=Tapez le nom d'un profil +lp_atlas_profile_msg_overwrite_confirm_title=Confirmez SVP +lp_atlas_profile_msg_overwrite_confirm=Le profil %s existe déjà. Remplacer le profil existant ? +#main button +lp_mian_create_btn_title=Créer l'atlas +lp_main_create_btn_tips=Crée l'atlas +lp_main_setting_button_title=Configuration +lp_main_setting_button_tips=Ouvre la boîte de dialogue des préférences +#tile store coverage +lp_tile_store_title=Zones disponibles hors connexion +lp_tile_store_title_tips=Affiche les zones de la source de carte actuellement sélectionnée qui ont été
\ +téléchargées et sont donc disponibles hors connexion dans le stock local de dalles (cache de dalles) +lp_tile_store_show_coverage_btn_title=Afficher les zones disponibles +lp_tile_store_show_coverage_btn_tips=Affiche les zones disponibles dans le stock local de dalles, pour la source de cartes en cours,
\ +le niveau de zoom en cours et la zone de la carte visible actuellement dans l'aperçu.
\ +Les zones en vert sont présentes dans le stock local, celles en gris ne le sont pas. +lp_tile_store_hide_coverage_btn_title=Masquer les zones disponibles +lp_tile_store_zoom_combo_tips=Sélectionne le niveau de zoom pour lequel vous voulez afficher les zones disponibles dans le stock local de dalles +lp_tile_store_zoom_title=Niveau de zoom : +lp_tile_store_layer_title=Couche : + +#---------- GPX Panel ------------------------- +rp_gpx_new_gpx=Nouveau GPX +rp_gpx_load_gpx=Charger GPX +rp_gpx_save_gpx=Enregistrer GPX +rp_gpx_clear_gpx=Effacer la liste +rp_gpx_add_wpt=Ajouter un point de passage +rp_gpx_default_node_name=fichiers GPX chargés... +rp_gpx_pop_menu_delete_element=Supprimer un élément +rp_gpx_pop_menu_rename_element=Renommer un élément +rp_gpx_menu_rename=Renommer +rp_gpx_menu_delete=Supprimer +rp_gpx_msg_confim_delete_title=Supprimer un nœud +rp_gpx_msg_confim_delete=Êtes-vous certain de vouloir supprimer ce nœud ? +rp_gpx_rename_element_title=Tapez le nom : +rp_gpx_msg_can_not_rename_track=Les segments de trace ne peuvent pas être nommés. +rp_gpx_msg_ask_create_new=Aucun fichier GPX sélectionné. Voulez-vous créer un nouveau fichier GPX ? +rp_gpx_msg_ask_create_new_title=Ajouter un point au nouveau fichier GPX ? +rp_gpx_msg_add_point_failed=Les points de passage peuvent seulement être ajoutés au fichier GPX, aux itinéraires ou aux segments de trace. +rp_gpx_root_default_name_nofile=sans_nom (nouveau GPX) +rp_gpx_root_default_name_hasfile=sans_nom (fichier %s) +rp_gpx_unname_route_name=itineraire_sans_nom +rp_gpx_unname_track_name=trace_sans_nom +rp_gpx_unname_wpt_name=point_de_passage_sans_nom +rp_gpx_msg_confirm_reopen_file=Ce (ou ces) fichier(s) GPX est (sont) déjà ouvert(s).\nVoulez-vous ouvrir d'autres instances de ce(s) fichier(s) ? +rp_gpx_msg_error_save_gpx_file=Erreur lors de l'enregistrement du fichier GPX +rp_gpx_msg_no_select_file=Aucun fichier GPX sélectionné +rp_gpx_node_seg_name=segment %s + +#---------- Main Menu ------------------------- +#Atlas +menu_atlas=Atlas +menu_atlas_new=Nouvel atlas +menu_atlas_convert_format=Changer le format de l'atlas +menu_atlas_create=Créer un atlas +#Maps +menu_maps=Cartes +menu_maps_selection=Mode de sélection +menu_maps_selection_rect=Rectangulaire +menu_maps_selection_polygon=Polygonal +menu_maps_selection_circle=Circulaire +menu_maps_selection_add=Ajouter la sélection +menu_maps_selection_add_by_gpx=Sélectionner avec une trace GPX +#Bookmark +menu_bookmark=Favoris +menu_bookmark_save=Enregistrer la vue actuelle +menu_bookmark_manage=Gérer les favoris +#Panels +menu_panels=Panneaux +menu_show_hide_left_panel=Afficher/Masquer le panneau gauche +menu_show_hide_gpx_panel=Afficher/Masquer le panneau GPX +#Debug +menu_debug=Debug +menu_debug_show_hide_tile_border=Afficher/Masquer les limites des dalles +menu_debug_show_all_map_source=Afficher toutes les sources de cartes +menu_debug_refresh_map_source=Mettre à jour les sources de cartes personnalisées +menu_debug_show_log_file=Afficher le journal de bord +menu_debug_log_level=Niveau d'enregistrement du journal de bord +menu_debug_system_report=Créer un rapport système +#help +menu_help=Aide +menu_help_readme=Afficher les notes +menu_help_how_to_preview=Comment utiliser l'aperçu de la carte +menu_help_licenses=Licences +menu_help_about=À propos +#tools +menu_tool=Outils + +#---------- Dialog ------------------------- +#About +dlg_about_title=À propos +dlg_about_version=Version : +dlg_about_program_version=Révision du programme : +dlg_about_download_mapplus=Télécharger Map Plus +dlg_about_download_mapplus_url=http://itunes.apple.com/us/app/map-plus/id438868200?mt=8&ls=1 +#Font choose +dlg_font_choose_title=Choisir la police de caractères +dlg_font_choose_preview=Aperçu +dlg_font_choose_name=Nom +dlg_font_choose_style=Style +dlg_font_choose_size=Taille +#Licenses +dlg_license_title=Licences +#Help +dlg_help_title=Aide +#Manager Bookmark +dlg_mgn_bookmark_title=Gérer les favoris +dlg_mgn_bookmark_delete=Supprimer le favori +#Bookmark Add +dlg_add_bookmark_msg=Tapez le nom du nouveau favori +#New Atlas +dlg_new_atlas_title=Paramètres du nouvel atlas +dlg_new_atlas_name_title=Nom du nouvel atlas : +dlg_new_atlas_default_atlas_name=Atlas_sans_nom +dlg_new_atlas_select_format_title=Choisissez le format de l'atlas +#Working Progress +dlg_progress_title=Progression +dlg_progress_about_btn=À propos +dlg_progress_count=Nombre : 0 +dlg_progress_count_i=Nombre : %d +#Select GPX Track +dlg_gpx_track_select_title=Distance autour de la trace +dlg_gpx_track_select_distance=Distance autour de la trace : %d %s +#Add GPS point +dlg_gpx_inpu_point_name=Tapez le nom du nouveau point : +#Show All Map Source +dlg_show_source_title=Noms des sources de cartes +dlg_show_source_column_name=Nom +dlg_show_source_column_display_text=Afficher le texte +dlg_show_source_column_rev=Révision +dlg_show_source_column_type=Type +#Select Dir +dlg_select_dir_title=Choisissez le dossier +dlg_select_dir_description=Dossiers +#Atlas download progress +dlg_download_title=Création de l'atlas en cours +dlg_download_zoom_level_progress=Récupération des dalles pour le niveau de zoom +dlg_download_map_progress=Traitement des cartes de l'atlas : +dlg_download_atlas_progress=%d%% réalisés - traitement de l'atlas %s de type %s +dlg_download_done_percent=%d%% réalisés +dlg_download_done_tenthpercent=%.1f%% réalisés +dlg_download_window_title=

CRÉATION DE L'ATLAS EN COURS...

+dlg_download_map_info_label_default=Création de la carte ABCDEFGHIJKLMNOPQRSTUVWXYZ-nn de la couche ABCDEFGHIJKLMNOPQRSTUVWXYZ de la source de cartes ABCDEFGHIJKLMNOPQRSTUVWXYZ +dlg_download_map_info_label=Création de la carte %s de la couche %s de la source de cartes %s +dlg_download_map_done_count_default=000 sur 000 réalisés +dlg_download_map_done_count=%d sur %d réalisés +dlg_download_remain_time_default=Temps restant : 00000 minutes 00 secondes +dlg_download_remain_time=Temps restant : %s +dlg_download_tile_done_count_default=1000000 sur 1000000 dalles traitées +dlg_download_tile_done_count=%d sur %d dalles traitées +dlg_download_map_create_title=Création de la carte +dlg_download_avg_speed=Vitesse moyenne de téléchargement +dlg_download_avg_speed_value=: %s / seconde +dlg_download_total_bytes=Téléchargé +dlg_download_bytes_from_cache=Trouvé dans le stock local de dalles +dlg_download_thread_count=Nombre de téléchargements simultanés +dlg_download_retry_count=Erreurs de téléchargement temporaires +dlg_download_retry_count_value=: carte actuelle : %d, total : %d +dlg_download_failed_count=Erreurs de téléchargement permanentes +dlg_download_failed_count_value=: carte actuelle : %d, total: %d +dlg_download_error_tips=

Erreurs de téléchargement pour la carte actuelle et pour l'atlas complet (temporaires/permanentes)

\ +

Si un téléchargement de dalle échoue, Mobile Atlas Creator effectue deux nouvelles tentatives.
\ +Si le téléchargement échoue la seconde fois, la dalle sera considérée comme
\ +non disponible et aucune nouvelle tentative ne sera faite pendant la création de la carte.

+dlg_download_total_time=Temps de création total +dlg_download_checkbox_ignore_error=Ignorer les erreurs de téléchargement et continuer automatiquement +dlg_download_status_title=État : +dlg_download_status_running=EN COURS +dlg_download_status_aborted=INTERROMPU +dlg_download_status_finished=TERMINÉ +dlg_download_status_paused=EN PAUSE +dlg_download_btn_abort=Interrrompre la création +dlg_download_btn_abort_tips=Interrompre le téléchargement en cours +dlg_download_btn_close_win=Fermer la fenêtre +dlg_download_btn_close_win_tips_disable=Création de l'atlas en cours... +dlg_download_btn_close_win_tips_enable=Fermer la fenêtre de proression de la création de l'atlas +dlg_download_btn_open_folder=Ouvrir le dossier des atlas +dlg_download_btn_open_folder_tips_disabled=Création de l'atlas en cours... +dlg_download_btn_open_folder_tips_enabled=Ouvre le dossier contenant l'atlas +dlg_download_btn_pause_resume=Pause/Reprise +dlg_download_time_unknown=inconnu +dlg_download_abort_title=Création de l'atlas interrompue +dlg_download_abort_window_title=

LA CRÉATION DE L'ATLAS A ÉTÉ INTERROMPUE PAR L'UTILISATEUR

+dlg_download_succeed_title=L'atlas a été créé avec succès +dlg_download_succeed_window_title=

L'ATLAS A ÉTÉ CRÉÉ AVCE SUCCÈS

+dlg_download_errors_todo=Nombreuses erreurs de téléchargement - que faire ? +dlg_download_errors_todo_msg=Plusieurs téléchargemenst de dalles ont échoué. \ +Il y a peut-être un problème avec votre connexion Internet ou avec la zone sélectionnée. \ +
Voulez-vous :

\ +Continuer la création de la carte en ignorant les erreurs ? (il y aura des dalles manquantes)
\ +Réessayer de télécharger la carte, en recommençant du début ?
\ +Passer la carte en cours et continuer avec les autres cartes de l'atlas ?
\ +Interrompre la carte en cours et la création de l'atlas ?
+dlg_download_errors_missing_tile=Erreur - des dalles sont manquantes - voulez-vous continuer tout de même ? +dlg_download_errors_missing_tile_msg=Un problème est survenu pendant le chargement des dalles de l'atlas.\n\ +Le nombre de dalles téléchargées est inférieur à celui qui a été calculé.\nPar conséquent, des dalles \ +manqueront dans l'atlas.\n %d dalles manquent.\n\n\ +Voulez-vous continuer et créer tout de même l'atlas ? +dlg_download_show_error_report=Afficher le rapport d'erreur +dlg_download_erro_head=Une erreur est survenue : +#Splash Frame +splash_title=Mobile Atlas Creator démarre... + +#---------- Setting ------------------------- +set_title=Paramètres +#Display +set_display_title=Affichage +set_display_unit_system_title=Système d'unités +set_display_unit_system_metric=Métrique +set_display_unit_system_imperial=Impérial +set_display_unit_system_scale_bar=Système d'unités pour l'échelle : +set_display_language=Langue +set_display_language_choose=Choisissez la langue : +set_display_language_choose_tips=Choisissez la langue et redémarrez MOBAC. +set_display_language_restart_desc=Vous devez redémarrer MOBAC après le changement de langue. +set_display_language_msg_title=Redémarrage indispensable. +set_display_language_msg=Veuillez redémarrer MOBAC pour qu'il affiche la nouvelle langue. +set_display_grid=Grille WGS 84 +set_display_grid_compress=Étiquettes compactes +set_display_grid_compress_tips=Si sélectionné, les étiquettes ne contiendront plus d'informations redondantes. +set_display_grid_title=Choix de la couleur de la grille +set_display_grid_width=Épaisseur : +set_display_grid_width_tips=Épaisseur des lignes. +set_display_grid_color=Couleur: +set_display_grid_color_tips=Couleur des lignes et des coordonnées. +set_display_grid_font=Police de caractères : +set_display_grid_font_tips=Choix de la police de caractètres utilisée pour l'affichage des coordonnées. +#Map sources config +set_mapsrc_config_title=Configuration des sources de cartes +set_mapsrc_config_online_update=Mise à jour des sources de cartes +set_mapsrc_config_online_update_btn=Rechercher une mise à jour +set_mapsrc_config_online_update_no_update=Aucune mise à jour disponible +set_mapsrc_config_online_update_done=Les mises à jour ont été téléchargées +set_mapsrc_config_online_update_failed=La mise à jour a échoué +set_mapsrc_config_online_update_msg_outdate=Cette version de MOBAC n'est plus supportée pour les mises à jours en ligne.\ +Veuillez installer la dernière version disponible. +set_mapsrc_config_online_update_msg_noneed=La dernière version des sources de carte est déjà installée. +set_mapsrc_config_online_update_msg_done=%d sources de cartes ont été mises à jour.\n\ +Veuillez redémarrer MOBAC pour installer les mises à jour. +set_mapsrc_config_osmhiking=Reit- und Wanderkarte ($Abo) +set_mapsrc_config_osmhiking_purchased=Purchased ticket ID: +set_mapsrc_config_osmhiking_howto=How to get a ticket (German) +set_mapsrc_config_osmhiking_howto_url=http://www.wanderreitkarte.de/shop_abo_de.php +#Map sources +set_mapsrc_mgr_title=Sources de cartes +set_mapsrc_mgr_title_enabled=Sources de cartes actives +set_mapsrc_mgr_title_disabled=Sources de cartes inactives +set_mapsrc_mgr_move_up_tips=Monter la carte active sélectionnée
d'une ligne +set_mapsrc_mgr_move_down_tips=Descendre la carte active sélectionnée
d'une ligne +set_mapsrc_mgr_move_left_tips=Activer la(les) carte(s) sélectionnée(s) +set_mapsrc_mgr_move_right_tips=Désactiver la(les) carte(s) sélectionnée(s) +#Tile Update +set_tile_update_title=Mise à jour des dalles +set_tile_update_desc=Les dalles sont mises à jour automatiquement selon les paramètres suivants. \ +Chaque dalle possède
une date d'expiration qui est parfois fournie par \ +le serveur. Si le serveur n'en fournit
pas une, la durée de validité par défaut est utilisée. +set_tile_update_default_expiration=Durée de validité par défaut +set_tile_update_default_expiration_desc=La durée de validité par défaut est utilisée pour les sources de cartes
\ +qui ne fournissent pas une date d'expiration pour chaque dalle. +set_tile_update_max_expiration=Durée de validité maximale +set_tile_update_min_expiration=Durée de validité minimale +#Tile Store +set_tile_store_title=Stockage local des dalles +set_tile_store_enable_checkbox=Activer le stockage local des dalles pour l'aperçu de la carte et le téléchargement de l'atlas +set_tile_store_settings=Paramètre du stockage local de dalles +set_tile_store_information=Information +set_tile_store_info_mapsrc=Source de cartes +set_tile_store_info_tiles=Dalles +set_tile_store_info_size=Taille +set_tile_store_info_disabled_subfix= (inutilisé) +set_tile_store_info_delete_tips=Supprimer toutes les dalles %s. +set_tile_store_info_deleteing_tips=Suppression en cours - patientez SVP +set_tile_store_info_total=Total +#Map Size +set_map_size_title=Taille des cartes +set_map_size_settings=Paramètres des tailles de cartes +set_map_size_max_size_of_rect=Taille maximale (largeur et hauteur) des cartes rectangulaires : +set_map_size_overlap_tiles=Nombre de dalles communes à deux zones voisines (recouvrement) : +set_map_size_desc=Si la taille de la zone à télécharger est plus grande que la taille maximale de la carte,
la zone sera coupée en plusieurs morceaux lors de l'ajout de la zone sélectionnée.
\ +Chaque morceau ne peut être plus grand que la taille maximale de carte spécifiée.
\ +Remarque : les cartes polygonales ne sont pas concernées par ce paramètre !
\ +Vous pouvez voir le nom de cartes et leurs zones dans l'arborescence de l'atlas.
\ +La modification de la taille maximale n'a aucun effet sur les zones déjà ajoutées à l'atlas.

\ +Remarque pour les utilisateurs de TrekBuddy :
Les versions de TrekBuddy versions antérieures à la version v0.9.88 \ +ont une taille maximale de cartes de 32767 pixels.
\ +Les nouvelles versions peuvent gérer des cartes jusqu'à une taille de 1048575 pixels. +#Directory +set_directory_title=Dossiers +set_directory_output=Dossier de stockage des atlas +set_directory_output_tips=Si laissé vide, le dossier par défaut est utilisé :
%s +set_directory_output_select=Sélectionner +set_directory_output_select_dlg_title=Sélectionnez un dossier +#Network +set_net_title=Réseau +set_net_connection=Connexions réseau +set_net_connection_desc=Nombre maximal de connexions simultanées pour le téléchargement des dalles +set_net_bandwidth_desc=Limitation de débit pour le téléchargement des dalles +set_net_bandwidth_unlimited=Non limité +set_net_proxy=Mandataire HTTP +set_net_proxy_settings=Paramètres du mandataire : +set_net_proxy_settings_java=Utiliser les paramètres standards de Java +set_net_proxy_settings_application=Utiliser les paramètres de l'application +set_net_proxy_settings_custom=Utiliser des paramètres personnalisés (définis par l'utilisateur) +set_net_proxy_settings_custom_auth=Utiliser des paramètres personnalisés avec authentification +set_net_proxy_host=Nom de l'hôte : +set_net_proxy_port=Port : +set_net_proxy_username=Utilisateur : +set_net_proxy_password=Mot de passe : +set_net_default=Défaut +set_net_default_ignore_error=Ignorer les erreurs de téléchargement et continuer automatiquement +#Paper Atlas +set_paper_title=Atlas papier +#Paper Atlas - Size +set_paper_size=Taille +set_paper_size_default=Défaut +set_paper_size_default_tips=Si coché, la taille du papier sera déterminée pat le format choisi. +set_paper_size_default_combo_tips=Choisissez un format de page par défaut. +set_paper_size_default_landscape=Paysage +set_paper_size_default_landscape_tips=Si sélectionné, le papier sera positionné dans le sens paysage. +set_paper_size_custom=Personnalisé +set_paper_size_custom_tips=Si sélectionné, l'utilisateur peut choisir la taille du papier. +set_paper_size_custom_width=Largeur : +set_paper_size_custom_width_tips=Largeur de la page. +set_paper_size_custom_height=Hauteur : +set_paper_size_custom_height_tips=Hauteur de la page. +set_paper_size_selection=Sélection +set_paper_size_selection_tips=Si sélectionné, la taille du papier sera déterminée par la taille de la carte. +#Paper Atlas - Additions +set_paper_additions=Compléments +set_paper_wgs_grid=Grille WGS 84 +set_paper_wgs_grid_tips=Si sélectionné, une grille WGS 84 sera dessinée sur la carte. +set_paper_wgs_grid_density_tips=Choisissez le pas de la grille WGS-84. +set_paper_paper_nubmer=Numéros de page +set_paper_paper_nubmer_tips=Si sélectionné, un numéro de page apparaîtra sur chaque page. +set_paper_scale_bar=Échelle +set_paper_scale_tips=Si sélectionné, l'échelle apparaîtra sur la carte. +set_paper_compass=Rose des vents +set_paper_compass_tips=Si sélectionné, une rose des vents apparaîtra sur la carte. +#Paper Atlas - Margins +set_paper_margins=Marges +set_paper_margins_tips=Taille des marges. +set_paper_margins_top=Haut : +set_paper_margins_left=Gauche : +set_paper_margins_bottom=Bas : +set_paper_margins_right=Droite : +#Paper Atlas - Advanced +set_paper_advanced=Paramètres avancés +set_paper_advanced_compression=Compression: +set_paper_advanced_compression_tips=Indique le niveau de compression utilisé dans le fichier PDF.
\ +0 correspond à aucune compression et 9 à la compression maximale.
\ +La valeur par défaut est 6. +set_paper_advanced_dpi=Résolution +set_paper_advanced_dpi_tips=Indique la résolution (DPI) à utiliser pour les images.
\ +En général, plus la valeur sera grande et meilleure sera la qualité,
mais les surimpressions seront plus petites
\ +(par exemple l'échelle, la grille WGS-84, la rose des vents et les numéros de page).
\ +La valeur par défaut et 96, une meilleure qualité peut être obtenue avec la valeur 120.
\ +Avertissement : Assurez vous que votre visualisateur PDF est capable d'afficher la page
\ +avec la résolution choisie. +set_paper_advanced_overlap=Recouvrement: +set_paper_advanced_overlap_tips=Indique la taille du recouvrement entre deux pages voisines. +set_paper_advanced_crop=Découpage : +set_paper_advanced_crop_tips=Indique le pourcentage de couverture minimal d'une page.
\ +Si une page du bord est remplie moins que ce pourcentage,
\ +elle sera supprimée. +#Paper Atlas - Actions +set_paper_actions=Actions +set_paper_actions_import_xml=Importer XML +set_paper_actions_import_xml_tip=Importe tous les paramètres de l'atlas papier à partir d'un fichier XML. +set_paper_actions_export_xml=Exporter XML +set_paper_actions_export_xml_tip=Exporte tous les paramètres de l'atlas papier vers un fichier XML. +set_paper_actions_restore_default=Réinitialiser tous les paramètres à la valeur par défaut. +set_paper_actions_restore_default_tips=Remet tous les paramètres à la valeur par défaut.
\ +Toutes les modifications effectuées seront perdues. +set_paper_actions_error_import=Impossible d'importer les paramètres de l'atlas papier à partir du fichier : +set_paper_actions_error_export=Impossible d'exporter les paramètres de l'atlas papier vers le fichier : +set_paper_actions_error_reason=Raison : +set_paper_actions_error_title=Une erreur est survenue ! +set_paper_actions_xml_filter=Fichiers XML (*.xml) +#Error +set_error_saving_title=Erreur lors de l'enregistrement des paramètres +set_error_saving_msg=Erreur lors de l'enregistrement des paramètres dans le fichier :\n ( %s ) + +#---------- Message ------------------------- +#common +msg_no_zoom_level_selected=Auncun niveau de zoon sélectionné +msg_no_select_area=Veuillez sélectionner une zone +#Add GPX Track Polygon +msg_add_gpx_polygon_too_many_track=Le fichier GPX sélectionné contient plus d'un trace.\nVeuillez sélectionner une trace ou un segment de trace. +msg_add_gpx_polygon_too_many_segment=La trace sélectionnée contient plus d'un segment de trace.\nVeuillez sélectionner un segment de trace. +msg_add_gpx_polygon_no_select=Aucun segment de trace n'a été sélectionné. +x=Maximum map size violated +msg_add_gpx_polygon_maxsize=Au moins une carte dépasse la taille maximale autorisée.\n\ +Le découpage automatique d'une carte polygonale en morceaux plus petits n'est pas encore implémenté.\n\nVoulez-vous continuer tout de même ? +#Convert Atlas +msg_convert_atlas_format=Changer le format de l'atlas +msg_convert_incompatible_format=Carte incompatible avec le format de l'atlas +#Log file +msg_no_log_file_config=Aucin fichier journal configuré +msg_no_log_file=Le fichier journal n'existe pas :

%s +#Setting +msg_settings_write_error=Erreur lors de l'écriture des paramètres du programme dans "settings.xml" +#Refresh +msg_refresh_all_map_source_done=%d sources de cartes personnalisées ont été mises à jour +#Help +msg_no_found_readme_file=Fichier README.HTM introuvable +#Merge Layer +msg_confirm_merge_layer_title=Confirmer la fusion des couches +msg_confirm_merge_layer=Êtes-vous certain de vouloir superposer la couche\n %s à la couche %s? +msg_merge_layer_failed=La fusion des couches a échoué +#Tile Size select +msg_invalid_tile_size=Taille de dalle incorrecte !
Tapez un nombre entre %d et %d +#Tile Store Coverage +msg_tile_store_failed_retrieve_coverage=Impossible de trouver les informations sur l'étendue du stock de dalles.\n\ +Il est possible que la zone sélectionnée ou le zoom soient trop grands. +#Atlas download thread +msg_out_of_memory_title=Mémoire insuffisante. +msg_out_of_memory_head=Mobile Atlas Creator n'a pas assez de mémoire disponible. +msg_out_of_memory_detail=\nLa taille de la mémoire allouée à MOBAC est actuellement de %d Mo +msg_atlas_download_abort=Téléchargement de l'atlas interrompu +msg_too_many_tiles_title=Téléchargement de l'atlas interdit +msg_too_many_tiles_msg=Mobile Atlas Creator a détecté que vous essayez\n\ +de télécharger un atlas énorme avec un très grand nombre de dalles.\n\ +Veuillez réduire les zones sélectionnées avec des niveaux de zoom élevés et recommencez.\n\ +Le nombre maximal de dalles autorisé par atlas est : %d\n\ +Le nombre maximal de dalles dans l'atlas en cours est : %d +msg_atlas_version_mismatch=Cet atlas a été créé avec une ancienne version de MOBAC. Cette vieille version\n\ +utilisait un format de profil d'atlas légèrement différent et incompatible avec la version actuelle de MOBAC.\n\n\ +Vous devriez effacer cet atlas et supprimer le profil correspondant.\n\ +Sinon, des erreurs pourront survenir pendant l'utilisation de l'atlas. +msg_atlas_data_check_failed=Un problème a été détecté pendant le chargement du profil de l'atlas.\n\ +En général, cela signifie que le fichier de profil est incorrect ou que le format du fichier a changé.\n\n\ +Vous devriez effacer cet atlas et supprimer le profil correspondant.\n\ +Sinon, des erreurs pourront survenir pendant l'utilisation de l'atlas. +#Settings +msg_setting_file_is_changed_by_other_title=Enregistrer les modifications ? +msg_setting_file_is_changed_by_other=Le fichier settings.xml a été modifié par une autre application.\n\ +Voulez-vous sauvegarder vos modifications ?\n\ +Toutes les modifications faites par l'autre application seront perdues ! +msg_settings_file_can_not_parse=Impossible de lire le fichier settings.xml - Le programme va s'arrêter. +#Others +msg_atlas_is_empty=L'atlas est vide - ajoutez au moins une zone à l'atlas. +msg_environment_not_support_title=Environnement non supporté +msg_environment_not_support=Votre environnement n'est pas supporté.
Bureau : %s
Système : %s +msg_environment_failed_open_output_title=Impossible d'ouvrir le dossier des atlas +msg_environment_failed_open_output=Impossible d'ouvrir le dossier des atlas.
+msg_environment_use_commond=
Commande utilisée : %s
+msg_environment_commond_param=Paramètre : %d : %s +msg_environment_lack_memory_title=Attention : mémoire disponible très faible +msg_environment_lack_memory=ATTENTION : Mobile Atlas Creator a été lancé \ +avec une quantité très faible de mémoire allouée.
\ +La quantité maximale de mémoire utilisable actuellement par MOBAC est %s.

\ +La prochaine fois, assurez-vous de démarrer Mobile Atlas Creator\ +en utilisant les scripts fournis Mobile Atlas Creator.exe
\ +pour Windows or start.sh pour Linux/Unix/OSX, ou ajoutez \ +le paramètre -Xmx 512M à votre commande de lancement.

\ +Exemple : java -Xmx512M -jar Mobile_Atlas_Creator.jar
\ +
Cliquez sur OK pour continuer et dèmarrer Mobile Atlas Creator
+msg_environment_error_create_dir=Erreur lors de la création du dossier %s %s\n Le programme va s'arrêter. +msg_environment_error_write_file=Impossible d'écrire dans le dossier %s \n %s \n Corrigez les droits d'accès aux fichiers et redémarrez MOBAC. +msg_environment_error_create_setting=Impossible de créer le fichier settings.xml - le programme va s'arrêter. +msg_environment_error_init_mapsrc_dir=Erreur lors de l'initialisation du dossier de sources de cartes :\n +msg_environment_error_load_cloudmade=Impossible de charger la source de carte personnalisée CloudMade. Vérifiez que le lot de sources de cartes OpenStreetMap est disponible. +msg_environment_mapsrc_dir_not_exist=Le dossier des sources de dalles n'existe pas - chemin :\n %s \n\ +Vérifiez que vous avez bien décompacté l'archive ZIP\n\ +de MOBAC, y compris tous les sous-dossiers ! +msg_environment_unable_to_start=Impossible de lancer Mobile Atlas Creator : \n +msg_environment_jre_bellow=L'environnement d'exécution Java ne possède pas les caractéristiques minimales requises.\n\ +Mobile Atlas Creator nécessite Java 6 (1.6) ou plus récent.\n\ +Veuillez mettre à jour votre environnement d'exécution Java avant de lancer Mobile Atlas Creator.\n\n\ +Version de l'environnement Java : %@ +msg_environment_jre_bellow_title=Problème de version de l'environnement Java +msg_environment_invalid_source_folder=Le dossier demandé n'existe pas :\nMap name: %s\ +\nDossier source : %s +msg_environment_invalid_source_folder_zoom=Aucun dossier de niveau de zoom n'a été trouvé :\nNom de la carte : %s\ +\nDossier source : %s +msg_environment_invalid_source_folder_title=Dossier source invalide +msg_tools_exec_command_ask=Commande à exécuter :
%s +msg_tools_exec_command_ask_title=Voulez-vous exécuter cette commande ? +msg_custom_map_invalid_source_file_title=Fichier source invalide +msg_custom_map_invalid_source_file=Aucun fichier de base de données n'a été fourni.\nNom de la carte : %s \nNom du fichier : %s +msg_custom_map_invalid_source_zip_title=Fichier ZIP source invalide +msg_custom_map_invalid_source_zip=Le fichier ZIP source indiqué n'existe pas :\nNom de la carte: %s \nFichier ZIP : %s +msg_custom_map_failed_open_source_zip_title=Erreur lors de la lecture du fichier ZIP +msg_custom_map_failed_open_source_zip=Le fichier ZIP source indiqué n'a pas pu être lu :\nNom de la carte : %s \nFichier ZIP : %s +msg_custom_map_invalid_source_sqlitedb=La base de données SQLite indiquée n'existe pas :\nNom de la carte : %s \nNom du fichier : %s +msg_custom_map_source_failed_load_sqlitedb_title=Erreur lors du chargement de la base de données +msg_custom_map_source_failed_load_sqlitedb=Cette base de données source SQLite n'a pas pu être chargéee :\nNom de la carte : %s\ +\nNom du fichier: %s \nErreur: %s +msg_update_map_pack_error_title=Erreur lors du chargement de la mise à jour des sources de cartes +msg_update_map_pack_error=Une erreur est survenue lors de l'installation de la mise à jour des sources de cartes. Veuillez redémarrer MOBAC pour recharger la version précédente. +msg_error_load_atlas_profile_title=Erreur pendant le téléchargement de l'atlas - continuer ? +msg_error_load_atlas_profile=Erreur pendant le téléchargement de l'atlas :
 %s 
\ +
fichier : %s ligne/colonne : %d / %d
\ +Voulez-vous continuer le téléchargement de l'atlas ?
\ +La poursuite du téléchargement pourrait produire un atlas incomplet ou inutilisable.
+msg_environment_slqite_lib_missing_title="Erreur - SQLite non disponible" +msg_environment_slqite_lib_missing=Impossible de trouver les bibliothèques SQLite. \ +Elles sont indispensables pour le format d'atlas sélectionné.
Veuillez lire la page README.HTM \ +paragraphe "SQLite atlas formats". " +msg_environment_image_format_not_available_title=Format d'image non disponible - bibliothèques manquantes +msg_environment_image_format_not_available=Ce format d'image nécessite l'installation de biliothèques supplémentaires :
\ +Java Advanced Image library (jai_core.jar et jai_codec.jar)
\ +Pour plus de détails, lisez la page README.HTM \ +paragraphe Requirements. +msg_tile_store_access_conflict_title=Plusieurs instances de Mobile Atlas Creator sont en cours d'exécution +msg_tile_store_access_conflict=Plusieurs instances de Mobile Atlas Creator essaient \ +d'accéder au même stock local de dalles.\n\ +Le stock local de dalles ne peut être utilisé que par une seule instance de MOBAC à la fois.\n\ +Veuillez fermer l'autre instance et recommencer. diff --git a/src/main/resources/mobac/resources/text/localize_ja_JP.properties b/src/main/resources/mobac/resources/text/localize_ja_JP.properties new file mode 100644 index 0000000..19e6300 --- /dev/null +++ b/src/main/resources/mobac/resources/text/localize_ja_JP.properties @@ -0,0 +1,653 @@ +#---------- Version ------------------------- +mobac_version_subfix=多言語版 +mobac_version_subfix_dev= +#---------- Basic ------------------------- +OK=確定 +Cancel=キャンセル +Close=閉じる +Error=エラー +Unnamed=名称未設定 +Warning=警告 +minute=分 +minutes=分 +second=秒 +seconds=秒 +Information=情報 +Continue=続行 +Abort=中断 +Skip=スキップ +Retry=再度 +Undefined=未定義 +Exit=終了 +Bytes=バイト +KiByte=キロバイト +MiByte=メガバイト +GiByte=ギガバイト + +#---------- On map controls ------------------------- +map_ctrl_zoom_level_title=ズーム値 +map_ctrl_zoom_level_title_tips=現在のズーム値 +map_ctrl_zoom_grid_tips=グリッドの表示レベル +map_ctrl_zoom_grid_prefix_fmt=グリッド値 : %d +map_ctrl_zoom_grid_disable=グリッド 非表示 +map_ctrl_wgs_grid_title=WGS 84 グリッド +map_ctrl_wgs_grid_tips=WGS 84 グリッドの表示/非表示 +map_ctrl_wgs_grid_density_tips=WGS 84 グリッドの表示密度 +map_ctrl_wgs_grid_density_prefix= +map_ctrl_wgs_grid_density_degree=° +map_ctrl_wgs_grid_density_degrees=° +map_ctrl_wgs_grid_density_minute=′ +map_ctrl_wgs_grid_density_minutes=′ +map_ctrl_wgs_grid_density_seconds=″ +map_ctrl_wgs_grid_density_second=″ +map_loading_wait=お待ちください - マップデータの読み込み中... + +#---------- Left Panel ------------------------- +#coordinate +lp_coords_title=選択範囲の座標値 (最大値/最小値) +lp_coords_select_btn_title=座標の範囲を選択 +lp_coords_label_N=北 +lp_coords_label_E=東 +lp_coords_label_W=西 +lp_coords_label_S=南 +lp_coords_fmt_list_title=単位 +lp_coords_fmt_degree_eng=度 (英語) +lp_coords_fmt_degree_local=度 +lp_coords_fmt_degree_min_eng=度 分 (英語) +lp_coords_fmt_degree_min_local=度 分 +lp_coords_fmt_degree_min_sec_eng=度 分 秒 (英語) +lp_coords_fmt_degree_min_sec_local=度 分 秒 +lp_coords_fmt_tile=タイル ( X または Y 座標 / ズーム ) +lp_coords_invalid_text=無効な座標値です!
%s と %s の間の数値を入力してください +#map source +lp_map_source_title=地図データの取得元 +lp_map_source_combo_tips=地図データの取得元を選択します +#zoom level +lp_zoom_title=ズームレベル +lp_zoom_total_tile_count_tips=ダウンロードされるタイルの総数 +lp_zoom_number_tips=ズームレベル %d のタイルを取得します +lp_zoom_total_tile_title=タイルの総数 : %s +lp_zoom_total_tile_hint_head=ダウンロードされるタイルの総数 +lp_zoom_total_tile_hint_row=
ズームレベル %d : %d (%dx%d) +#Tile parameters settings +lp_tile_param_title=レイヤ設定 (タイル処理設定) +lp_tile_param_recreate_checkbox_title=タイルのサイズ変更とファイル形式変換 +lp_tile_param_recreate_checkbox_tips=\ +有効化するとタイルのサイズ変更とファイル形式変換を実行します。
\ +作成する地図(アトラス)全体に同じ条件のタイル処理を適用したり、
\ +レイヤやマップ毎に別条件のタイル処理を適用することもできます。
\ +なお、この処理には CPU パワーが必要なので時間が掛かります。 +lp_tile_param_width_title=幅 : +lp_tile_param_width_tips=タイルの幅 +lp_tile_param_height_title=高さ : +lp_tile_param_height_tips=タイルの高さ +lp_tile_param_image_fmt_title=ファイル形式 : +lp_tile_param_image_fmt_png=PNG +lp_tile_param_image_fmt_png_8bit=PNG 256 色 (8 ビット) +lp_tile_param_image_fmt_png_4bit=PNG 16 色 (4 ビット) +lp_tile_param_image_fmt_jpg_q100=JPEG - 品質 100 +lp_tile_param_image_fmt_jpg_q99=JPEG - 品質 99 +lp_tile_param_image_fmt_jpg_q95=JPEG - 品質 95 +lp_tile_param_image_fmt_jpg_q90=JPEG - 品質 90 +lp_tile_param_image_fmt_jpg_q85=JPEG - 品質 85 +lp_tile_param_image_fmt_jpg_q80=JPEG - 品質 80 +lp_tile_param_image_fmt_jpg_q70=JPEG - 品質 70 +lp_tile_param_image_fmt_jpg_q60=JPEG - 品質 60 +lp_tile_param_image_fmt_jpg_q50=JPEG - 品質 50 +lp_tile_param_msg_valid_height=タイルの高さの許容範囲は %d 以上 %d 以下です。\n +lp_tile_param_msg_valid_width=タイルの幅の許容範囲は %d 以上 %d 以下です。\n +#atlas content +lp_atlas_title=地図 (アトラス) +lp_atlas_new_btn_title=新規作成 +lp_atlas_add_selection_btn_title=選択範囲を地図に追加 +lp_atlas_name_label_title=レイヤ名 : +lp_atlas_name_field_tips=選択範囲を地図に追加する際のレイヤ名を入力します +# this one only support English +#lp_atlas_element_default_name=Layer name +#atlas content detail - Atlas +lp_atlas_info_atlas_title=アトラス
+lp_atlas_info_atlas_name=名称 : %s
+lp_atlas_info_atlas_layer=レイヤ数 : %d
+lp_atlas_info_atlas_format=地図形式 : %s
+lp_atlas_info_max_tile=最大タイル数 : %d
+lp_atlas_info_area_start=開始座標 : %s %s
+lp_atlas_info_area_end=終了座標 : %s %s
+#atlas content detail - Layer +lp_atlas_info_layer_title=レイヤ
+lp_atlas_info_layer_map_count=マップ数 : %d
+#atlas content detail - Map +lp_atlas_info_map_title=矩形マップ
+lp_atlas_info_polygon_map_title=ポリゴンマップ
+lp_atlas_info_polygon_map_point=ポリゴンポイント :
+lp_atlas_info_map_source_short=地図データ : %s
+lp_atlas_info_map_source=地図データ : %s (%s)
+lp_atlas_info_map_zoom_lv=ズームレベル : %d
+lp_atlas_info_map_area_start=開始座標 : %s (%d / %d)
+lp_atlas_info_map_area_end=終了座標 : %s (%d / %d)
+lp_atlas_info_map_size=マップサイズ : %d x %d ピクセル
+lp_atlas_info_tile_size=タイルサイズ : %d x %d
+lp_atlas_info_tile_format=タイル形式 : %s
+lp_atlas_info_tile_format_origin=タイル形式 : 256x256 (タイル処理なし)
+#atlas content node pop menu +lp_atlas_pop_menu_show_detail=詳細表示 +lp_atlas_pop_menu_display_select_area=エリアを地図上に表示 +lp_atlas_pop_menu_select_map_box=エリアを選択 +lp_atlas_pop_menu_zoom_to_map_box=エリアをズーム表示 +lp_atlas_pop_menu_zoom_to=ズーム表示 +lp_atlas_pop_menu_rename=名称変更 +lp_atlas_pop_menu_apply_tile_process=タイル処理の適用 +lp_atlas_pop_menu_clear_atals=地図(アトラス)の消去 +lp_atlas_pop_menu_delete_node=削除 +lp_atlas_default_tip=右クリックでメニューを表示します +#Profile +lp_atlas_profile_title=プロファイル +lp_atlas_profile_combo_tips=プロファイルを選択またはプロファイル名を入力します +lp_atlas_profile_delete_btn_title=削除 +lp_atlas_profile_delete_btn_tips=プロファイルを削除します +lp_atlas_profile_save_btn_title=保存 +lp_atlas_profile_save_btn_tips=プロファイルを保存します +lp_atlas_profile_load_btn_title=読み込み +lp_atlas_profile_load_btn_tips=プロファイルを読み込みます +lp_atlas_profile_refresh_btn_tips=プロファイルリストを再読み込みします +lp_atlas_profile_msg_ask_name=プロファイル名を入力してください +lp_atlas_profile_msg_overwrite_confirm_title=確認 +lp_atlas_profile_msg_overwrite_confirm=プロファイル %s は既に存在します。上書きしますか? +#main button +lp_mian_create_btn_title=地図(アトラス)の構築処理の実行 +lp_main_create_btn_tips=地図データの取得元からタイルをダウンロードして
\地図(アトラス)の構築処理を実行します +lp_main_setting_button_title=設定 +lp_main_setting_button_tips=設定画面を開きます +#tile store coverage +lp_tile_store_title=タイル取得済みエリアの表示 +lp_tile_store_title_tips=\ +選択した地図データの取得元について、
\ +タイルを既に取得したエリアを表示します +lp_tile_store_show_coverage_btn_title=表示 +lp_tile_store_show_coverage_btn_tips=\ +タイルを既に取得したエリアを表示します。
\ +緑色のエリアはタイル取得済みで、
\ +灰色のエリアはタイル未取得です。 +lp_tile_store_hide_coverage_btn_title=非表示 +lp_tile_store_zoom_combo_tips=タイル取得済みエリアを表示するズームレベルを選択します +lp_tile_store_zoom_title=ズームレベル : +lp_tile_store_layer_title=地図データ : + +#---------- GPX Panel ------------------------- +rp_gpx_new_gpx=新規作成 +rp_gpx_load_gpx=読み込み +rp_gpx_save_gpx=保存 +rp_gpx_clear_gpx=リストの消去 +rp_gpx_add_wpt=ポイントの追加 +rp_gpx_default_node_name=GPX ファイルの読み込み中... +rp_gpx_pop_menu_delete_element=削除 +rp_gpx_pop_menu_rename_element=名称変更 +rp_gpx_menu_rename=名称変更 +rp_gpx_menu_delete=削除 +rp_gpx_msg_confim_delete_title=削除 +rp_gpx_msg_confim_delete=本当に削除しますか? +rp_gpx_rename_element_title=名称を入力してください : +rp_gpx_msg_can_not_rename_track=トラックに名称を付けることはできません。 +rp_gpx_msg_ask_create_new=GPX ファイルが選択されていません。\n作成しますか? +rp_gpx_msg_ask_create_new_title=GPX ファイルにポイントを追加しますか? +rp_gpx_msg_add_point_failed=ポイントの追加は、GPX ファイル, ルート, トラックに対してのみ可能です +rp_gpx_root_default_name_nofile=新規 GPX +rp_gpx_root_default_name_hasfile=ファイル %s +rp_gpx_unname_route_name=ルート +rp_gpx_unname_track_name=トラック +rp_gpx_unname_wpt_name=ポイント +rp_gpx_msg_confirm_reopen_file=一つ以上の GPX ファイルが既に読み込まれています。\nこれらのファイルを新たに読み込みますか? +rp_gpx_msg_error_save_gpx_file=GPX ファイルの保存エラーが発生しました +rp_gpx_msg_no_select_file=Gpx ファイルが選択されていません +rp_gpx_node_seg_name=トラック %s + +#---------- Main Menu ------------------------- +#Atlas +menu_atlas=地図(アトラス) +menu_atlas_new=地図(アトラス)の新規作成 +menu_atlas_convert_format=地図形式の変更 +menu_atlas_create=地図(アトラス)の構築処理の実行 +#Maps +menu_maps=タイル選択 +menu_maps_selection=選択モード +menu_maps_selection_rect=矩形選択 +menu_maps_selection_polygon=ポリゴン選択 +menu_maps_selection_circle=円形選択 +menu_maps_selection_add=選択範囲を地図に追加 +menu_maps_selection_add_by_gpx=GPX に基づいて地図に追加 +#Bookmark +menu_bookmark=ブックマーク +menu_bookmark_save=地図の表示状態のブックマーク +menu_bookmark_manage=ブックマークの管理 +#Panels +menu_panels=パネル +menu_show_hide_left_panel=左パネルの表示/非表示 +menu_show_hide_gpx_panel=右パネルの表示/非表示 +#Debug +menu_debug=Debug +menu_debug_show_hide_tile_border=タイルの境界線の表示/非表示 +menu_debug_show_all_map_source=地図データの取得元のリスト表示 +menu_debug_refresh_map_source=地図データの取得元の再読み込み +menu_debug_show_log_file=ログファイルの表示 +menu_debug_log_level=ログレベルの変更 +menu_debug_system_report=システムレポートの作成 +#help +menu_help=ヘルプ +menu_help_readme=Readme の表示 +menu_help_how_to_preview=キーボードとマウスによる操作 +menu_help_licenses=ライセンス +menu_help_about=このソフトウェアについて +#tools +menu_tool=ツール + +#---------- Dialog ------------------------- +#About +dlg_about_title=このソフトウェアについて +dlg_about_version=バージョン : +dlg_about_program_version=リビジョン : +dlg_about_download_mapplus=Map Plus のダウンロード +dlg_about_download_mapplus_url=http://itunes.apple.com/us/app/map-plus/id438868200?mt=8&ls=1 +#Font choose +dlg_font_choose_title=フォントの選択 +dlg_font_choose_preview=プレビュー +dlg_font_choose_name=名称 +dlg_font_choose_style=スタイル +dlg_font_choose_size=サイズ +#Licenses +dlg_license_title=ライセンス +#Help +dlg_help_title=ヘルプ +#Manager Bookmark +dlg_mgn_bookmark_title=ブックマークの管理 +dlg_mgn_bookmark_delete=ブックマークの削除 +#Bookmark Add +dlg_add_bookmark_msg=ブックマーク名を入力してください +#New Atlas +dlg_new_atlas_title=地図(アトラス)の設定 +dlg_new_atlas_name_title=地図(アトラス)の名称 +dlg_new_atlas_default_atlas_name=アトラス +dlg_new_atlas_select_format_title=地図形式を選択してください +#Working Progress +dlg_progress_title=進捗状況 +dlg_progress_about_btn=アバウト +dlg_progress_count=カウント : 0 +dlg_progress_count_i=カウント : %d +#Select GPX Track +dlg_gpx_track_select_title=幅の設定 +dlg_gpx_track_select_distance=トラックに沿って選択するエリアの幅 : %d %s +#Add GPS point +dlg_gpx_inpu_point_name=ポイントの名称を入力してください : +#Show All Map Source +dlg_show_source_title=地図データの取得元のリスト +dlg_show_source_column_name=名称 +dlg_show_source_column_display_text=表示テキスト +dlg_show_source_column_rev=リビジョン +dlg_show_source_column_type=タイプ +#Select Dir +dlg_select_dir_title=フォルダの選択 +dlg_select_dir_description=フォルダ +#Atlas download progress +dlg_download_title=地図(アトラス)の作成中... +dlg_download_zoom_level_progress=タイル取得 : ズームレベル +dlg_download_map_progress=マップ処理 +dlg_download_atlas_progress=%d%% 実行中 - 名称 "%s", 地図形式 "%s" の処理中 +dlg_download_done_percent=%d%% +dlg_download_done_tenthpercent=%.1f%% +dlg_download_window_title=

地図(アトラス)の作成中...

+dlg_download_map_info_label_default=Processing map ABCDEFGHIJKLMNOPQRSTUVWXYZ-nn of layer ABCDEFGHIJKLMNOPQRSTUVWXYZ from map source ABCDEFGHIJKLMNOPQRSTUVWXYZ +dlg_download_map_info_label=処理中 : マップ %s , レイヤ %s , 地図データ %s +dlg_download_map_done_count_default=000 of 000 done +dlg_download_map_done_count=%d / %d +dlg_download_remain_time_default=Time remaining: 00000 minutes 00 seconds +dlg_download_remain_time=残り時間 : %s +dlg_download_tile_done_count_default=1000000 of 1000000 tiles done +dlg_download_tile_done_count=%d / %d タイル +dlg_download_map_create_title=地図(アトラス)の構築 +dlg_download_avg_speed=平均 DL 速度 +dlg_download_avg_speed_value=: %s / 秒 +dlg_download_total_bytes=ダウンロード +dlg_download_bytes_from_cache=保管タイルの読込 +dlg_download_thread_count=DL スレッド数 +dlg_download_retry_count=一時的エラー +dlg_download_retry_count_value=: 処理中: %d, 全体: %d +dlg_download_failed_count=修復不能エラー +dlg_download_failed_count_value=: 処理中: %d, 全体: %d +dlg_download_error_tips=

処理中のマップと地図全体におけるダウンロードの一時的エラーと修復不能エラー

\ +

Mobile Atlas Creator はタイルのダウンロードの失敗を一時的エラーとして扱い、
\ +さらにタイルの再ダウンロードにも失敗すると修復不能エラーとして扱います。
\ +この場合、現在の地図作成中に再びダウンロードを試みることはありません。

+dlg_download_total_time=所要時間 +dlg_download_checkbox_ignore_error=ダウンロードエラーを無視して処理を続行する +dlg_download_status_title=状態 : +dlg_download_status_running=処理中 +dlg_download_status_aborted=中断 +dlg_download_status_finished=終了 +dlg_download_status_paused=停止 +dlg_download_btn_abort=処理中断 +dlg_download_btn_abort_tips=地図(アトラス)作成を中断します +dlg_download_btn_close_win=ウィンドウを閉じる +dlg_download_btn_close_win_tips_disable=地図(アトラス)の作成中... +dlg_download_btn_close_win_tips_enable=地図作成の進捗状況表示ウィンドウを閉じる +dlg_download_btn_open_folder=地図(アトラス)フォルダを開く +dlg_download_btn_open_folder_tips_disabled=地図(アトラス)の作成中... +dlg_download_btn_open_folder_tips_enabled=地図(アトラス)が作成されたフォルダを開く +dlg_download_btn_pause_resume=停止/再開 +dlg_download_time_unknown=不明 +dlg_download_abort_title=地図(アトラス)の作成を中断しました +dlg_download_abort_window_title=

ユーザーにより地図(アトラス)の作成が中断されました

+dlg_download_succeed_title=地図(アトラス)の作成に成功しました +dlg_download_succeed_window_title=

地図(アトラス)の作成に成功しました

+dlg_download_errors_todo=ダウンロードエラーが多数発生しました - 続行しますか? +dlg_download_errors_todo_msg=複数のタイルのダウンロードに失敗しました。\ +サーバーへの接続またはエリアの選択に問題があると思われます。\ +
どうしますか? :

\ +エラーを無視してダウンロードを 続行 する。(タイルに空白部分が生じます)
\ +ダウンロードを 再試行 する。?
\ +現在のマップを スキップ して他のマップの構築処理を続行する。
\ +地図(アトラス)の構築処理を 中断 する。
+dlg_download_errors_missing_tile=エラー - タイルがありません - とにかく続行しますか? +dlg_download_errors_missing_tile_msg=タイルのダウンロードに問題があります。\n\ +タイルのダウンロード数が試算したほど多くありません。\nしたがって、構築した地図(アトラス)ではタイルの不足があると思われます。\n タイルの不足数は %d です。\n\n\ +とにかく続行して地図(アトラス)を構築しますか? +dlg_download_show_error_report=エラーレポートの表示 +dlg_download_erro_head=エラーが発生しました : +#Splash Frame +splash_title=Mobile Atlas Creator の起動中... + +#---------- Setting ------------------------- +set_title=設定 +#Display +set_display_title=表示 +set_display_unit_system_title=単位系 +set_display_unit_system_metric=キロメートル・メートル +set_display_unit_system_imperial=マイル・フィート +set_display_unit_system_scale_bar=距離目盛りの単位 : +set_display_language=言語 +set_display_language_choose=言語の選択 : +set_display_language_choose_tips=言語を選択します。その後 MOBAC を再起動してください +set_display_language_restart_desc=言語を変更した後は MOBAC の再起動が必要です +set_display_language_msg_title=再起動の要求 +set_display_language_msg=言語の変更を有効にするには MOBAC を再起動してください +set_display_grid=WGS 84 グリッド +set_display_grid_compress=ラベルの短縮表示 +set_display_grid_compress_tips=有効にすると、座標ラベルが短縮して表示されます +set_display_grid_title=グリッドの色の選択 +set_display_grid_width=幅 : +set_display_grid_width_tips=グリッド線の幅を指定します +set_display_grid_color=色 : +set_display_grid_color_tips=グリッド線と座標ラベルの色を選択します +set_display_grid_font=フォント : +set_display_grid_font_tips=座標ラベルに使用するフォントを選択します +#Map sources config +set_mapsrc_config_title=地図パック +set_mapsrc_config_online_update=地図パックのオンライン更新 +set_mapsrc_config_online_update_btn=オンライン更新の実行 +set_mapsrc_config_online_update_no_update=更新はありません +set_mapsrc_config_online_update_done=更新をダウンロードしました +set_mapsrc_config_online_update_failed=更新に失敗しました +set_mapsrc_config_online_update_msg_outdate=このバージョンの MOBAC ではオンライン更新がサポートされません。\ +MOBAC 最新のバージョンを使用してください +set_mapsrc_config_online_update_msg_noneed=最新の地図パックが既にインストールされています +set_mapsrc_config_online_update_msg_done=地図パックが更新されました : %d\n\ +更新をインストールするには MOBAC を再起動してください +set_mapsrc_config_osmhiking=Reit- und Wanderkarte ($Abo) +set_mapsrc_config_osmhiking_purchased=購入済みチケットの ID: +set_mapsrc_config_osmhiking_howto=チケットの取得方法 (ドイツ語) +set_mapsrc_config_osmhiking_howto_url=http://www.wanderreitkarte.de/shop_abo_de.php +#Map sources +set_mapsrc_mgr_title=地図データ +set_mapsrc_mgr_title_enabled=有効 +set_mapsrc_mgr_title_disabled=無効 +set_mapsrc_mgr_move_up_tips=選択した地図を一つ上に移動します +set_mapsrc_mgr_move_down_tips=選択した地図を一つ上に移動します +set_mapsrc_mgr_move_left_tips=選択した地図を有効化します +set_mapsrc_mgr_move_right_tips=選択した地図を無効化します +#Tile Update +set_tile_update_title=タイルの更新 +set_tile_update_desc=タイルは、設定した有効期限に基づいて自動的に更新されます。\ +タイルには、ときにサーバーで指定されている
有効期限があります。\ +もしサーバーで有効期限が指定されていない場合はデフォルトの有効期限が適用されます。

+set_tile_update_default_expiration=デフォルトの有効期限 +set_tile_update_default_expiration_desc=デフォルトの有効期限は、タイルの有効期限が地図データの取得元で指定されていない場合に適用されます。 +set_tile_update_max_expiration=最長有効期限 +set_tile_update_min_expiration=最短有効期限 +#Tile Store +set_tile_store_title=タイルの保管 +set_tile_store_enable_checkbox=タイルの保管を有効にして、地図のプレビューとダウンロードに使用する +set_tile_store_settings=タイルの保管設定 +set_tile_store_information=情報 +set_tile_store_info_mapsrc=地図データの取得元 +set_tile_store_info_tiles=タイル数 +set_tile_store_info_size=容量 +set_tile_store_info_disabled_subfix= (未使用) +set_tile_store_info_delete_tips=%s のタイルを全削除します +set_tile_store_info_deleteing_tips=削除しています - しばらくお待ちください +set_tile_store_info_total=合計 +#Map Size +set_map_size_title=マップサイズ +set_map_size_settings=マップサイズの設定 +set_map_size_max_size_of_rect=マップの最大サイズ (幅と高さ) : +set_map_size_overlap_tiles=マップの重複部分のタイル数 : +set_map_size_desc=\ +矩形選択エリアの幅や高さがマップの最大サイズを超える場合、\ +地図に追加する際に複数のマップに分割されます。
\ +各マップは最大サイズより大きくなりません。\ +ポリゴンで選択したマップには適用されないことに注意してください!
\ +マップの数とマップのエリアは地図(アトラス)のツリー表示で確認できます。
\ +選択範囲を地図に追加した後で最大サイズを変更しても効果はありません。

\ +TrekBuddy ユーザーへの注意 :
\ +バージョン 0.9.88 以前の TrekBuddy では 32767 より大きいマップサイズをサポートしていません。
\ +これより新しいバージョンでは、最大でサイズが 1048575 のマップを取り扱えます。 +#Directory +set_directory_title=フォルダ +set_directory_output=地図(アトラス)の出力フォルダ +set_directory_output_tips=空欄の場合はデフォルトのフォルダに出力されます :
%s +set_directory_output_select=選択 +set_directory_output_select_dlg_title=フォルダを選択 +#Network +set_net_title=ネットワーク +set_net_connection=ネットワーク接続 +set_net_connection_desc=タイルのダウンロード時の並列ネットワーク接続数 +set_net_bandwidth_desc=タイルのダウンロードの帯域制限 +set_net_bandwidth_unlimited=無制限 +set_net_proxy=HTTP プロキシ +set_net_proxy_settings=プロキシ設定: +set_net_proxy_settings_java=Java の標準プロキシ設定を使用する +set_net_proxy_settings_application=アプリケーションの設定を使用する +set_net_proxy_settings_custom=(ユーザーが定義した)カスタムプロキシを使用する +set_net_proxy_settings_custom_auth=認証カスタムプロキシを使用する +set_net_proxy_host=ホスト名 : +set_net_proxy_port=ポート : +set_net_proxy_username=ユーザー名 : +set_net_proxy_password=パスワード : +set_net_default=デフォルト +set_net_default_ignore_error=ダウンロードエラーを無視して自動的に続行する +#Paper Atlas +set_paper_title=ペーパーアトラス +#Paper Atlas - Size +set_paper_size=サイズ +set_paper_size_default=デフォルト +set_paper_size_default_tips=ページサイズを規定値から選択します +set_paper_size_default_format_tips=ページサイズを選択します +set_paper_size_default_landscape=ランドスケープ +set_paper_size_default_landscape_tips=用紙方向をランドスケープ(横置き)にします +set_paper_size_custom=カスタム +set_paper_size_custom_tips=任意のページサイズを指定します +set_paper_size_custom_width=幅 : +set_paper_size_custom_width_tips=ページの幅 +set_paper_size_custom_height=高さ : +set_paper_size_custom_height_tips=ページの高さ +set_paper_size_selection=地図全体 +set_paper_size_selection_tips=地図全体を1ページにします +#Paper Atlas - Additions +set_paper_additions=追加の項目 +set_paper_wgs_grid=WGS 84 グリッド +set_paper_wgs_grid_tips=WGS 84 グリッドを表示します +set_paper_wgs_grid_density_tips=WGS 84 グリッドの表示密度を選択します +set_paper_paper_nubmer=ページ番号 +set_paper_paper_nubmer_tips=ページ番号を表示します +set_paper_scale_bar=距離目盛り +set_paper_scale_tips=距離目盛りを表示します +set_paper_compass=コンパス +set_paper_compass_tips=コンパスを表示します +#Paper Atlas - Margins +set_paper_margins=マージン +set_paper_margins_tips=マージンを指定します +set_paper_margins_top=上 : +set_paper_margins_left=左 : +set_paper_margins_bottom=下 : +set_paper_margins_right=右 : +#Paper Atlas - Advanced +set_paper_advanced=詳細設定 +set_paper_advanced_compression=圧縮レベル +set_paper_advanced_compression_tips=PDF ファイルに使用する圧縮レベルを指定します。
圧縮レベル 0 は無圧縮で、圧縮レベルの数値が大きいほど高圧縮となり、
圧縮レベル 9 が最高値です。デフォルトの圧縮レベルは 6 です。 +set_paper_advanced_dpi=解像度 +set_paper_advanced_dpi_tips=画像の解像度(DPI)を指定します。
一般的に数値が大きいほど高品質になりますが、
グリッドや距離目盛り等が小さく表示されます。
デフォルトの解像度は 96 DPI で、
120 DPI の指定でより高品質になります。 +set_paper_advanced_overlap=のりしろ +set_paper_advanced_overlap_tips=隣接ページとの重ね合わせ部分(のりしろ)のサイスを指定します +set_paper_advanced_crop=クロップ +set_paper_advanced_crop_tips=\ +クロップ率を指定します。地図の周辺部が表示されるページにおいて、
\ +ページ面積に対する地図の表示面積がクロップ割合より小さい場合、
\ +このページを地図の構築処理に使用しないようにします。 +#Paper Atlas - Actions +set_paper_actions=設定のインポートとエクスポート +set_paper_actions_import_xml=XML ファイルをインポート +set_paper_actions_import_xml_tip=ペーパーアトラスの設定を XML ファイルからインポートします +set_paper_actions_export_xml=XML ファイルにエクスポート +set_paper_actions_export_xml_tip=ペーパーアトラスの設定を XML ファイルにエクスポートします +set_paper_actions_restore_default=デフォルト値にリセット +set_paper_actions_restore_default_tips=ペーパーアトラスの各設定をデフォルト値に戻します。
\ +以前の変更は失われます。 +set_paper_actions_error_import=ペーパーアトラスの設定をインポートできません : +set_paper_actions_error_export=ペーパーアトラスの設定をエクスポートできません : +set_paper_actions_error_reason=理由 : +set_paper_actions_error_title=エラー発生! +set_paper_actions_xml_filter=XML ファイル (*.xml) +#Error +set_error_saving_title=設定の保存エラー +set_error_saving_msg=設定の保存エラー :\n ( %s ) + +#---------- Message ------------------------- +#common +msg_no_zoom_level_selected=ズームレベルが選択されていません +msg_no_select_area=タイルを選択してください +#Add GPX Track Polygon +msg_add_gpx_polygon_too_many_track=選択された GPX ファイルには複数のトラックが含まれています。\n必要なトラックまたはトラックセグメントを選択してください。 +msg_add_gpx_polygon_too_many_segment=選択されたトラックには複数のトラックセグメントが含まれています。\n必要なトラックセグメントを選択してください。 +msg_add_gpx_polygon_no_select=GPX トラックが選択されていません +x=最大マップサイズを超えています +msg_add_gpx_polygon_maxsize=少なくとも一つのマップが最大サイズを超えています。\n\ +ポリゴンマップを小さく分割する機能はまだサポートされていません。\n\n構わずに続行しますか? +#Convert Atlas +msg_convert_atlas_format=地図形式の変更 +msg_convert_incompatible_format=このマップは地図形式と互換性がありません +#Log file +msg_no_log_file_config=ファイルロガーが設定されていません +msg_no_log_file=ログファイルがありません :

%s +#Setting +msg_settings_write_error=プログラム設定を "settings.xml" に書き込む際にエラーが発生しました +#Refresh +msg_refresh_all_map_source_done=地図データの取得元を再読み込みしました : %d ファイル +#Help +msg_no_found_readme_file=README.HTM が見つかりません +#Merge Layer +msg_confirm_merge_layer_title=レイヤの統合の確認 +msg_confirm_merge_layer=本当にレイヤ %s をレイヤ %s に統合しますか? +msg_merge_layer_failed=レイヤの統合に失敗しました +#Tile Size select +msg_invalid_tile_size=無効なタイルサイズです!
%d と %d の間の数値を入力してください +#Tile Store Coverage +msg_tile_store_failed_retrieve_coverage=タイルを既に取得したエリアのデータ収集に失敗しました。\n\ +おそらく選択範囲とズームレベルが大きすぎます。 +#Atlas download thread +msg_out_of_memory_title=メモリ不足 +msg_out_of_memory_head=Mobile Atlas Creator の実行に必要なメモリが不足しています。 +msg_out_of_memory_detail=\nMOBAC に割り当てられたメモリの最大値は %d MB です +msg_atlas_download_abort=地図(アトラス)のダウンロードを中断しました +msg_too_many_tiles_title=ダウンロードの差し止め +msg_too_many_tiles_msg=膨大な量のタイルがダウンロードされることを Mobile Atlas Creator が検知しました。\n\ +高いズームレベルでの選択エリアを削減してから再試行してください。\n\ +地図(アトラス)に許容されるタイル数の最大値は %d です。\n\ +現在の地図(アトラス)に含まれるタイル数 : %d +msg_atlas_version_mismatch=読み込まれたアトラスは古いバージョンの Mobile Atlas Creator のものです。\n\ +この古いバージョンでは現在のバージョンと互換性のないプロファイル形式が使用されています。\n\n\ +読み込んだアトラスを消去すると共に影響が及んだプロファイルを削除することをおすすめします。\n\ +そうしないと、このアトラスで作業している間に様々な例外が発生します。 +msg_atlas_data_check_failed=保存されたプロファイルの読み込み中に少なくとも一つの問題が検出されました。\n\ +通常これはプロファイルに互換性がないかファイル形式が変更されたことを示唆します。\n\n\ +読み込んだアトラスを消去すると共に影響が及んだプロファイルを削除することをおすすめします。\n\ +そうしないと、このアトラスで作業している間に様々な例外が発生します。 +#Settings +msg_setting_file_is_changed_by_other_title=変更の上書き +msg_setting_file_is_changed_by_other=settings.xml ファイルが他のアプリケーションから変更されました。\n\ +これらの変更を上書きしますか?\n\ +他のアプリケーションによる変更点はすべて失われます! +msg_settings_file_can_not_parse=settings.xml ファイルを読み込むことができません。プログラムを終了します。 +#Others +msg_atlas_is_empty=地図(アトラス)に選択範囲が追加されていません。 - 少なくとも一つの選択範囲を地図(アトラス)に追加してください +msg_environment_not_support_title=サポート外の環境 +msg_environment_not_support=この環境はサポートされていません。
デスクトップ : %s
オペレーティングシステム : %s +msg_environment_failed_open_output_title=出力フォルダのオープンエラー +msg_environment_failed_open_output=地図(アトラス)の出力フォルダのオープンに失敗しました。
+msg_environment_use_commond=
使用コマンド : %s
+msg_environment_commond_param=パラメーター : %d : %s +msg_environment_lack_memory_title=警告 : メモリ不足 +msg_environment_lack_memory=警告 : メモリの割り当てが非常に少ない状態で Mobile Atlas Creator が起動されました。
\ +現在 Mobile Atlas Creator が使用できるメモリの最大値は %s です。

\ +今後 Mobile Atlas Creator を起動する際は、\ +Windows では Mobile Atlas Creator.exe から起動するか、
\ +Linux/Unix/OSX では start.sh から起動するか、\ +-Xmx 512M パラメーターをコマンドに追加してください。

\ +例 : java -Xmx512M -jar Mobile_Atlas_Creator.jar
\ +
Mobile Atlas Creator の起動を続行するには OK ボタンを押してください
+msg_environment_error_create_dir=%s フォルダの作成エラーが発生しました。\nプログラムを終了します。 +msg_environment_error_write_file=%s に書き込むことができません。\n %s \n フォルダ\n ファイルのパーミッションを修正してから MOBAC を再度起動してください。 +msg_environment_error_create_setting=settings.xml ファイルを作成できませんでした。- プログラムを終了します。 +msg_environment_error_init_mapsrc_dir=mapsources フォルダの初期化でエラーが発生しました :\n +msg_environment_error_load_cloudmade=CloudMade のカスタムマップソースを読み込むことができません。openstreetmap のマップソースバンドルが利用可能であることを確認してください。 +msg_environment_mapsrc_dir_not_exist=mapsources フォルダが存在しません。 - フォルダパス :\n %s \n\ +MOBAC の zip ファイルがサブフォルダを含めて適切に展開されていることを確認してください! +msg_environment_unable_to_start=Mobile Atlas Creator を起動できません : \n +msg_environment_jre_bellow=使用中の Java Runtime Environment は最小限の要求を満たしていません。\n\ +Mobile Atlas Creator は少なくとも Java 6 (1.6) またはそれ以上を必要とします。\n\ +Mobile Atlas Creator を起動する前に Java Runtime を更新してください。\n\n\ +検出された Java Runtime のバージョン : %@ +msg_environment_jre_bellow_title=Java Runtime バージョンの問題検出 +msg_environment_invalid_source_folder=指定された mapsources フォルダが存在しません :\n地図の名称 : %s\ +\nmapources フォルダ: %s +msg_environment_invalid_source_folder_zoom=ズームフォルダが見つかりません :\n地図の名称 : %s\ +\nmapsources フォルダ : %s +msg_environment_invalid_source_folder_title=不正な mapsources フォルダ +msg_tools_exec_command_ask=実行されるコマンド :
%s +msg_tools_exec_command_ask_title=コマンドの実行 +msg_custom_map_invalid_source_file_title=不正なソースファイル +msg_custom_map_invalid_source_file=データベースファイルが指定されていません。\n地図の名称 : %s \nファイル名 : %s +msg_custom_map_invalid_source_zip_title=不正なソース zip ファイル +msg_custom_map_invalid_source_zip=指定されたソース zip ファイルが存在しません :\n地図の名称 : %s \nZip ファイル : %s +msg_custom_map_failed_open_source_zip_title=zip ファイルの読み込みエラー +msg_custom_map_failed_open_source_zip=指定されたソース zip ファイルを読み込むことができません :\n地図の名称 : %s \nZip ファイル : %s +msg_custom_map_invalid_source_sqlitedb=指定されたソース sqlite データベースファイルが存在しません :\n地図の名称 : %s \nファイル名 : %s +msg_custom_map_source_failed_load_sqlitedb_title=データベースの読み込みエラー +msg_custom_map_source_failed_load_sqlitedb=指定されたソース sqlite データベースファイルを読み込むことができません :\n地図の名称 : %s\ +\nファイル名 : %s \nエラー : %s +msg_update_map_pack_error_title=更新された地図パックの読み込みエラー +msg_update_map_pack_error=更新された地図パックのインストール中にエラーが発生しました。前のバージョンをリストアするには MOBAC を再起動してください。 +msg_error_load_atlas_profile_title=アトラスの読み込みエラー - 読み込みを続行しますか? +msg_error_load_atlas_profile=アトラスの読み込みエラー :
 %s 
\ +
ファイル : %s 行/カラム : %d / %d
\ +現在のアトラスの読み込みを続行しますか?
\ +読み込みを続行するとアトラスが不完全または機能不全になるかもしれません。
+msg_environment_slqite_lib_missing_title="エラー - SQLite が使用できません" +msg_environment_slqite_lib_missing=SQLite ライブラリが見つかりません。\ +現在選択されている地図(アトラス)形式に必要です。
\ +README.HTM の "SQLite アトラス形式" の項目を参照してください。 +msg_environment_image_format_not_available_title=画像形式が利用できません - ライブラリが見つかりません +msg_environment_image_format_not_available=この画像形式には追加ライブラリがインストールされている必要があります :
\ +Java Advanced Image library (jai_core.jar と jai_codec.jar)
\ +詳細は README.HTM ファイルの\ +Requirements の項目を参照してください。 +msg_tile_store_access_conflict_title=Mobile Atlas Creator の複数起動 +msg_tile_store_access_conflict=複数の Mobile Atlas Creator が\ +同じタイル保管フォルダにアクセスしようとしています。\n\ +タイル保管フォルダは一度に一つの Mobile Atlas Creator からのみ使用できます。\n\ +他の Mobile Atlas Creator を終了してから再試行してください。 diff --git a/src/main/resources/mobac/resources/text/localize_zh_CN.properties b/src/main/resources/mobac/resources/text/localize_zh_CN.properties new file mode 100644 index 0000000..4774056 --- /dev/null +++ b/src/main/resources/mobac/resources/text/localize_zh_CN.properties @@ -0,0 +1,662 @@ +#---------- Version ------------------------- +mobac_version_subfix=多语言版 +mobac_version_subfix_dev= +#---------- Basic ------------------------- +OK=确定 +Cancel=取消 +Close=关闭 +Error=出错 +Unnamed=未命名 +Warning=警告 +minute=分 +minutes=分 +second=秒 +seconds=秒 +Information=消息 +Continue=继续 +Abort=终止 +Skip=跳过 +Retry=重试 +Undefined=未定义 +Exit=退出 +Bytes=字节 +KiByte=KB +MiByte=MB +GiByte=GB + +#---------- On map controls ------------------------- +map_ctrl_zoom_level_title=缩放等级 +map_ctrl_zoom_level_title_tips=当前缩放等级 +map_ctrl_zoom_grid_tips=在地图上显示指定缩放等级的图片块网格 +map_ctrl_zoom_grid_prefix_fmt=%d 级网格 +map_ctrl_zoom_grid_disable=不显示网格 +map_ctrl_wgs_grid_title=显示经纬度网格 +map_ctrl_wgs_grid_tips=在地图上显示经纬度网格线(不会保存到地图册) +map_ctrl_wgs_grid_density_tips=设置经纬度网格的显示密度 +map_ctrl_wgs_grid_density_prefix=每 +map_ctrl_wgs_grid_density_degree=度 +map_ctrl_wgs_grid_density_degrees=度 +map_ctrl_wgs_grid_density_minute=分 +map_ctrl_wgs_grid_density_minutes=分 +map_ctrl_wgs_grid_density_seconds=秒 +map_ctrl_wgs_grid_density_second=秒 +map_loading_wait=正在载入地图数据,请稍等... + +#---------- Left Panel ------------------------- +#coordinate +lp_coords_title=坐标信息 +lp_coords_select_btn_title=选择该区域 +lp_coords_label_N=北 +lp_coords_label_E=东 +lp_coords_label_W=西 +lp_coords_label_S=南 +lp_coords_fmt_list_title=单位 +lp_coords_fmt_degree_eng=度(英语) +lp_coords_fmt_degree_local=度(本地) +lp_coords_fmt_degree_min_eng=度 分(英文) +lp_coords_fmt_degree_min_local=度 分 (本地) +lp_coords_fmt_degree_min_sec_eng=度 分 秒 (英语) +lp_coords_fmt_degree_min_sec_local=度 分 秒 (本地) +lp_coords_fmt_tile=地图块 (x/z 或 y/z) +lp_coords_invalid_text=无效坐标!
请输入 %s 到 %s 之间的数字 +#map source +lp_map_source_title=地图源 +lp_map_source_combo_tips=选择地图源 +#zoom level +lp_zoom_title=缩放比例 +lp_zoom_total_tile_count_tips=图片块总数 +lp_zoom_number_tips=选取级别 %d 的地图 +lp_zoom_total_tile_title=约 %s 个图片块 +lp_zoom_total_tile_hint_head=全部图片块: +lp_zoom_total_tile_hint_row=
等级 %d: %d (%dx%d) +#Tile parameters settings +lp_tile_param_title=图片转换 +lp_tile_param_recreate_checkbox_title=图片保存前转换 (需要CPU额外计算) +lp_tile_param_recreate_checkbox_tips=如果关闭这个选项,服务器下载的图片将直接保存(速度快) .
\ +否则,图片会在保存前转换为指定的大小和格式(速度很慢,依赖CPU). +lp_tile_param_width_title=宽度: +lp_tile_param_width_tips=图块宽度 +lp_tile_param_height_title=高度: +lp_tile_param_height_tips=图块高度 +lp_tile_param_image_fmt_title=图块格式: +lp_tile_param_image_fmt_png=PNG +lp_tile_param_image_fmt_png_8bit=PNG 256 色 (8 位) +lp_tile_param_image_fmt_png_4bit=PNG 16 色 (4 位) +lp_tile_param_image_fmt_jpg_q100=JPEG - 品质系数 100 +lp_tile_param_image_fmt_jpg_q99=JPEG - 品质系数 99 +lp_tile_param_image_fmt_jpg_q95=JPEG - 品质系数 95 +lp_tile_param_image_fmt_jpg_q90=JPEG - 品质系数 90 +lp_tile_param_image_fmt_jpg_q85=JPEG - 品质系数 85 +lp_tile_param_image_fmt_jpg_q80=JPEG - 品质系数 80 +lp_tile_param_image_fmt_jpg_q70=JPEG - 品质系数 70 +lp_tile_param_image_fmt_jpg_q60=JPEG - 品质系数 60 +lp_tile_param_image_fmt_jpg_q50=JPEG - 品质系数 50 +lp_tile_param_msg_valid_height=图片块高度只能在 %d 到 %d 之间.\n +lp_tile_param_msg_valid_width=图片块宽度只能在 %d 到 %d 之间.\n +#atlas content +lp_atlas_title=当前地图册 +lp_atlas_new_btn_title=新建地图册 +lp_atlas_add_selection_btn_title=添加选择的区域 +lp_atlas_name_label_title=新建地图名字: +lp_atlas_name_field_tips=输入一个地图名字 +# this one only support English +#lp_atlas_element_default_name=Layer +#atlas content detail - Atlas +lp_atlas_info_atlas_title=地图册
+lp_atlas_info_atlas_name=名字: %s
+lp_atlas_info_atlas_layer=地图层个数: %d
+lp_atlas_info_atlas_format=地图册格式: %s
+lp_atlas_info_max_tile=最大图片块个数: %d
+lp_atlas_info_area_start=区域开始: %s %s
+lp_atlas_info_area_end=区域结束: %s %s
+#atlas content detail - Layer +lp_atlas_info_layer_title=地图层
+lp_atlas_info_layer_map_count=地图个数: %d
+#atlas content detail - Map +lp_atlas_info_map_title=地图
+lp_atlas_info_polygon_map_title=多边形地图
+lp_atlas_info_polygon_map_point=多边形点数:
+lp_atlas_info_map_source_short=地图源: %s
+lp_atlas_info_map_source=地图源: %s (%s)
+lp_atlas_info_map_zoom_lv=缩放级别: %d
+lp_atlas_info_map_area_start=区域开始: %s (%d / %d)
+lp_atlas_info_map_area_end=区域结束: %s (%d / %d)
+lp_atlas_info_map_size=地图大小: %d x %d 像素
+lp_atlas_info_tile_size=图片块大小: %d x %d
+lp_atlas_info_tile_format=图片块格式: %s
+lp_atlas_info_tile_format_origin=图片块格式: 256x256 (无处理)
+#atlas content node pop menu +lp_atlas_pop_menu_show_detail=显示详细信息 +lp_atlas_pop_menu_display_select_area=显示下载区域 +lp_atlas_pop_menu_select_map_box=选择该地图所在区域 +lp_atlas_pop_menu_zoom_to_map_box=缩放到边界 +lp_atlas_pop_menu_zoom_to=缩放到自身 +lp_atlas_pop_menu_renmae=重命名 +lp_atlas_pop_menu_apply_tile_process=应用图片转换 +lp_atlas_pop_menu_clear_atals=清除整个地图册 +lp_atlas_pop_menu_delete_node=删除 +lp_atlas_default_tip=使用右键菜单来查看所有的节点命令. +#Profile +lp_atlas_profile_title=地图册配置 +lp_atlas_profile_combo_tips=选择一个地图册配置\n 或者输入新的名字来保存地图册配置 +lp_atlas_profile_delete_btn_title=删除配置 +lp_atlas_profile_delete_btn_tips=从配置列表中删除地图册 +lp_atlas_profile_save_btn_title=保存当前配置 +lp_atlas_profile_save_btn_tips=保存当前地图册的配置 +lp_atlas_profile_load_btn_title=载入配置 +lp_atlas_profile_load_btn_tips=载入选择的地图册配置 +lp_atlas_profile_refresh_btn_tips=刷新地图册配置列表 +lp_atlas_profile_msg_ask_name=请输入一个配置名称 +lp_atlas_profile_msg_overwrite_confirm_title=覆盖确认 +lp_atlas_profile_msg_overwrite_confirm=同名地图册配置 %s 已经存在,要覆盖它吗? +#main button +lp_mian_create_btn_title=开始下载地图册 +lp_main_create_btn_tips=开始下载地图册 +lp_main_setting_button_title=设置 +lp_main_setting_button_tips=打开偏好设置对话框 +#tile store coverage +lp_tile_store_title=本地缓存 +lp_tile_store_title_tips=显示当前地图源的已下载(缓存)区域
\ +已下载缓存可以离线查看或者作为地图源制作新地图 +lp_tile_store_show_coverage_btn_title=显示已缓存区域 +lp_tile_store_show_coverage_btn_tips=显示当前地图源在指定级别下
\ +已经下载到本地的地图数据范围,
\ +绿色:表示已经下载到本地缓存的区域,
\ +灰色:表示没有下载的区域. +lp_tile_store_hide_coverage_btn_title=取消显示 +lp_tile_store_zoom_combo_tips=选择缓存区域的缩放级别 +lp_tile_store_zoom_title=缩放级别: +lp_tile_store_layer_title=地图源图层: + +#---------- GPX Panel ------------------------- +rp_gpx_new_gpx=新建GPX +rp_gpx_load_gpx=载入GPX +rp_gpx_save_gpx=保存GPX +rp_gpx_clear_gpx=清除列表 +rp_gpx_add_wpt=添加路点 +rp_gpx_default_node_name=已载入GPX文件... +rp_gpx_pop_menu_delete_element=删除节点 +rp_gpx_pop_menu_rename_element=重命名节点 +rp_gpx_menu_rename=重命名 +rp_gpx_menu_delete=删除 +rp_gpx_msg_confim_delete_title=删除节点 +rp_gpx_msg_confim_delete=您确认要删除这个节点吗? +rp_gpx_rename_element_title=请输入名称: +rp_gpx_msg_can_not_rename_track=Track段的名称不支持修改。 +rp_gpx_msg_ask_create_new=尚未选择GPX任何文件,希望新建一个GPX文件吗? +rp_gpx_msg_ask_create_new_title=添加路点到GPX文件? +rp_gpx_msg_add_point_failed=路点只能加入到GPX文件根节点、route段以及Track段下。 +rp_gpx_root_default_name_nofile=未命名 (新建GPX) +rp_gpx_root_default_name_hasfile=未命名 (文件 %s) +rp_gpx_unname_route_name=未命名 route +rp_gpx_unname_track_name=未命名 track +rp_gpx_unname_wpt_name=未命名 waypoint +rp_gpx_msg_confirm_reopen_file=GPX文件已经被打开,您希望再次打开这些文件吗? +rp_gpx_msg_error_save_gpx_file=保存GPX文件出错 +rp_gpx_msg_no_select_file=很抱歉,您尚未选择GPX文件 +rp_gpx_node_seg_name=线路段 %s + +#---------- Main Menu ------------------------- +#Atlas +menu_atlas=地图册 +menu_atlas_new=新建地图册 +menu_atlas_convert_format=转换地图册类型 +menu_atlas_create=开始下载地图册 +#Maps +menu_maps=地图 +menu_maps_selection=选择区域按 +menu_maps_selection_rect=矩形 +menu_maps_selection_polygon=多边形 +menu_maps_selection_circle=圆 +menu_maps_selection_add=添加选择的区域 +menu_maps_selection_add_by_gpx=添加GPX沿线区域 +#Bookmark +menu_bookmark=书签 +menu_bookmark_save=保存当前显示区域 +menu_bookmark_manage=管理书签 +#Panels +menu_panels=面板 +menu_show_hide_left_panel=显示/隐藏 左侧面板 +menu_show_hide_gpx_panel=显示/隐藏 GPX面板 +#Debug +menu_debug=高级 +menu_debug_show_hide_tile_border=显示/隐藏 图片边框 +menu_debug_show_all_map_source=显示所有地图源 +menu_debug_refresh_map_source=刷新自定义地图源 +menu_debug_show_log_file=查看日志文件 +menu_debug_log_level=日志级别 +menu_debug_system_report=查看系统信息 +#help +menu_help=帮助 +menu_help_readme=帮助文档 +menu_help_how_to_preview=如何查看地图 +menu_help_licenses=版权声明与许可证 +menu_help_about=关于 +#tools +menu_tool=工具 + +#---------- Dialog ------------------------- +#About +dlg_about_title=关于 +dlg_about_version=版本: +dlg_about_program_version=开发版本: +dlg_about_download_mapplus=下载地图加加 +dlg_about_download_mapplus_url=https://itunes.apple.com/cn/app/id438868200?mt=8 +#Font choose +dlg_font_choose_title=选择字体 +dlg_font_choose_preview=预览 +dlg_font_choose_name=名称 +dlg_font_choose_style=风格 +dlg_font_choose_size=大小 +#Licenses +dlg_license_title=版权声明与许可证 +#Help +dlg_help_title=帮助 +#Manager Bookmark +dlg_mgn_bookmark_title=管理书签 +dlg_mgn_bookmark_delete=删除书签 +#Bookmark Add +dlg_add_bookmark_msg=请为新建书签设置一个名字 +#New Atlas +dlg_new_atlas_title=新建地图册 +dlg_new_atlas_name_title=新建地图册名字: +dlg_new_atlas_default_atlas_name=我的地图册 +dlg_new_atlas_select_format_title=请选择地图册类型 +#Working Progress +dlg_progress_title=完成 +dlg_progress_about_btn=关于 +dlg_progress_count=已完成: 0 +dlg_progress_count_i=已完成: %d +#Select GPX Track +dlg_gpx_track_select_title=设置区域宽度 +dlg_gpx_track_select_distance=设置边沿到线路中心线的距离: %d %s +#Add GPS point +dlg_gpx_inpu_point_name=请为该点输入一个名字: +#Show All Map Source +dlg_show_source_title=全部地图源 +dlg_show_source_column_name=名称 +dlg_show_source_column_display_text=全称 +dlg_show_source_column_rev=内部版本号 +dlg_show_source_column_type=类型 +#Select Dir +dlg_select_dir_title=选择目录 +dlg_select_dir_description=目录 +#Atlas download progress +dlg_download_title=正在创建地图册 +dlg_download_zoom_level_progress=正在下载缩放等级: +dlg_download_map_progress=正在下载地图: +dlg_download_atlas_progress=%d%% 已完成 - 当前地图册: %s, 类型: %s +dlg_download_done_percent=%d%% 已完成 +dlg_download_done_tenthpercent=%.1f%% 已完成 +dlg_download_window_title=

正在创建地图册...

+dlg_download_map_info_label_default=正在下载地图 ABCDEFGHIJKLMNOPQRSTUVWXYZ-nn, 图层 ABCDEFGHIJKLMNOPQRSTUVWXYZ, 地图源 ABCDEFGHIJKLMNOPQRSTUVWXYZ +dlg_download_map_info_label=正在下载地图 %s 的图层 %s , 从地图源: %s +dlg_download_map_done_count_default=已完成000, 共 000 +dlg_download_map_done_count=已完成%d, 共 %d +dlg_download_remain_time_default=剩余时间: 00000 分 00 秒 +dlg_download_remain_time=剩余时间: %s +dlg_download_tile_done_count_default=已下载图片 1000000, 共 1000000 块 +dlg_download_tile_done_count=已下载图片 %d 块,共 %d 块 +dlg_download_map_create_title=创建地图文件 +dlg_download_avg_speed=平均下载速度 +dlg_download_avg_speed_value=: %s / 秒 +dlg_download_total_bytes=从地图源下载 +dlg_download_bytes_from_cache=从本地缓存读取 +dlg_download_thread_count=当前使用线程数 +dlg_download_retry_count=临时出错图片 +dlg_download_retry_count_value=: 当前地图: %d 次, 总共: %d 次 +dlg_download_failed_count=无法下载图片 +dlg_download_failed_count_value=: 当前地图: %d 次, 总共: %d 次 +dlg_download_error_tips=

地图下载的出错次数(临时出错/无法下载)。

\ +

下载某个图片失败时,系统会重试2次,如果第二次重试时
\ +仍然无法下载. 则会标记为出错块, 并且在本次地图
\ +下载过程中不会对其进行再次下载

+dlg_download_total_time=下载时间 +dlg_download_checkbox_ignore_error=忽略下载错误并自动继续 +dlg_download_status_title=状态: +dlg_download_status_running=运行中 +dlg_download_status_aborted=被终止 +dlg_download_status_finished=成功完成 +dlg_download_status_paused=暂停 +dlg_download_btn_abort=终止下载 +dlg_download_btn_abort_tips=终止地图册的下载 +dlg_download_btn_close_win=关闭窗口 +dlg_download_btn_close_win_tips_disable=地图册正在下载中... +dlg_download_btn_close_win_tips_enable=关闭地图册下载窗口 +dlg_download_btn_open_folder=打开下载文件夹 +dlg_download_btn_open_folder_tips_disabled=地图册正在下载中... +dlg_download_btn_open_folder_tips_enabled=打开地图册下载保存的文件夹 +dlg_download_btn_pause_resume=暂停/恢复 +dlg_download_time_unknown=未知 +dlg_download_abort_title=地图册下载已终止 +dlg_download_abort_window_title=

地图册已经被用户终止下载

+dlg_download_succeed_title=地图册下载已成功 +dlg_download_succeed_window_title=

地图册以成功完成

+dlg_download_errors_todo=下载地图图片出错多个错误 - 您希望如何处理? +dlg_download_errors_todo_msg=多个地图图片下载失败. \ +您当前连接的地图服务器或者选择的下载区域可能存在问题. \ +
您希望:

\ +继续 下载,并忽略遇到的图片错误? (将导致部分图块空白/丢失)
\ +重试 从头开始重新下载一遍该本地图?
\ +跳过 当前地图所有图片,进行下一个地图的下载?
\ +终止 当前地图和整个地图册的所有下载?
+dlg_download_errors_missing_tile=错误:部分地图图片在下载过程丢失,仍然继续吗? +dlg_download_errors_missing_tile_msg=下载地图册存在问题.\n\ +成功下载的地图图片数量少于期望下载的图片数量.\n\ +即地图册下载过程中存在图片的丢失.\n共有 %d 张图片未成功下载.\n\n\ +你仍然希望继续整个地图册的创建吗? +dlg_download_show_error_report=查看错误报告 +dlg_download_erro_head=运行出错: +#Splash Frame +splash_title=正在启动 Mobile Atlas Creator 中文版... + +#---------- Setting ------------------------- +set_title=设置 +#Display +set_display_title=显示 +set_display_unit_system_title=单位 +set_display_unit_system_metric=公制单位 +set_display_unit_system_imperial=英制单位 +set_display_unit_system_scale_bar=比例尺单位: +set_display_language=界面语言 +set_display_language_choose=选择界面语言(需重启): +set_display_language_choose_tips=选择一种语言,需要重启MOBAC才能生效 +set_display_language_restart_desc=您需要重新启动MOBAC才能使得新语言生效. +set_display_language_msg_title=需要重启 +set_display_language_msg=语言修改需要重新启动MOBAC才能生效,现在就退出MOBAC吗? +set_display_grid=经纬度网格线 +set_display_grid_compress=简化坐标 +set_display_grid_compress_tips=选中后,坐标文本将使用简写. +set_display_grid_title=选择颜色 +set_display_grid_width=线宽: +set_display_grid_width_tips=设置绘制宽度 +set_display_grid_color=颜色: +set_display_grid_color_tips=设置网格线和文本的颜色 +set_display_grid_font=字体: +set_display_grid_font_tips=设置文本坐标的字体 +#Map sources config +set_mapsrc_config_title=地图源设置 +set_mapsrc_config_online_update=在线更新地图源 +set_mapsrc_config_online_update_btn=开始在线更新 +set_mapsrc_config_online_update_no_update=无可用更新 +set_mapsrc_config_online_update_done=更新成功 +set_mapsrc_config_online_update_failed=更新失败 +set_mapsrc_config_online_update_msg_outdate=当前版本的 MOBAC 不支持在线更新,\ +请升级到最新版本后再试。 +set_mapsrc_config_online_update_msg_noneed=您已经拥有最新版本的地图包,无需更新。 +set_mapsrc_config_online_update_msg_done=%d 个地图源被更新.\n\ +请重新启动 MOBAC 来安装这些更新。 +set_mapsrc_config_osmhiking=Reit-und Wanderkarte ($Abo) 地图设置 +set_mapsrc_config_osmhiking_purchased=已支付的 ticket ID: +set_mapsrc_config_osmhiking_howto=如果获取一个 ticket (德语) +set_mapsrc_config_osmhiking_howto_url=http://www.wanderreitkarte.de/shop_abo_de.php +#Map sources +set_mapsrc_mgr_title=管理地图源 +set_mapsrc_mgr_title_enabled=启用的地图源 +set_mapsrc_mgr_title_disabled=禁用的地图源 +set_mapsrc_mgr_move_up_tips=上移选中的地图源 +set_mapsrc_mgr_move_down_tips=下移选中的地图源 +set_mapsrc_mgr_move_left_tips=启用选中的地图源 +set_mapsrc_mgr_move_right_tips=禁用选中的地图源 +#Tile Update +set_tile_update_title=图片块更新 +set_tile_update_desc=\ +本地缓存的图片块会按此规则在必要时重新下载(更新)
\ +每个图片块会首先按地图服务器预设的有效时间控制更新,
\ +当地图源服务器没有提供相关设置,则按照下面设置更新 +set_tile_update_default_expiration=默认有效时间 +set_tile_update_default_expiration_desc=只有当地图源未提供有效时间时,
\ +才会使用默认有效时间进行管理. +set_tile_update_max_expiration=最大有效时间 +set_tile_update_min_expiration=最小有效时间 +#Tile Store +set_tile_store_title=本地缓存库 +set_tile_store_enable_checkbox=允许使用本地缓存来预览和下载地图 +set_tile_store_settings=本地缓存设置 +set_tile_store_information=信息 +set_tile_store_info_mapsrc=地图源 +set_tile_store_info_tiles=已缓存图片 +set_tile_store_info_size=已缓存大小 +set_tile_store_info_disabled_subfix= (禁用) +set_tile_store_info_delete_tips=删除所有 %s 的本地缓存图片. +set_tile_store_info_deleteing_tips=正在删除 - 请稍等... +set_tile_store_info_total=缓存大小合计 +#Map Size +set_map_size_title=地图大小 +set_map_size_settings=地图大小设置 +set_map_size_max_size_of_rect=矩形选区对应的地图最大宽度或高度: +set_map_size_overlap_tiles=地图区域的图片块叠加个数: +set_map_size_desc=\ +如果矩形选区的长或者宽超出设置的最大大小,
\ +MOBAC会在您添加区域到地图册的时候,
\ +自动拆分区域为多个子地图,以保证每个子地图的大小
\ +都会小于设置的最大地图大小.
\ +注意:该设置对多边形选区无效,对于已经添加的选区也无效
\ +您可以在地图册的树型结构中查看当前地图的大小.
\ +
\ +使用TrekBuddy用户请注意:
\ +TrekBuddy 在 v0.9.88 版本之前不支持地图大于 32767.
\ +之后版本可以支持大小在 1048575 以内的地图. +#Directory +set_directory_title=目录 +set_directory_output=地图册导出目录 +set_directory_output_tips=如果设置为空,则使用默认目录:
%s +set_directory_output_select=重新选择 +set_directory_output_select_dlg_title=选择目录 +#Network +set_net_title=网络 +set_net_connection=网络连接 +set_net_connection_desc=同时运行的下载连接数 +set_net_bandwidth_desc=下载带宽限速 +set_net_bandwidth_unlimited=无限制 +set_net_proxy=HTTP 代理 +set_net_proxy_settings=代理设置: +set_net_proxy_settings_java=使用Java代理设置 +set_net_proxy_settings_application=使用应用的代理设置 +set_net_proxy_settings_custom=用户自定义 +set_net_proxy_settings_custom_auth=用户自定义(带认证) +set_net_proxy_host=代理服务器: +set_net_proxy_port=端口: +set_net_proxy_username=用户名: +set_net_proxy_password=密码: +set_net_default=默认 +set_net_default_ignore_error=下载时忽略错误并继续下载 +#Paper Atlas +set_paper_title=页面设置 +#Paper Atlas - Size +set_paper_size=大小 +set_paper_size_default=默认 +set_paper_size_default_tips=选择该选项, 则页面大小由指定的格式决定. +set_paper_size_default_combo_tips=选择一个默认的页面格式 +set_paper_size_default_landscape=横向排版 +set_paper_size_default_landscape_tips=选择该选项,页面方向为横向。 +set_paper_size_custom=自定义 +set_paper_size_custom_tips=选择该选项,用户可以手工指定页面大小 +set_paper_size_custom_width=宽度: +set_paper_size_custom_width_tips=指定页面宽度. +set_paper_size_custom_height=高度: +set_paper_size_custom_height_tips=指定页面高度 +set_paper_size_selection=选择区域 +set_paper_size_selection_tips=选择该选项,页面按地图的大小区域决定 +#Paper Atlas - Additions +set_paper_additions=附加 +set_paper_wgs_grid=经纬度网格线 +set_paper_wgs_grid_tips=如果选择,将会打印经纬度格线(WGS-64) +set_paper_wgs_grid_density_tips=选择经纬度网格线密度 +set_paper_paper_nubmer=页面编号 +set_paper_paper_nubmer_tips=选择该选项,页码编号将会打印在每一页 +set_paper_scale_bar=比例尺 +set_paper_scale_tips=选择该选项,比例尺将会打印在地图上 +set_paper_compass=指南针 +set_paper_compass_tips=选择该选项,指南针将会打印在地图上 +#Paper Atlas - Margins +set_paper_margins=缩进 +set_paper_margins_tips=指定缩进的尺寸 +set_paper_margins_top=上: +set_paper_margins_left=左: +set_paper_margins_bottom=下: +set_paper_margins_right=右: +#Paper Atlas - Advanced +set_paper_advanced=高级 +set_paper_advanced_compression=压缩: +set_paper_advanced_compression_tips=指定PDF文档的压缩级别.
\ +0 表示不压缩, 9 表示最大压缩.
\ +数字越大,压缩比例越高,默认为6. +set_paper_advanced_dpi=分辨率 +set_paper_advanced_dpi_tips=指定图片的分辨率 (DPI) .
\ +一般地,更高的值意味着更好的视觉效果.
\ +默认值为 96, 可以提升到120以获得更好的效果,
\ +但请注意:您的PDF阅读器也须以相当的分辨率来显示
\ +才能获得最佳效果 + +set_paper_advanced_overlap=重叠: +set_paper_advanced_overlap_tips=指定相邻两个页面的重叠区大小. +set_paper_advanced_crop=裁剪: +set_paper_advanced_crop_tips=指定裁剪的百分比.
\ +超过这个比例位置的页面边界将会被裁剪掉 + +#Paper Atlas - Actions +set_paper_actions=操作 +set_paper_actions_import_xml=从XML导入 +set_paper_actions_import_xml_tip=从XML导入所有页面设置 +set_paper_actions_export_xml=导出到XML +set_paper_actions_export_xml_tip=导出所有页面设置到XML文件 +set_paper_actions_restore_default=恢复到默认值 +set_paper_actions_restore_default_tips=重置全部页面设置为默认值
\ +所有当前的设置的参数将会被覆盖. +set_paper_actions_error_import=无法导入页面设置文件: +set_paper_actions_error_export=无法导出到页面设置文件: +set_paper_actions_error_reason=原因: +set_paper_actions_error_title=发生错误! +set_paper_actions_xml_filter=XML 文件 (*.xml) +#Error +set_error_saving_title=保存设置出错 +set_error_saving_msg=无法保存设置到文件:\n ( %s ) + +#---------- Message ------------------------- +#common +msg_no_zoom_level_selected=请至少选择一个缩放级别 +msg_no_select_area=请选择一个区域 +#Add GPX Track Polygon +msg_add_gpx_polygon_too_many_track=当前选择文件包含不止一条线路\n请选择其中一条线路(track)或线路段(segment)。 +msg_add_gpx_polygon_too_many_segment=当前选择的线路包含不止一条线路段(segment)\n请选择其中一条线路段(segment)。 +msg_add_gpx_polygon_no_select=没有选择GPX线路(track)或线路段(segment) +msg_add_gpx_polygon_maxsize_title=超出最大地图尺寸 +msg_add_gpx_polygon_maxsize=至少一个地图超出了最大尺寸,\n\ +但本程序暂不支持多边形区域的自动拆分,\n您仍然希望继续吗? +#Convert Atlas +msg_convert_atlas_format=转换地图册格式 +msg_convert_incompatible_format=地图和地图册的格式不兼容 +#Log file +msg_no_log_file_config=没有配置的日志文件 +msg_no_log_file=日志文件不存在:

%s +#Setting +msg_settings_write_error=写入设置信息到"settings.xml"时出错 +#Refresh +msg_refresh_all_map_source_done=%d 个自定义地图源被成功刷新 +#Help +msg_no_found_readme_file=不能找到文件:README.HTM +#Merge Layer +msg_confirm_merge_layer_title=确认合并地图层 +msg_confirm_merge_layer=您确定要将地图层 %s 合并到 %s 吗? +msg_merge_layer_failed=图层合并失败 +#Tile Size select +msg_invalid_tile_size=无效的图片块大小!
请输入 %d 到 %d 之间的数 +#Tile Store Coverage +msg_tile_store_failed_retrieve_coverage=无法显示本地缓存的覆盖范围.\n\ +可能是因为选择的区域过大或者缩放级别过大. +#Atlas download thread +msg_out_of_memory_title=内存不足. +msg_out_of_memory_head=Mobile Atlas Creator没有足够内存继续运行. +msg_out_of_memory_detail=\n当前分配给MOBAC的最大内存数为 %d MB +msg_atlas_download_abort=地图册下载已经被终止 +msg_too_many_tiles_title=无法进行地图册下载 +msg_too_many_tiles_msg=Mobile Atlas Creator 检测到您即将\n\ +下载的地图册拥有远远超出常规数目的图片块数量,\n\ +请减少在高缩放级别时选择的地图区域大小,并重新下载.\n\ +最大允许的地图图片块数为: %d\n\ +当前地图册中的图片块数为:%d +msg_atlas_version_mismatch=当前打开/加载的地图册是MOBAC早期版本创建的,\n\ +该版本与当前版本的存储格式无法兼容。\n\n\ +建议您清除掉整个地图册,并删除受影响的地图册保存文件,\n\ +否则在使用相关地图册时,可能出现更多异常情况.\n +msg_atlas_data_check_failed=载入保存的地图册时出现错误。\n\ +这可能是因为文件被修改或者格式不兼容造成的,\n\n\ +建议您清除掉整个地图册,并删除受影响的地图册保存文件,\n\ +否则在使用相关地图册时,可能出现更多异常情况.\n +#Settings +msg_setting_file_is_changed_by_other_title=覆盖已修改内容? +msg_setting_file_is_changed_by_other=参数文件 settings.xml 已经被其它程序修改.\n\ +您希望覆盖其它程序的修改吗?\n\ +当覆盖后,其它程序所作的所有修改都会被清除! +msg_settings_file_can_not_parse=无法读取或解析设置文件,程序将会退出。 +#Others +msg_atlas_is_empty=地图册为空,请首先为地图册添加一个区域 +msg_environment_not_support_title=不支持平台 +msg_environment_not_support=不支持您的运行环境.
桌面平台: %s
操作系统: %s +msg_environment_failed_open_output_title=打开地图册文件夹失败 +msg_environment_failed_open_output=打开地图册输出文件夹失败.
+msg_environment_use_commond=
使用命令: %s
+msg_environment_commond_param=参数: %d : %s +msg_environment_lack_memory_title=警告: 内存设置过小 +msg_environment_lack_memory=\ +警告: Mobile Atlas Creator 在运行时允许使用的内存过小.
\ +当前Mobile Atlas Creator 最大可用内存仅为: %s.

\ +请确认您使用了正确启动程序或脚本来运行MOBAC:
\ +对于Window系统,请使用Mobile Atlas Creator.exe
\ +对于Linux/Unix/OSX系统,请使用 start.sh 脚本,
\ +脚本参数:-Xmx 512M 可指定512M最大可用内存.

\ +例如: java -Xmx512M -jar Mobile_Atlas_Creator.jar
\ +
Mobile Atlas Creator将在您确定后继续运行
+msg_environment_error_create_dir=创建文件夹 %s 出错,文件夹路径 %s\n 程序将退出. +msg_environment_error_write_file=无法写入到文件:\n %s \n路径:%s \n 请修改文件权限并重启MOBAC. +msg_environment_error_create_setting=无法写入到设置文件settings.xml, 程序将退出. +msg_environment_error_init_mapsrc_dir=无法初始化地图源文件夹:\n +msg_environment_error_load_cloudmade=无法载入自定义的CloudMade 地图源. 请确保openstreetmap地图源包完好可用. +msg_environment_mapsrc_dir_not_exist=地图源文件夹不存在 - 位置:\n %s \n\ +请确保您在解压缩MOBAC的zip包时,\n\ +包含了所有的子文件夹. +msg_environment_unable_to_start=无法启动Mobile Atlas Creator: \n +msg_environment_jre_bellow=您电脑上安装的Java环境(Java Runtime Environment)版本过低.\n\ +Mobile Atlas Creator 需要至少Java 6 (1.6) 或更高版本.\n\ +请升级您的Java运行时环境,再重启Mobile Atlas Creator.\n\n\ +当前检测到的 Java 版本为: %@ +msg_environment_jre_bellow_title=Java运行环境不兼容 +msg_environment_invalid_source_folder=指定的地图源文件夹不存在:\n地图名: %s\ +\n地图源目录: %s +msg_environment_invalid_source_folder_zoom=无法找到指定缩放等级的目录:\n地图名: %s\ +\n地图源目录: %s +msg_environment_invalid_source_folder_title=无效地图源目录 +msg_tools_exec_command_ask=要执行的命令行:
%s +msg_tools_exec_command_ask_title=您希望执行该命令吗? +msg_custom_map_invalid_source_file_title=无效地图源文件 +msg_custom_map_invalid_source_file=指定数据库文件不存在.\n地图名: %s \n文件名: %s +msg_custom_map_invalid_source_zip_title=无效的zip源 +msg_custom_map_invalid_source_zip=指定的zip地图源文件不存在:\n地图名: %s \nZip 文件: %s +msg_custom_map_failed_open_source_zip_title=读取zip文件失败 +msg_custom_map_failed_open_source_zip=无法读取指定的地图源zip文件:\n地图名: %s \nZip 文件: %s +msg_custom_map_invalid_source_sqlitedb=指定的 SQLite 数据库文件不存在:\n地图名: %s \n文件名: %s +msg_custom_map_source_failed_load_sqlitedb_title=加载数据库文件出错 +msg_custom_map_source_failed_load_sqlitedb=指定的数据库文件无法被加载:\n地图名: %s\ +\n文件名: %s \n错误信息:%s +msg_update_map_pack_error_title=加载地图包出错 +msg_update_map_pack_error=在加载升级的地图包时出现错误,请重新启动 MOBAC 来恢复到之前的版本。 +msg_error_load_atlas_profile_title=加载地图册出错 - 继续吗? +msg_error_load_atlas_profile=加载地图册出错:
 %s 
\ +
文件: %s 行/列: %d / %d
\ +您希望继续加载该地图册吗?
\ +这可能导致地图册不完整或者无法正常工作.
+msg_environment_slqite_lib_missing_title="出错 - SQLite 库不可用" +msg_environment_slqite_lib_missing=无法找到SQLite库文件. \ +您当前的地图册格式必须Sqlite库才能运行.
请查阅帮助文件 README.HTM \ +的"SQLite atlas formats"章节获得更多帮助. " +msg_environment_image_format_not_available_title=图片格式不支持 - 缺少图片库 +msg_environment_image_format_not_available=该文件格式需要安装额外的Java图片处理库:
\ +Java Advanced Image library (jai_core.jar & jai_codec.jar)
\ +更多信息请查看帮助文件 README.HTM \ +中的 Requirements章节. +msg_tile_store_access_conflict_title=运行冲突 +msg_tile_store_access_conflict=\ +多个 Mobile Atlas Creator 正在试图同时访问\n\ +相同的本地地图缓存,本地地图缓存只能同时有一个MOBAC\n\ +实例访问.请关闭其它MOBAC程序实例后再试.\n \ No newline at end of file diff --git a/src/main/resources/mobac/resources/text/localize_zh_TW.properties b/src/main/resources/mobac/resources/text/localize_zh_TW.properties new file mode 100644 index 0000000..dbed8bc --- /dev/null +++ b/src/main/resources/mobac/resources/text/localize_zh_TW.properties @@ -0,0 +1,662 @@ +#---------- Version ------------------------- +mobac_version_subfix=多語言版 +mobac_version_subfix_dev= +#---------- Basic ------------------------- +OK=確定 +Cancel=取消 +Close=關閉 +Error=出錯 +Unnamed=未命名 +Warning=警告 +minute=分 +minutes=分 +second=秒 +seconds=秒 +Information=消息 +Continue=繼續 +Abort=終止 +Skip=跳過 +Retry=重試 +Undefined=未定義 +Exit=退出 +Bytes=Bytes +KiByte=KB +MiByte=MB +GiByte=GB + +#---------- On map controls ------------------------- +map_ctrl_zoom_level_title=縮放等級 +map_ctrl_zoom_level_title_tips=當前縮放等級 +map_ctrl_zoom_grid_tips=在地圖上顯示指定縮放等級的圖片塊網格 +map_ctrl_zoom_grid_prefix_fmt=%d 級網格 +map_ctrl_zoom_grid_disable=不顯示網格 +map_ctrl_wgs_grid_title=顯示經緯度網格 +map_ctrl_wgs_grid_tips=在地圖上顯示經緯度網格線(不會保存到地圖冊) +map_ctrl_wgs_grid_density_tips=設置經緯度網格的顯示密度 +map_ctrl_wgs_grid_density_prefix=每 +map_ctrl_wgs_grid_density_degree=度 +map_ctrl_wgs_grid_density_degrees=度 +map_ctrl_wgs_grid_density_minute=分 +map_ctrl_wgs_grid_density_minutes=分 +map_ctrl_wgs_grid_density_seconds=秒 +map_ctrl_wgs_grid_density_second=秒 +map_loading_wait=正在載入地圖數據,請稍等... + +#---------- Left Panel ------------------------- +#coordinate +lp_coords_title=坐標信息 +lp_coords_select_btn_title=選擇該區域 +lp_coords_label_N=北 +lp_coords_label_E=東 +lp_coords_label_W=西 +lp_coords_label_S=南 +lp_coords_fmt_list_title=單位 +lp_coords_fmt_degree_eng=度(英語) +lp_coords_fmt_degree_local=度(本地) +lp_coords_fmt_degree_min_eng=度 分(英文) +lp_coords_fmt_degree_min_local=度 分 (本地) +lp_coords_fmt_degree_min_sec_eng=度 分 秒 (英語) +lp_coords_fmt_degree_min_sec_local=度 分 秒 (本地) +lp_coords_fmt_tile=地圖塊 (x/z 或 y/z) +lp_coords_invalid_text=無效坐標!
請輸入 %s 到 %s 之間的數字 +#map source +lp_map_source_title=地圖源 +lp_map_source_combo_tips=選擇地圖源 +#zoom level +lp_zoom_title=縮放比例 +lp_zoom_total_tile_count_tips=圖片塊總數 +lp_zoom_number_tips=選取級別 %d 的地圖 +lp_zoom_total_tile_title=約 %s 個圖片塊 +lp_zoom_total_tile_hint_head=全部圖片塊: +lp_zoom_total_tile_hint_row=
等級 %d: %d (%dx%d) +#Tile parameters settings +lp_tile_param_title=圖片轉換 +lp_tile_param_recreate_checkbox_title=圖片保存前轉換 (需要CPU額外計算) +lp_tile_param_recreate_checkbox_tips=如果關閉這個選項,服務器下載的圖片將直接保存(速度快) .
\ +否則,圖片會在保存前轉換為指定的大小和格式(速度很慢,依賴CPU). +lp_tile_param_width_title=寬度: +lp_tile_param_width_tips=圖塊寬度 +lp_tile_param_height_title=高度: +lp_tile_param_height_tips=圖塊高度 +lp_tile_param_image_fmt_title=圖塊格式: +lp_tile_param_image_fmt_png=PNG +lp_tile_param_image_fmt_png_8bit=PNG 256 色 (8 位) +lp_tile_param_image_fmt_png_4bit=PNG 16 色 (4 位) +lp_tile_param_image_fmt_jpg_q100=JPEG - 品質系數 100 +lp_tile_param_image_fmt_jpg_q99=JPEG - 品質系數 99 +lp_tile_param_image_fmt_jpg_q95=JPEG - 品質系數 95 +lp_tile_param_image_fmt_jpg_q90=JPEG - 品質系數 90 +lp_tile_param_image_fmt_jpg_q85=JPEG - 品質系數 85 +lp_tile_param_image_fmt_jpg_q80=JPEG - 品質系數 80 +lp_tile_param_image_fmt_jpg_q70=JPEG - 品質系數 70 +lp_tile_param_image_fmt_jpg_q60=JPEG - 品質系數 60 +lp_tile_param_image_fmt_jpg_q50=JPEG - 品質系數 50 +lp_tile_param_msg_valid_height=圖片塊高度只能在 %d 到 %d 之間.\n +lp_tile_param_msg_valid_width=圖片塊寬度只能在 %d 到 %d 之間.\n +#atlas content +lp_atlas_title=當前地圖冊 +lp_atlas_new_btn_title=新建地圖冊 +lp_atlas_add_selection_btn_title=添加選擇的區域 +lp_atlas_name_label_title=新建地圖名字: +lp_atlas_name_field_tips=輸入一個地圖名字 +# this one only support English +#lp_atlas_element_default_name=Layer +#atlas content detail - Atlas +lp_atlas_info_atlas_title=地圖冊
+lp_atlas_info_atlas_name=名字: %s
+lp_atlas_info_atlas_layer=地圖層個數: %d
+lp_atlas_info_atlas_format=地圖冊格式: %s
+lp_atlas_info_max_tile=最大圖片塊個數: %d
+lp_atlas_info_area_start=區域開始: %s %s
+lp_atlas_info_area_end=區域結束: %s %s
+#atlas content detail - Layer +lp_atlas_info_layer_title=地圖層
+lp_atlas_info_layer_map_count=地圖個數: %d
+#atlas content detail - Map +lp_atlas_info_map_title=地圖
+lp_atlas_info_polygon_map_title=多邊形地圖
+lp_atlas_info_polygon_map_point=多邊形點數:
+lp_atlas_info_map_source_short=地圖源: %s
+lp_atlas_info_map_source=地圖源: %s (%s)
+lp_atlas_info_map_zoom_lv=縮放級別: %d
+lp_atlas_info_map_area_start=區域開始: %s (%d / %d)
+lp_atlas_info_map_area_end=區域結束: %s (%d / %d)
+lp_atlas_info_map_size=地圖大小: %d x %d 像素
+lp_atlas_info_tile_size=圖片塊大小: %d x %d
+lp_atlas_info_tile_format=圖片塊格式: %s
+lp_atlas_info_tile_format_origin=圖片塊格式: 256x256 (無處理)
+#atlas content node pop menu +lp_atlas_pop_menu_show_detail=顯示詳細信息 +lp_atlas_pop_menu_display_select_area=顯示下載區域 +lp_atlas_pop_menu_select_map_box=選擇該地圖所在區域 +lp_atlas_pop_menu_zoom_to_map_box=縮放到邊界 +lp_atlas_pop_menu_zoom_to=縮放到自身 +lp_atlas_pop_menu_renmae=重命名 +lp_atlas_pop_menu_apply_tile_process=應用圖片轉換 +lp_atlas_pop_menu_clear_atals=清除整個地圖冊 +lp_atlas_pop_menu_delete_node=刪除 +lp_atlas_default_tip=使用右鍵菜單來查看所有的節點命令. +#Profile +lp_atlas_profile_title=地圖冊配置 +lp_atlas_profile_combo_tips=選擇一個地圖冊配置\n 或者輸入新的名字來保存地圖冊配置 +lp_atlas_profile_delete_btn_title=刪除配置 +lp_atlas_profile_delete_btn_tips=從配置列表中刪除地圖冊 +lp_atlas_profile_save_btn_title=保存當前配置 +lp_atlas_profile_save_btn_tips=保存當前地圖冊的配置 +lp_atlas_profile_load_btn_title=載入配置 +lp_atlas_profile_load_btn_tips=載入選擇的地圖冊配置 +lp_atlas_profile_refresh_btn_tips=刷新地圖冊配置列表 +lp_atlas_profile_msg_ask_name=請輸入一個配置名稱 +lp_atlas_profile_msg_overwrite_confirm_title=覆蓋確認 +lp_atlas_profile_msg_overwrite_confirm=同名地圖冊配置 %s 已經存在,要覆蓋它嗎? +#main button +lp_mian_create_btn_title=開始下載地圖冊 +lp_main_create_btn_tips=開始下載地圖冊 +lp_main_setting_button_title=設置 +lp_main_setting_button_tips=打開偏好設置對話框 +#tile store coverage +lp_tile_store_title=本地緩存 +lp_tile_store_title_tips=顯示當前地圖源的已下載(緩存)區域
\ +已下載緩存可以離線查看或者作為地圖源制作新地圖 +lp_tile_store_show_coverage_btn_title=顯示已緩存區域 +lp_tile_store_show_coverage_btn_tips=顯示當前地圖源在指定級別下
\ +已經下載到本地的地圖數據範圍,
\ +綠色:表示已經下載到本地緩存的區域,
\ +灰色:表示沒有下載的區域. +lp_tile_store_hide_coverage_btn_title=取消顯示 +lp_tile_store_zoom_combo_tips=選擇緩存區域的縮放級別 +lp_tile_store_zoom_title=縮放級別: +lp_tile_store_layer_title=地圖源圖層: + +#---------- GPX Panel ------------------------- +rp_gpx_new_gpx=新建GPX +rp_gpx_load_gpx=載入GPX +rp_gpx_save_gpx=保存GPX +rp_gpx_clear_gpx=清除列表 +rp_gpx_add_wpt=添加路點 +rp_gpx_default_node_name=已載入GPX檔案... +rp_gpx_pop_menu_delete_element=刪除節點 +rp_gpx_pop_menu_rename_element=重命名節點 +rp_gpx_menu_rename=重命名 +rp_gpx_menu_delete=刪除 +rp_gpx_msg_confim_delete_title=刪除節點 +rp_gpx_msg_confim_delete=您確認要刪除這個節點嗎? +rp_gpx_rename_element_title=請輸入名稱: +rp_gpx_msg_can_not_rename_track=Track段的名稱不支持修改。 +rp_gpx_msg_ask_create_new=尚未選擇GPX任何檔案,希望新建一個GPX檔案嗎? +rp_gpx_msg_ask_create_new_title=添加路點到GPX檔案? +rp_gpx_msg_add_point_failed=路點只能加入到GPX檔案根節點、route段以及Track段下。 +rp_gpx_root_default_name_nofile=未命名 (新建GPX) +rp_gpx_root_default_name_hasfile=未命名 (檔案 %s) +rp_gpx_unname_route_name=未命名 route +rp_gpx_unname_track_name=未命名 track +rp_gpx_unname_wpt_name=未命名 waypoint +rp_gpx_msg_confirm_reopen_file=GPX檔案已經被打開,您希望再次打開這些檔案嗎? +rp_gpx_msg_error_save_gpx_file=保存GPX檔案出錯 +rp_gpx_msg_no_select_file=很抱歉,您尚未選擇GPX檔案 +rp_gpx_node_seg_name=線路段 %s + +#---------- Main Menu ------------------------- +#Atlas +menu_atlas=地圖冊 +menu_atlas_new=新建地圖冊 +menu_atlas_convert_format=轉換地圖冊類型 +menu_atlas_create=開始下載地圖冊 +#Maps +menu_maps=地圖 +menu_maps_selection=選擇區域按 +menu_maps_selection_rect=矩形 +menu_maps_selection_polygon=多邊形 +menu_maps_selection_circle=圓 +menu_maps_selection_add=添加選擇的區域 +menu_maps_selection_add_by_gpx=添加GPX沿線區域 +#Bookmark +menu_bookmark=書簽 +menu_bookmark_save=保存當前顯示區域 +menu_bookmark_manage=管理書簽 +#Panels +menu_panels=面板 +menu_show_hide_left_panel=顯示/隱藏 左側面板 +menu_show_hide_gpx_panel=顯示/隱藏 GPX面板 +#Debug +menu_debug=高級 +menu_debug_show_hide_tile_border=顯示/隱藏 圖片邊框 +menu_debug_show_all_map_source=顯示所有地圖源 +menu_debug_refresh_map_source=刷新自定義地圖源 +menu_debug_show_log_file=查看日誌檔案 +menu_debug_log_level=日誌級別 +menu_debug_system_report=查看系統信息 +#help +menu_help=幫助 +menu_help_readme=幫助文檔 +menu_help_how_to_preview=如何查看地圖 +menu_help_licenses=版權聲明與許可證 +menu_help_about=關於 +#tools +menu_tool=工具 + +#---------- Dialog ------------------------- +#About +dlg_about_title=關於 +dlg_about_version=版本: +dlg_about_program_version=開發版本: +dlg_about_download_mapplus=下載地圖加加 +dlg_about_download_mapplus_url=https://itunes.apple.com/cn/app/id438868200?mt=8 +#Font choose +dlg_font_choose_title=選擇字體 +dlg_font_choose_preview=預覽 +dlg_font_choose_name=名稱 +dlg_font_choose_style=風格 +dlg_font_choose_size=大小 +#Licenses +dlg_license_title=版權聲明與許可證 +#Help +dlg_help_title=幫助 +#Manager Bookmark +dlg_mgn_bookmark_title=管理書簽 +dlg_mgn_bookmark_delete=刪除書簽 +#Bookmark Add +dlg_add_bookmark_msg=請為新建書簽設置一個名字 +#New Atlas +dlg_new_atlas_title=新建地圖冊 +dlg_new_atlas_name_title=新建地圖冊名字: +dlg_new_atlas_default_atlas_name=我的地圖冊 +dlg_new_atlas_select_format_title=請選擇地圖冊類型 +#Working Progress +dlg_progress_title=完成 +dlg_progress_about_btn=關於 +dlg_progress_count=已完成: 0 +dlg_progress_count_i=已完成: %d +#Select GPX Track +dlg_gpx_track_select_title=設置區域寬度 +dlg_gpx_track_select_distance=設置邊沿到線路中心線的距離: %d %s +#Add GPS point +dlg_gpx_inpu_point_name=請為該點輸入一個名字: +#Show All Map Source +dlg_show_source_title=全部地圖源 +dlg_show_source_column_name=名稱 +dlg_show_source_column_display_text=全稱 +dlg_show_source_column_rev=內部版本號 +dlg_show_source_column_type=類型 +#Select Dir +dlg_select_dir_title=選擇檔案夾 +dlg_select_dir_description=檔案夾 +#Atlas download progress +dlg_download_title=正在創建地圖冊 +dlg_download_zoom_level_progress=正在下載縮放等級: +dlg_download_map_progress=正在下載地圖: +dlg_download_atlas_progress=%d%% 已完成 - 當前地圖冊: %s, 類型: %s +dlg_download_done_percent=%d%% 已完成 +dlg_download_done_tenthpercent=%.1f%% 已完成 +dlg_download_window_title=

正在創建地圖冊...

+dlg_download_map_info_label_default=正在下載地圖 ABCDEFGHIJKLMNOPQRSTUVWXYZ-nn, 圖層 ABCDEFGHIJKLMNOPQRSTUVWXYZ, 地圖源 ABCDEFGHIJKLMNOPQRSTUVWXYZ +dlg_download_map_info_label=正在下載地圖 %s 的圖層 %s , 從地圖源: %s +dlg_download_map_done_count_default=已完成000, 共 000 +dlg_download_map_done_count=已完成%d, 共 %d +dlg_download_remain_time_default=剩余時間: 00000 分 00 秒 +dlg_download_remain_time=剩余時間: %s +dlg_download_tile_done_count_default=已下載圖片 1000000, 共 1000000 塊 +dlg_download_tile_done_count=已下載圖片 %d 塊,共 %d 塊 +dlg_download_map_create_title=創建地圖檔案 +dlg_download_avg_speed=平均下載速度 +dlg_download_avg_speed_value=: %s / 秒 +dlg_download_total_bytes=從地圖源下載 +dlg_download_bytes_from_cache=從本地緩存讀取 +dlg_download_thread_count=當前使用線程數 +dlg_download_retry_count=臨時出錯圖片 +dlg_download_retry_count_value=: 當前地圖: %d 次, 總共: %d 次 +dlg_download_failed_count=無法下載圖片 +dlg_download_failed_count_value=: 當前地圖: %d 次, 總共: %d 次 +dlg_download_error_tips=

地圖下載的出錯次數(臨時出錯/無法下載)。

\ +

下載某個圖片失敗時,系統會重試2次,如果第二次重試時
\ +仍然無法下載. 則會標記為出錯塊, 並且在本次地圖
\ +下載過程中不會對其進行再次下載

+dlg_download_total_time=下載時間 +dlg_download_checkbox_ignore_error=忽略下載錯誤並自動繼續 +dlg_download_status_title=狀態: +dlg_download_status_running=運行中 +dlg_download_status_aborted=終止 +dlg_download_status_finished=完成 +dlg_download_status_paused=暫停 +dlg_download_btn_abort=終止下載 +dlg_download_btn_abort_tips=終止地圖冊的下載 +dlg_download_btn_close_win=關閉窗口 +dlg_download_btn_close_win_tips_disable=地圖冊正在下載中... +dlg_download_btn_close_win_tips_enable=關閉地圖冊下載窗口 +dlg_download_btn_open_folder=打開下載檔案夾 +dlg_download_btn_open_folder_tips_disabled=地圖冊正在下載中... +dlg_download_btn_open_folder_tips_enabled=打開地圖冊下載保存的檔案夾 +dlg_download_btn_pause_resume=暫停/恢復 +dlg_download_time_unknown=未知 +dlg_download_abort_title=地圖冊下載已終止 +dlg_download_abort_window_title=

地圖冊已經被用戶終止下載

+dlg_download_succeed_title=地圖冊下載已成功 +dlg_download_succeed_window_title=

地圖冊以成功完成

+dlg_download_errors_todo=下載地圖圖片出錯多個錯誤 - 您希望如何處理? +dlg_download_errors_todo_msg=多個地圖圖片下載失敗. \ +您當前連接的地圖服務器或者選擇的下載區域可能存在問題. \ +
您希望:

\ +繼續 下載,並忽略遇到的圖片錯誤? (將導致部分圖塊空白/丟失)
\ +重試 從頭開始重新下載一遍該本地圖?
\ +跳過 當前地圖所有圖片,進行下一個地圖的下載?
\ +終止 當前地圖和整個地圖冊的所有下載?
+dlg_download_errors_missing_tile=錯誤:部分地圖圖片在下載過程丟失,仍然繼續嗎? +dlg_download_errors_missing_tile_msg=下載地圖冊存在問題.\n\ +成功下載的地圖圖片數量少於期望下載的圖片數量.\n\ +即地圖冊下載過程中存在圖片的丟失.\n共有 %d 張圖片未成功下載.\n\n\ +你仍然希望繼續整個地圖冊的創建嗎? +dlg_download_show_error_report=查看錯誤報告 +dlg_download_erro_head=運行出錯: +#Splash Frame +splash_title=正在啟動 Mobile Atlas Creator 中文版... + +#---------- Setting ------------------------- +set_title=設置 +#Display +set_display_title=顯示 +set_display_unit_system_title=單位 +set_display_unit_system_metric=公制單位 +set_display_unit_system_imperial=英制單位 +set_display_unit_system_scale_bar=比例尺單位: +set_display_language=界面語言 +set_display_language_choose=選擇界面語言(需重啟): +set_display_language_choose_tips=選擇一種語言,需要重啟MOBAC才能生效 +set_display_language_restart_desc=您需要重新啟動MOBAC才能使得新語言生效. +set_display_language_msg_title=需要重啟 +set_display_language_msg=語言修改需要重新啟動MOBAC才能生效,現在就退出MOBAC嗎? +set_display_grid=經緯度網格線 +set_display_grid_compress=簡化坐標 +set_display_grid_compress_tips=選中後,坐標文本將使用簡寫. +set_display_grid_title=選擇顏色 +set_display_grid_width=線寬: +set_display_grid_width_tips=設置繪制寬度 +set_display_grid_color=顏色: +set_display_grid_color_tips=設置網格線和文本的顏色 +set_display_grid_font=字體: +set_display_grid_font_tips=設置文本坐標的字體 +#Map sources config +set_mapsrc_config_title=地圖源設置 +set_mapsrc_config_online_update=在線更新地圖源 +set_mapsrc_config_online_update_btn=開始在線更新 +set_mapsrc_config_online_update_no_update=無可用更新 +set_mapsrc_config_online_update_done=更新成功 +set_mapsrc_config_online_update_failed=更新失敗 +set_mapsrc_config_online_update_msg_outdate=當前版本的 MOBAC 不支持在線更新,\ +請升級到最新版本後再試。 +set_mapsrc_config_online_update_msg_noneed=您已經擁有最新版本的地圖包,無需更新。 +set_mapsrc_config_online_update_msg_done=%d 個地圖源被更新.\n\ +請重新啟動 MOBAC 來安裝這些更新。 +set_mapsrc_config_osmhiking=Reit-und Wanderkarte ($Abo) 地圖設置 +set_mapsrc_config_osmhiking_purchased=已支付的 ticket ID: +set_mapsrc_config_osmhiking_howto=如果獲取一個 ticket (德語) +set_mapsrc_config_osmhiking_howto_url=http://www.wanderreitkarte.de/shop_abo_de.php +#Map sources +set_mapsrc_mgr_title=管理地圖源 +set_mapsrc_mgr_title_enabled=啟用的地圖源 +set_mapsrc_mgr_title_disabled=禁用的地圖源 +set_mapsrc_mgr_move_up_tips=上移選中的地圖源 +set_mapsrc_mgr_move_down_tips=下移選中的地圖源 +set_mapsrc_mgr_move_left_tips=啟用選中的地圖源 +set_mapsrc_mgr_move_right_tips=禁用選中的地圖源 +#Tile Update +set_tile_update_title=圖片塊更新 +set_tile_update_desc=\ +本地緩存的圖片塊會按此規則在必要時重新下載(更新)
\ +每個圖片塊會首先按地圖服務器預設的有效時間控制更新,
\ +當地圖源服務器沒有提供相關設置,則按照下面設置更新 +set_tile_update_default_expiration=默認有效時間 +set_tile_update_default_expiration_desc=只有當地圖源未提供有效時間時,
\ +才會使用默認有效時間進行管理. +set_tile_update_max_expiration=最大有效時間 +set_tile_update_min_expiration=最小有效時間 +#Tile Store +set_tile_store_title=本地緩存庫 +set_tile_store_enable_checkbox=允許使用本地緩存來預覽和下載地圖 +set_tile_store_settings=本地緩存設置 +set_tile_store_information=信息 +set_tile_store_info_mapsrc=地圖源 +set_tile_store_info_tiles=已緩存圖片 +set_tile_store_info_size=已緩存大小 +set_tile_store_info_disabled_subfix= (禁用) +set_tile_store_info_delete_tips=刪除所有 %s 的本地緩存圖片. +set_tile_store_info_deleteing_tips=正在刪除 - 請稍等... +set_tile_store_info_total=緩存大小合計 +#Map Size +set_map_size_title=地圖大小 +set_map_size_settings=地圖大小設置 +set_map_size_max_size_of_rect=矩形選區對應的地圖最大寬度或高度: +set_map_size_overlap_tiles=地圖區域的圖片塊疊加個數: +set_map_size_desc=\ +如果矩形選區的長或者寬超出設置的最大大小,
\ +MOBAC會在您添加區域到地圖冊的時候,
\ +自動拆分區域為多個子地圖,以保證每個子地圖的大小
\ +都會小於設置的最大地圖大小.
\ +註意:該設置對多邊形選區無效,對於已經添加的選區也無效
\ +您可以在地圖冊的樹型結構中查看當前地圖的大小.
\ +
\ +使用TrekBuddy用戶請註意:
\ +TrekBuddy 在 v0.9.88 版本之前不支持地圖大於 32767.
\ +之後版本可以支持大小在 1048575 以內的地圖. +#Directory +set_directory_title=檔案夾 +set_directory_output=地圖冊導出檔案夾 +set_directory_output_tips=如果設置為空,則使用默認檔案夾:
%s +set_directory_output_select=重新選擇 +set_directory_output_select_dlg_title=選擇檔案夾 +#Network +set_net_title=網絡 +set_net_connection=網絡連接 +set_net_connection_desc=同時運行的下載連接數 +set_net_bandwidth_desc=下載帶寬限速 +set_net_bandwidth_unlimited=無限制 +set_net_proxy=HTTP 代理 +set_net_proxy_settings=代理設置: +set_net_proxy_settings_java=使用Java代理設置 +set_net_proxy_settings_application=使用應用的代理設置 +set_net_proxy_settings_custom=用戶自定義 +set_net_proxy_settings_custom_auth=用戶自定義(帶認證) +set_net_proxy_host=代理服務器: +set_net_proxy_port=端口: +set_net_proxy_username=用戶名: +set_net_proxy_password=密碼: +set_net_default=默認 +set_net_default_ignore_error=下載時忽略錯誤並繼續下載 +#Paper Atlas +set_paper_title=頁面設置 +#Paper Atlas - Size +set_paper_size=大小 +set_paper_size_default=默認 +set_paper_size_default_tips=選擇該選項, 則頁面大小由指定的格式決定. +set_paper_size_default_combo_tips=選擇一個默認的頁面格式 +set_paper_size_default_landscape=橫向排版 +set_paper_size_default_landscape_tips=選擇該選項,頁面方向為橫向。 +set_paper_size_custom=自定義 +set_paper_size_custom_tips=選擇該選項,用戶可以手工指定頁面大小 +set_paper_size_custom_width=寬度: +set_paper_size_custom_width_tips=指定頁面寬度. +set_paper_size_custom_height=高度: +set_paper_size_custom_height_tips=指定頁面高度 +set_paper_size_selection=選擇區域 +set_paper_size_selection_tips=選擇該選項,頁面按地圖的大小區域決定 +#Paper Atlas - Additions +set_paper_additions=附加 +set_paper_wgs_grid=經緯度網格線 +set_paper_wgs_grid_tips=如果選擇,將會打印經緯度格線(WGS-64) +set_paper_wgs_grid_density_tips=選擇經緯度網格線密度 +set_paper_paper_nubmer=頁面編號 +set_paper_paper_nubmer_tips=選擇該選項,頁碼編號將會打印在每一頁 +set_paper_scale_bar=比例尺 +set_paper_scale_tips=選擇該選項,比例尺將會打印在地圖上 +set_paper_compass=指南針 +set_paper_compass_tips=選擇該選項,指南針將會打印在地圖上 +#Paper Atlas - Margins +set_paper_margins=縮進 +set_paper_margins_tips=指定縮進的尺寸 +set_paper_margins_top=上: +set_paper_margins_left=左: +set_paper_margins_bottom=下: +set_paper_margins_right=右: +#Paper Atlas - Advanced +set_paper_advanced=高級 +set_paper_advanced_compression=壓縮: +set_paper_advanced_compression_tips=指定PDF文檔的壓縮級別.
\ +0 表示不壓縮, 9 表示最大壓縮.
\ +數字越大,壓縮比例越高,默認為6. +set_paper_advanced_dpi=分辨率 +set_paper_advanced_dpi_tips=指定圖片的分辨率 (DPI) .
\ +一般地,更高的值意味著更好的視覺效果.
\ +默認值為 96, 可以提升到120以獲得更好的效果,
\ +但請註意:您的PDF閱讀器也須以相當的分辨率來顯示
\ +才能獲得最佳效果 + +set_paper_advanced_overlap=重疊: +set_paper_advanced_overlap_tips=指定相鄰兩個頁面的重疊區大小. +set_paper_advanced_crop=裁剪: +set_paper_advanced_crop_tips=指定裁剪的百分比.
\ +超過這個比例位置的頁面邊界將會被裁剪掉 + +#Paper Atlas - Actions +set_paper_actions=操作 +set_paper_actions_import_xml=從XML導入 +set_paper_actions_import_xml_tip=從XML導入所有頁面設置 +set_paper_actions_export_xml=導出到XML +set_paper_actions_export_xml_tip=導出所有頁面設置到XML檔案 +set_paper_actions_restore_default=恢復到默認值 +set_paper_actions_restore_default_tips=重置全部頁面設置為默認值
\ +所有當前的設置的參數將會被覆蓋. +set_paper_actions_error_import=無法導入頁面設置檔案: +set_paper_actions_error_export=無法導出到頁面設置檔案: +set_paper_actions_error_reason=原因: +set_paper_actions_error_title=發生錯誤! +set_paper_actions_xml_filter=XML 檔案 (*.xml) +#Error +set_error_saving_title=保存設置出錯 +set_error_saving_msg=無法保存設置到檔案:\n ( %s ) + +#---------- Message ------------------------- +#common +msg_no_zoom_level_selected=請至少選擇一個縮放級別 +msg_no_select_area=請選擇一個區域 +#Add GPX Track Polygon +msg_add_gpx_polygon_too_many_track=當前選擇檔案包含不止一條線路\n請選擇其中一條線路(track)或線路段(segment)。 +msg_add_gpx_polygon_too_many_segment=當前選擇的線路包含不止一條線路段(segment)\n請選擇其中一條線路段(segment)。 +msg_add_gpx_polygon_no_select=沒有選擇GPX線路(track)或線路段(segment) +msg_add_gpx_polygon_maxsize_title=超出最大地圖尺寸 +msg_add_gpx_polygon_maxsize=至少一個地圖超出了最大尺寸,\n\ +但本程序暫不支持多邊形區域的自動拆分,\n您仍然希望繼續嗎? +#Convert Atlas +msg_convert_atlas_format=轉換地圖冊格式 +msg_convert_incompatible_format=地圖和地圖冊的格式不兼容 +#Log file +msg_no_log_file_config=沒有配置的日誌檔案 +msg_no_log_file=日誌檔案不存在:

%s +#Setting +msg_settings_write_error=寫入設置信息到"settings.xml"時出錯 +#Refresh +msg_refresh_all_map_source_done=%d 個自定義地圖源被成功刷新 +#Help +msg_no_found_readme_file=不能找到檔案:README.HTM +#Merge Layer +msg_confirm_merge_layer_title=確認合並地圖層 +msg_confirm_merge_layer=您確定要將地圖層 %s 合並到 %s 嗎? +msg_merge_layer_failed=圖層合並失敗 +#Tile Size select +msg_invalid_tile_size=無效的圖片塊大小!
請輸入 %d 到 %d 之間的數 +#Tile Store Coverage +msg_tile_store_failed_retrieve_coverage=無法顯示本地緩存的覆蓋範圍.\n\ +可能是因為選擇的區域過大或者縮放級別過大. +#Atlas download thread +msg_out_of_memory_title=存儲體不足. +msg_out_of_memory_head=Mobile Atlas Creator沒有足夠存儲體繼續運行. +msg_out_of_memory_detail=\n當前分配給MOBAC的最大存儲體數為 %d MB +msg_atlas_download_abort=地圖冊下載已經被終止 +msg_too_many_tiles_title=無法進行地圖冊下載 +msg_too_many_tiles_msg=Mobile Atlas Creator 檢測到您即將\n\ +下載的地圖冊擁有遠遠超出常規數目的圖片塊數量,\n\ +請減少在高縮放級別時選擇的地圖區域大小,並重新下載.\n\ +最大允許的地圖圖片塊數為: %d\n\ +當前地圖冊中的圖片塊數為:%d +msg_atlas_version_mismatch=當前打開/加載的地圖冊是MOBAC早期版本創建的,\n\ +該版本與當前版本的存儲格式無法兼容。\n\n\ +建議您清除掉整個地圖冊,並刪除受影響的地圖冊保存檔案,\n\ +否則在使用相關地圖冊時,可能出現更多異常情況.\n +msg_atlas_data_check_failed=載入保存的地圖冊時出現錯誤。\n\ +這可能是因為檔案被修改或者格式不兼容造成的,\n\n\ +建議您清除掉整個地圖冊,並刪除受影響的地圖冊保存檔案,\n\ +否則在使用相關地圖冊時,可能出現更多異常情況.\n +#Settings +msg_setting_file_is_changed_by_other_title=覆蓋已修改內容? +msg_setting_file_is_changed_by_other=參數檔案 settings.xml 已經被其它程序修改.\n\ +您希望覆蓋其它程序的修改嗎?\n\ +當覆蓋後,其它程序所作的所有修改都會被清除! +msg_settings_file_can_not_parse=無法讀取或解析設置檔案,程序將會退出。 +#Others +msg_atlas_is_empty=地圖冊為空,請首先為地圖冊添加一個區域 +msg_environment_not_support_title=不支持平臺 +msg_environment_not_support=不支持您的運行環境.
桌面平臺: %s
操作系統: %s +msg_environment_failed_open_output_title=打開地圖冊檔案夾失敗 +msg_environment_failed_open_output=打開地圖冊輸出檔案夾失敗.
+msg_environment_use_commond=
使用命令: %s
+msg_environment_commond_param=參數: %d : %s +msg_environment_lack_memory_title=警告: 存儲體設置過小 +msg_environment_lack_memory=\ +警告: Mobile Atlas Creator 在運行時允許使用的存儲體過小.
\ +當前Mobile Atlas Creator 最大可用存儲體僅為: %s.

\ +請確認您使用了正確啟動程序或腳本來運行MOBAC:
\ +對於Window系統,請使用Mobile Atlas Creator.exe
\ +對於Linux/Unix/OSX系統,請使用 start.sh 腳本,
\ +腳本參數:-Xmx 512M 可指定512M最大可用存儲體.

\ +例如: java -Xmx512M -jar Mobile_Atlas_Creator.jar
\ +
Mobile Atlas Creator將在您確定後繼續運行
+msg_environment_error_create_dir=創建檔案夾 %s 出錯,檔案夾路徑 %s\n 程序將退出. +msg_environment_error_write_file=無法寫入到檔案:\n %s \n路徑:%s \n 請修改檔案權限並重啟MOBAC. +msg_environment_error_create_setting=無法寫入到設置檔案settings.xml, 程序將退出. +msg_environment_error_init_mapsrc_dir=無法初始化地圖源檔案夾:\n +msg_environment_error_load_cloudmade=無法載入自定義的CloudMade 地圖源. 請確保openstreetmap地圖源包完好可用. +msg_environment_mapsrc_dir_not_exist=地圖源檔案夾不存在 - 位置:\n %s \n\ +請確保您在解壓縮MOBAC的zip包時,\n\ +包含了所有的子檔案夾. +msg_environment_unable_to_start=無法啟動Mobile Atlas Creator: \n +msg_environment_jre_bellow=您電腦上安裝的Java環境(Java Runtime Environment)版本過低.\n\ +Mobile Atlas Creator 需要至少Java 6 (1.6) 或更高版本.\n\ +請升級您的Java運行時環境,再重啟Mobile Atlas Creator.\n\n\ +當前檢測到的 Java 版本為: %@ +msg_environment_jre_bellow_title=Java運行環境不兼容 +msg_environment_invalid_source_folder=指定的地圖源檔案夾不存在:\n地圖名: %s\ +\n地圖源檔案夾: %s +msg_environment_invalid_source_folder_zoom=無法找到指定縮放等級的檔案夾:\n地圖名: %s\ +\n地圖源檔案夾: %s +msg_environment_invalid_source_folder_title=無效地圖源檔案夾 +msg_tools_exec_command_ask=要執行的命令行:
%s +msg_tools_exec_command_ask_title=您希望執行該命令嗎? +msg_custom_map_invalid_source_file_title=無效地圖源檔案 +msg_custom_map_invalid_source_file=指定數據庫檔案不存在.\n地圖名: %s \n檔案名: %s +msg_custom_map_invalid_source_zip_title=無效的zip源 +msg_custom_map_invalid_source_zip=指定的zip地圖源檔案不存在:\n地圖名: %s \nZip 檔案: %s +msg_custom_map_failed_open_source_zip_title=讀取zip檔案失敗 +msg_custom_map_failed_open_source_zip=無法讀取指定的地圖源zip檔案:\n地圖名: %s \nZip 檔案: %s +msg_custom_map_invalid_source_sqlitedb=指定的 SQLite 數據庫檔案不存在:\n地圖名: %s \n檔案名: %s +msg_custom_map_source_failed_load_sqlitedb_title=加載數據庫檔案出錯 +msg_custom_map_source_failed_load_sqlitedb=指定的數據庫檔案無法被加載:\n地圖名: %s\ +\n檔案名: %s \n錯誤信息:%s +msg_update_map_pack_error_title=加載地圖包出錯 +msg_update_map_pack_error=在加載升級的地圖包時出現錯誤,請重新啟動 MOBAC 來恢復到之前的版本。 +msg_error_load_atlas_profile_title=加載地圖冊出錯 - 繼續嗎? +msg_error_load_atlas_profile=加載地圖冊出錯:
 %s 
\ +
檔案: %s 行/列: %d / %d
\ +您希望繼續加載該地圖冊嗎?
\ +這可能導致地圖冊不完整或者無法正常工作.
+msg_environment_slqite_lib_missing_title="出錯 - SQLite 庫不可用" +msg_environment_slqite_lib_missing=無法找到SQLite庫檔案. \ +您當前的地圖冊格式必須Sqlite庫才能運行.
請查閱幫助檔案 README.HTM \ +的"SQLite atlas formats"章節獲得更多幫助. " +msg_environment_image_format_not_available_title=圖片格式不支持 - 缺少圖片庫 +msg_environment_image_format_not_available=該檔案格式需要安裝額外的Java圖片處理庫:
\ +Java Advanced Image library (jai_core.jar & jai_codec.jar)
\ +更多信息請查看幫助檔案 README.HTM \ +中的 Requirements章節. +msg_tile_store_access_conflict_title=運行沖突 +msg_tile_store_access_conflict=\ +多個 Mobile Atlas Creator 正在試圖同時訪問\n\ +相同的本地地圖緩存,本地地圖緩存只能同時有一個MOBAC\n\ +實例訪問.請關閉其它MOBAC程序實例後再試.\n \ No newline at end of file diff --git a/src/main/resources/mobac/resources/xsl/gpx10to11.xsl b/src/main/resources/mobac/resources/xsl/gpx10to11.xsl new file mode 100644 index 0000000..459619e --- /dev/null +++ b/src/main/resources/mobac/resources/xsl/gpx10to11.xsl @@ -0,0 +1,75 @@ + + + + + + + + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" + Transformation changes from GPX version 1.0 to version 1.1 are as follows: + All gpx element child elements except wpt, rte and trk regrouped under newly created metadata element + author and email re-written, author child link element and copyright elements not created. + all occurances of url and urlname elements re-written as Link element, + extensions element, trk and rte child elements not created. + trk child elements course and speed disregarded as they are obsolete in version 1.1 + by William A Slabbekoorn aka Cybarber, August 2005. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/featuretests/SimpleHttpClient.java b/src/test/java/featuretests/SimpleHttpClient.java new file mode 100644 index 0000000..3a48996 --- /dev/null +++ b/src/test/java/featuretests/SimpleHttpClient.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package featuretests; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.SecureRandom; + +public class SimpleHttpClient implements Runnable { + + static SecureRandom RND = new SecureRandom(); + + public static void main(String[] args) throws InterruptedException { + + int threadCount = 8; + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + Thread t = new Thread(new SimpleHttpClient(), "Worker " + i); + t.start(); + threads[i] = t; + } + for (Thread t : threads) + t.join(); + } + + public void run() { + try { + for (int i = 0; i < 100; i++) + load(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void load() throws MalformedURLException, IOException { + String url = String.format("http://localhost/tile?x=%d&y=%d&z=8", RND.nextInt(1000), RND.nextInt(1000)); + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.connect(); + + int code = conn.getResponseCode(); + if (code != 200) + throw new IOException("Invalid HTTP response"); + InputStream in = conn.getInputStream(); + ByteArrayOutputStream bout = new ByteArrayOutputStream(32000); + byte[] buffer = new byte[2049]; + do { + int read = in.read(buffer); + if (read <= 0) + break; + bout.write(buffer, 0, read); + } while (true); + System.out.println(Thread.currentThread().getName() + " retrieved " + bout.size() + " bytes - url: " + url); + } +} diff --git a/src/test/java/featuretests/SimpleServer.java b/src/test/java/featuretests/SimpleServer.java new file mode 100644 index 0000000..1984b76 --- /dev/null +++ b/src/test/java/featuretests/SimpleServer.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package featuretests; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Properties; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import Acme.Serve.Serve; + +public class SimpleServer { + static SecureRandom RND = new SecureRandom(); + private static final byte[] COLORS = { 0,// + (byte) 0xff, (byte) 0xff, (byte) 0xff, // white + (byte) 0xff, (byte) 0x00, (byte) 0x00 // red + }; + + private static final IndexColorModel COLORMODEL = new IndexColorModel(8, 2, COLORS, 1, false); + + private static final Font FONT_LARGE = new Font("Sans Serif", Font.BOLD, 30); + private static final Font FONT_SMALL = new Font("Sans Serif", Font.BOLD, 20); + + public static class DummyDataServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, + IOException { + BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_BYTE_INDEXED, COLORMODEL); + Graphics2D g2 = image.createGraphics(); + try { + g2.setColor(Color.WHITE); + g2.fillRect(0, 0, 255, 255); + g2.setColor(Color.RED); + g2.drawRect(0, 0, 255, 255); + g2.drawLine(0, 0, 255, 255); + g2.drawLine(255, 0, 0, 255); + String url = request.getRequestURL().toString(); + String query = request.getQueryString(); + if (query != null) + url += "?" + query; + String[] strings = url.split("[\\&\\?]"); + int y = 40; + g2.setFont(FONT_SMALL); + for (String s : strings) { + g2.drawString(s, 8, y); + g2.setFont(FONT_LARGE); + y += 35; + } + ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next(); + writer.setOutput(ImageIO.createImageOutputStream(response.getOutputStream())); + IIOImage ioImage = new IIOImage(image, null, null); + writer.write(null, ioImage, null); + } catch (Exception e) { + e.printStackTrace(); + } finally { + g2.dispose(); + } + } + + } + + public static void main(String[] args) { + Properties properties = new Properties(); + + properties.put("port", 80); + properties.put("z", "20"); + properties.put("keep-alive", Boolean.TRUE); + properties.put("bind-address", "127.0.0.1"); + properties.setProperty(Acme.Serve.Serve.ARG_NOHUP, "nohup"); + Serve tjws = new Serve(properties, System.out); + + tjws.addServlet("/", new DummyDataServlet()); + tjws.serve(); + } +} diff --git a/src/test/java/featuretests/TileCoverageTest.java b/src/test/java/featuretests/TileCoverageTest.java new file mode 100644 index 0000000..b0cea6c --- /dev/null +++ b/src/test/java/featuretests/TileCoverageTest.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package featuretests; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.File; + +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.Logging; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.berkeleydb.BerkeleyDbTileStore; +import mobac.program.tilestore.berkeleydb.DelayedInterruptThread; + +public class TileCoverageTest { + + public static class MyThread extends DelayedInterruptThread { + + public MyThread() { + super("Test"); + } + + @Override + public void run() { + BerkeleyDbTileStore tileStore = (BerkeleyDbTileStore) TileStore.getInstance(); + try { + int zoom = 8; + BufferedImage image = tileStore.getCacheCoverage( + MapSourcesManager.getInstance().getSourceByName("Google Maps"), zoom, new Point(80, 85), + new Point(1 << zoom, 1 << zoom)); + ImageIO.write(image, "png", new File("test.png")); + JFrame f = new JFrame("Example"); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + f.getContentPane().add( + new JLabel(new ImageIcon( + image.getScaledInstance(image.getWidth() * 4, image.getHeight() * 4, 0)))); + f.pack(); + f.setLocationRelativeTo(null); + f.setVisible(true); + } catch (Exception e) { + e.printStackTrace(); + } + tileStore.closeAll(); + } + + } + + public static void main(String[] args) throws InterruptedException { + Logging.configureConsoleLogging(); + + TileStore.initialize(); + Thread t = new MyThread(); + t.start(); + t.join(); + } + +} diff --git a/src/test/java/mapsources/MapSourceTestCase.java b/src/test/java/mapsources/MapSourceTestCase.java new file mode 100644 index 0000000..8397205 --- /dev/null +++ b/src/test/java/mapsources/MapSourceTestCase.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mapsources; + +import java.net.HttpURLConnection; + +import junit.framework.TestCase; +import mobac.mapsources.AbstractHttpMapSource; +import mobac.program.download.TileDownLoader; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.interfaces.MapSource.LoadMethod; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.Settings; +import mobac.tools.Cities; +import mobac.utilities.Utilities; + +public class MapSourceTestCase extends TestCase { + + private final HttpMapSource mapSource; + private final EastNorthCoordinate testCoordinate; + + public MapSourceTestCase(Class mapSourceClass) throws InstantiationException, + IllegalAccessException { + this(mapSourceClass.newInstance()); + } + + public MapSourceTestCase(HttpMapSource mapSource) { + this(mapSource, Cities.getTestCoordinate(mapSource, Cities.BERLIN)); + } + + public MapSourceTestCase(HttpMapSource mapSource, EastNorthCoordinate testCoordinate) { + super(mapSource.getName()); + this.mapSource = mapSource; + this.testCoordinate = testCoordinate; + } + + public void runMapSourceTest() throws Exception { + runTest(); + } + + @Override + protected void runTest() throws Exception { + int zoom = mapSource.getMaxZoom(); + + MapSpace mapSpace = mapSource.getMapSpace(); + int tilex = mapSpace.cLonToX(testCoordinate.lon, zoom) / mapSpace.getTileSize(); + int tiley = mapSpace.cLatToY(testCoordinate.lat, zoom) / mapSpace.getTileSize(); + + if (mapSource instanceof AbstractHttpMapSource) + try { + mapSource.getTileData(-1, 0, 0, LoadMethod.SOURCE); + } catch (Exception e) { + } + + HttpURLConnection c = mapSource.getTileUrlConnection(zoom, tilex, tiley); + c.setReadTimeout(10000); + c.addRequestProperty("User-agent", Settings.getInstance().getUserAgent()); + c.setRequestProperty("Accept", TileDownLoader.ACCEPT); + + c.connect(); + try { + // if (c.getResponseCode() == 302) { + // log.debug(c.getResponseMessage()); + // } + if (c.getResponseCode() != 200) { + throw new MapSourceTestFailedException(mapSource, c); + } + byte[] imageData = Utilities.getInputBytes(c.getInputStream()); + if (imageData.length == 0) + throw new MapSourceTestFailedException(mapSource, "Image data empty", c); + if (Utilities.getImageType(imageData) == null) { + throw new MapSourceTestFailedException(mapSource, "Image data of unknown format", c); + } + switch (mapSource.getTileUpdate()) { + case ETag: + case IfNoneMatch: + if (c.getHeaderField("ETag") == null) { + throw new MapSourceTestFailedException(mapSource, "No ETag present but map sources uses " + + mapSource.getTileUpdate() + "\n", c); + } + break; + case LastModified: + if (c.getHeaderField("Last-Modified") == null) + throw new MapSourceTestFailedException(mapSource, + "No Last-Modified entry present but map sources uses " + mapSource.getTileUpdate() + "\n", + c); + break; + } + } finally { + c.disconnect(); + } + } +} diff --git a/src/test/java/mapsources/MapSourceTestFailedException.java b/src/test/java/mapsources/MapSourceTestFailedException.java new file mode 100644 index 0000000..bee4391 --- /dev/null +++ b/src/test/java/mapsources/MapSourceTestFailedException.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mapsources; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import mobac.program.interfaces.MapSource; + +public class MapSourceTestFailedException extends Exception { + + private static final long serialVersionUID = 1L; + + final int httpResponseCode; + final HttpURLConnection conn; + final URL url; + final Class mapSourceClass; + + public MapSourceTestFailedException(MapSource mapSource, String msg, HttpURLConnection conn) + throws IOException { + super(msg); + this.mapSourceClass = mapSource.getClass(); + this.conn = conn; + this.url = conn.getURL(); + this.httpResponseCode = conn.getResponseCode(); + } + + public MapSourceTestFailedException(MapSource mapSource, HttpURLConnection conn) throws IOException { + this(mapSource, "", conn); + } + + public MapSourceTestFailedException(Class mapSourceClass, URL url, + int httpResponseCode) { + super(); + this.mapSourceClass = mapSourceClass; + this.url = url; + this.conn = null; + this.httpResponseCode = httpResponseCode; + } + + public int getHttpResponseCode() { + return httpResponseCode; + } + + @Override + public String getMessage() { + String msg = super.getMessage(); + msg = "MapSource test failed: " + msg + " " + mapSourceClass.getSimpleName() + " HTTP " + + httpResponseCode + "\n" + conn.getURL(); + if (conn != null) + msg += "\n" + printHeaders(conn); + return msg; + } + + protected String printHeaders(HttpURLConnection conn) { + StringWriter sw = new StringWriter(); + sw.append("Headers:\n"); + for (Map.Entry> entry : conn.getHeaderFields().entrySet()) { + String key = entry.getKey(); + for (String elem : entry.getValue()) { + if (key != null) + sw.append(key + " = "); + sw.append(elem); + sw.append("\n"); + } + } + return sw.toString(); + } + +} diff --git a/src/test/java/mapsources/MapSourcesTestSuite.java b/src/test/java/mapsources/MapSourcesTestSuite.java new file mode 100644 index 0000000..1ee386b --- /dev/null +++ b/src/test/java/mapsources/MapSourcesTestSuite.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mapsources; + +import java.net.HttpURLConnection; +import java.util.HashSet; + +import javax.xml.bind.JAXBException; + +import junit.framework.TestSuite; +import mobac.mapsources.AbstractMultiLayerMapSource; +import mobac.mapsources.DefaultMapSourcesManager; +import mobac.mapsources.MapSourcesManager; +import mobac.mapsources.impl.DebugMapSource; +import mobac.mapsources.impl.LocalhostTestSource; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.Settings; +import mobac.tools.Cities; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import unittests.helper.DummyTileStore; + +/** + * {@link TestSuite} that tests every available map source for operability. The operability test consists of the + * download of one map tile at the highest available zoom level of the map source. By default the map tile to be + * downloaded is located in the middle of Berlin (at the coordinate of {@link #BERLIN}). As some map providers do not + * cover Berlin for each {@link MapSource} a different test coordinate can be specified using {@link #testCoordinates}. + * + */ +public class MapSourcesTestSuite extends TestSuite { + + protected final Logger log; + + public static final EastNorthCoordinate C_DEFAULT = Cities.BERLIN; + + private HashSet testedMapSources; + + public MapSourcesTestSuite() throws JAXBException { + super(); + HttpURLConnection.setFollowRedirects(false); + Logging.configureConsoleLogging(); + Logger.getRootLogger().setLevel(Level.ERROR); + log = Logger.getLogger(MapSourcesTestSuite.class); + testedMapSources = new HashSet(); + DefaultMapSourcesManager.initialize(); + Settings.load(); + for (MapSource mapSource : MapSourcesManager.getInstance().getAllMapSources()) { + if (mapSource instanceof DebugMapSource || mapSource instanceof LocalhostTestSource) + continue; + if (mapSource instanceof AbstractMultiLayerMapSource) { + for (MapSource ms : (AbstractMultiLayerMapSource) mapSource) + addMapSourcesTestCase(ms); + } else + addMapSourcesTestCase(mapSource); + } + } + + private void addMapSourcesTestCase(MapSource mapSource) { + if (!(mapSource instanceof HttpMapSource)) + return; + if (testedMapSources.contains(mapSource.getName())) + return; + EastNorthCoordinate coordinate = Cities.getTestCoordinate(mapSource, C_DEFAULT); + addTest(new MapSourceTestCase((HttpMapSource) mapSource, coordinate)); + testedMapSources.add(mapSource.getName()); + } + + public static TestSuite suite() throws JAXBException { + Logging.configureConsoleLogging(); + ProgramInfo.initialize(); // Load revision info + DummyTileStore.initialize(); + DefaultMapSourcesManager.initializeEclipseMapPacksOnly(); + MapSourcesTestSuite testSuite = new MapSourcesTestSuite(); + return testSuite; + } + +} diff --git a/src/test/java/mobac/tools/Cities.java b/src/test/java/mobac/tools/Cities.java new file mode 100644 index 0000000..42cd5ea --- /dev/null +++ b/src/test/java/mobac/tools/Cities.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools; + +import java.util.HashMap; + +import mobac.mapsources.mappacks.region_europe_east.FreemapSlovakia; +import mobac.mapsources.mappacks.region_europe_east.FreemapSlovakiaCycling; +import mobac.mapsources.mappacks.region_europe_east.FreemapSlovakiaHiking; +import mobac.mapsources.mappacks.region_oceania.NzTopoMaps; +import mobac.program.interfaces.MapSource; +import mobac.program.model.EastNorthCoordinate; + +public class Cities { + + private static final HashMap, EastNorthCoordinate> TEST_COORDINATES; + + public static final EastNorthCoordinate NEY_YORK = new EastNorthCoordinate(40.75, -73.88); + public static final EastNorthCoordinate BERLIN = new EastNorthCoordinate(52.50, 13.39); + public static final EastNorthCoordinate MOSCOW = new EastNorthCoordinate(55.75, 37.63); + public static final EastNorthCoordinate PRAHA = new EastNorthCoordinate(50.00, 14.41); + public static final EastNorthCoordinate BANGALORE = new EastNorthCoordinate(12.95, 77.616667); + public static final EastNorthCoordinate SHANGHAI = new EastNorthCoordinate(31.2333, 121.4666); + public static final EastNorthCoordinate WARSZAWA = new EastNorthCoordinate(52.2166, 21.0333); + public static final EastNorthCoordinate VIENNA = new EastNorthCoordinate(48.20, 16.37); + public static final EastNorthCoordinate BRATISLAVA = new EastNorthCoordinate(48.154, 17.14); + public static final EastNorthCoordinate SEOUL = new EastNorthCoordinate(37.55, 126.98); + public static final EastNorthCoordinate SYDNEY = new EastNorthCoordinate(-33.8, 151.3); + public static final EastNorthCoordinate PERTH = new EastNorthCoordinate(-31.9, 115.8); + public static final EastNorthCoordinate BUDAPEST = new EastNorthCoordinate(47.47, 19.05); + public static final EastNorthCoordinate MUNICH = new EastNorthCoordinate(48.13, 11.58); + public static final EastNorthCoordinate OSLO = new EastNorthCoordinate(59.91, 10.75); + public static final EastNorthCoordinate BERN = new EastNorthCoordinate(46.95, 7.45); + public static final EastNorthCoordinate LONDON = new EastNorthCoordinate(51.51, -0.11); + public static final EastNorthCoordinate INNSBRUCK = new EastNorthCoordinate(47.26, 11.39); + public static final EastNorthCoordinate TOKYO = new EastNorthCoordinate(35.683889, 139.774444); + public static final EastNorthCoordinate TAIPEI = new EastNorthCoordinate(25.033333, 121.633333); + public static final EastNorthCoordinate WELLINGTON = new EastNorthCoordinate(-41.283333, 174.766667); + + static { + TEST_COORDINATES = new HashMap, EastNorthCoordinate>(); + // TEST_COORDINATES.put(GoogleMapMaker.class, Cities.BANGALORE); + // TEST_COORDINATES.put(Cykloatlas.class, Cities.PRAHA); + // TEST_COORDINATES.put(CykloatlasRelief.class, Cities.PRAHA); + // TEST_COORDINATES.put(GoogleMapsChina.class, Cities.SHANGHAI); + // TEST_COORDINATES.put(GoogleMapsKorea.class, Cities.SEOUL); + // TEST_COORDINATES.put(MicrosoftMapsChina.class, Cities.SHANGHAI); + // TEST_COORDINATES.put(MicrosoftVirtualEarth.class, Cities.NEY_YORK); + // TEST_COORDINATES.put(MultimapCom.class, Cities.LONDON); + // TEST_COORDINATES.put(MultimapOSUkCom.class, Cities.LONDON); + // TEST_COORDINATES.put(DoCeluPL.class, Cities.WARSZAWA); + // TEST_COORDINATES.put(YahooMapsJapan.class, TOKYO); + // TEST_COORDINATES.put(YahooMapsTaiwan.class, TAIPEI); + // TEST_COORDINATES.put(AustrianMap.class, Cities.VIENNA); + TEST_COORDINATES.put(FreemapSlovakia.class, Cities.BRATISLAVA); + TEST_COORDINATES.put(FreemapSlovakiaHiking.class, Cities.BRATISLAVA); + TEST_COORDINATES.put(FreemapSlovakiaCycling.class, Cities.BRATISLAVA); + // TEST_COORDINATES.put(NearMap.class, Cities.PERTH); + // TEST_COORDINATES.put(HubermediaBavaria.class, Cities.MUNICH); + // TEST_COORDINATES.put(OpenPisteMap.class, Cities.MUNICH); + // TEST_COORDINATES.put(StatkartTopo2.class, Cities.OSLO); + // TEST_COORDINATES.put(Turaterkep.class, Cities.BUDAPEST); + // TEST_COORDINATES.put(Bergfex.class, Cities.INNSBRUCK); + // TEST_COORDINATES.put(AeroChartsIFR.class, Cities.NEY_YORK); + // TEST_COORDINATES.put(AeroChartsIFRH.class, Cities.NEY_YORK); + // TEST_COORDINATES.put(AeroChartsVFR.class, Cities.NEY_YORK); + // TEST_COORDINATES.put(MicrosoftOrdnanceSurveyExplorer.class, Cities.LONDON); + // TEST_COORDINATES.put(YandexMap.class, Cities.MOSCOW); + // TEST_COORDINATES.put(YandexSat.class, Cities.MOSCOW); + TEST_COORDINATES.put(NzTopoMaps.class, Cities.WELLINGTON); + } + + public static EastNorthCoordinate getTestCoordinate(MapSource mapSource, EastNorthCoordinate defaultCoordinate) { + return getTestCoordinate(mapSource.getClass(), defaultCoordinate); + } + + public static EastNorthCoordinate getTestCoordinate(Class mapSourceClass, + EastNorthCoordinate defaultCoordinate) { + EastNorthCoordinate coord = TEST_COORDINATES.get(mapSourceClass); + if (coord != null) + return coord; + else + return defaultCoordinate; + } + +} diff --git a/src/test/java/mobac/tools/MapPackUploadSelector.java b/src/test/java/mobac/tools/MapPackUploadSelector.java new file mode 100644 index 0000000..3b72dc5 --- /dev/null +++ b/src/test/java/mobac/tools/MapPackUploadSelector.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools; + +import java.io.File; +import java.security.cert.CertificateException; + +import mobac.mapsources.loader.MapPackManager; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.utilities.Utilities; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +public class MapPackUploadSelector { + + /** + * @param args + */ + public static void main(String[] args) { + Logging.configureConsoleLogging(Level.DEBUG); + Logger log = Logger.getLogger(MapPackUploadSelector.class); + ProgramInfo.initialize(); + try { + File mapPackDir = new File("mapsources"); + File mapPackUpdateDir = new File(mapPackDir, "updates"); + Utilities.mkDirs(mapPackUpdateDir); + for (File newMapPack : mapPackUpdateDir.listFiles()) + Utilities.deleteFile(newMapPack); + + Utilities.mkDirs(mapPackUpdateDir); + MapPackManager mpm = new MapPackManager(mapPackDir); + String md5sumList = mpm.downloadMD5SumList(); + String[] changedMapPacks = mpm.searchForOutdatedMapPacks(md5sumList); + for (String mapPackName : changedMapPacks) { + log.info("Changed local map pack found: " + mapPackName); + File mapPack = new File(mapPackDir, mapPackName); + try { + mpm.testMapPack(mapPack); + File mapPackCopy = new File(mapPackUpdateDir, mapPackName); + Utilities.copyFile(mapPack, mapPackCopy); + } catch (CertificateException e) { + log.error("Map pack not copied because of invalid signature", e); + } + } + if (changedMapPacks.length > 0) { + Utilities.copyFile(new File(mapPackDir, "mappacks-md5.txt"), new File(mapPackUpdateDir, + "mappacks-md5.txt")); + } else { + log.info("No updated map packs found"); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + +} diff --git a/src/test/java/mobac/tools/MapSourceCapabilityDetector.java b/src/test/java/mobac/tools/MapSourceCapabilityDetector.java new file mode 100644 index 0000000..32e18a9 --- /dev/null +++ b/src/test/java/mobac/tools/MapSourceCapabilityDetector.java @@ -0,0 +1,338 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools; + +import static mobac.tools.Cities.BERLIN; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.xml.bind.JAXBException; + +import mobac.mapsources.DefaultMapSourcesManager; +import mobac.mapsources.MapSourcesManager; +import mobac.mapsources.mappacks.region_oceania.NzTopoMaps; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.download.TileDownLoader; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSource; +import mobac.program.interfaces.MapSpace; +import mobac.program.model.EastNorthCoordinate; +import mobac.program.model.Settings; +import mobac.program.model.TileImageType; +import mobac.utilities.Utilities; + +import org.apache.log4j.Logger; + +public class MapSourceCapabilityDetector { + + public static final Logger log = Logger.getLogger(MapSourceCapabilityDetector.class); + + public static final SecureRandom RND = new SecureRandom(); + + public static final EastNorthCoordinate C_DEFAULT = BERLIN; + + /** + * @param args + */ + public static void main(String[] args) { + + // *************************************************************************** + Class mapSourceClass = NzTopoMaps.class; + // *************************************************************************** + + Logging.configureLogging(); + ProgramInfo.initialize(); + try { + Settings.load(); + } catch (JAXBException e) { + System.err.println(e.getMessage()); + } + DefaultMapSourcesManager.initialize(); + MapSourcesManager.getInstance().getAllMapSources(); + List result = testMapSource(mapSourceClass); + MapSourceCapabilityGUI gui = new MapSourceCapabilityGUI(result); + gui.setVisible(true); + } + + public static List testMapSource(Class mapSourceClass) { + return testMapSource(mapSourceClass, Cities.getTestCoordinate(mapSourceClass, C_DEFAULT)); + } + + public static List testMapSource(Class mapSourceClass, + EastNorthCoordinate coordinate) { + if (coordinate == null) + throw new NullPointerException("Coordinate not set for " + mapSourceClass.getSimpleName()); + try { + return testMapSource(mapSourceClass.newInstance(), coordinate); + } catch (Exception e) { + System.err.println("Error while testing map source: " + e.getMessage()); + return null; + } + } + + public static List testMapSource(String mapSourceName, EastNorthCoordinate coordinate) { + MapSource mapSource = MapSourcesManager.getInstance().getSourceByName(mapSourceName); + if (!(mapSource instanceof HttpMapSource)) + throw new RuntimeException("Not an HTTP map source: " + mapSource.getName()); + return testMapSource((HttpMapSource) mapSource, coordinate); + } + + public static List testMapSource(HttpMapSource mapSource, + EastNorthCoordinate coordinate) { + if (!(mapSource instanceof HttpMapSource)) + throw new RuntimeException("Not an HTTP map source: " + mapSource.getName()); + ArrayList result = new ArrayList(); + for (int zoom = mapSource.getMinZoom(); zoom < mapSource.getMaxZoom(); zoom++) { + MapSourceCapabilityDetector mstd = new MapSourceCapabilityDetector((HttpMapSource) mapSource, coordinate, + zoom); + mstd.testMapSource(); + result.add(mstd); + // log.trace(mstd); + } + return result; + } + + private final HttpMapSource mapSource; + private final EastNorthCoordinate coordinate; + private final int zoom; + + private URL url; + private HttpURLConnection c; + + private boolean eTagPresent = false; + private boolean expirationTimePresent = false; + private boolean lastModifiedTimePresent = false; + private boolean ifNoneMatchSupported = false; + private boolean ifModifiedSinceSupported = false; + + private String contentType = "?"; + + public MapSourceCapabilityDetector(Class mapSourceClass, EastNorthCoordinate coordinate, + int zoom) throws InstantiationException, IllegalAccessException { + this(mapSourceClass.newInstance(), coordinate, zoom); + } + + public MapSourceCapabilityDetector(HttpMapSource mapSource, EastNorthCoordinate coordinate, int zoom) { + this.mapSource = mapSource; + if (mapSource == null) + throw new NullPointerException("MapSource not set"); + this.coordinate = coordinate; + this.zoom = zoom; + } + + public void testMapSource() { + try { + log.debug("Testing " + mapSource.toString()); + + MapSpace mapSpace = mapSource.getMapSpace(); + int tilex = mapSpace.cLonToX(coordinate.lon, zoom) / mapSpace.getTileSize(); + int tiley = mapSpace.cLatToY(coordinate.lat, zoom) / mapSpace.getTileSize(); + + c = mapSource.getTileUrlConnection(zoom, tilex, tiley); + url = c.getURL(); + log.trace("Sample url: " + c.getURL()); + log.trace("Connecting..."); + c.setReadTimeout(3000); + c.setRequestProperty("User-agent", ProgramInfo.getUserAgent()); + c.setRequestProperty("Accept", TileDownLoader.ACCEPT); + c.connect(); + log.debug("Connection established - response HTTP " + c.getResponseCode()); + if (c.getResponseCode() != 200) + return; + + // printHeaders(); + + byte[] content = Utilities.getInputBytes(c.getInputStream()); + TileImageType detectedContentType = Utilities.getImageType(content); + + contentType = c.getContentType(); + contentType = contentType.substring(6); + if ("png".equals(contentType)) + contentType = "png"; + else if ("jpeg".equals(contentType)) + contentType = "jpg"; + else + contentType = "unknown: " + c.getContentType(); + if (contentType.equals(detectedContentType.getFileExt())) + contentType += " (verified)"; + else + contentType += " (unverified)"; + log.debug("Image format : " + contentType); + + String eTag = c.getHeaderField("ETag"); + eTagPresent = (eTag != null); + if (eTagPresent) { + // log.debug("eTag : " + eTag); + testIfNoneMatch(content); + } + // else log.debug("eTag : -"); + + // long date = c.getDate(); + // if (date == 0) + // log.debug("Date time : -"); + // else + // log.debug("Date time : " + new Date(date)); + + long exp = c.getExpiration(); + expirationTimePresent = (c.getHeaderField("expires") != null) && (exp != 0); + if (exp == 0) { + // log.debug("Expiration time : -"); + } else { + // long diff = (exp - System.currentTimeMillis()) / 1000; + // log.debug("Expiration time : " + new Date(exp) + // + " => " + // + Utilities.formatDurationSeconds(diff)); + } + long modified = c.getLastModified(); + lastModifiedTimePresent = (c.getHeaderField("last-modified") != null) && (modified != 0); + // if (modified == 0) + // log.debug("Last modified time : not set"); + // else + // log.debug("Last modified time : " + new + // Date(modified)); + + testIfModified(); + + } catch (Exception e) { + e.printStackTrace(); + } + log.debug("\n"); + } + + private void testIfNoneMatch(byte[] content) throws Exception { + String eTag = c.getHeaderField("ETag"); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(content); + String hexDigest = getHexString(digest); + // log.debug("content MD5 : " + hexDigest); + if (hexDigest.equals(eTag)) + log.debug("eTag content : md5 hex string"); + String quotedHexDigest = "\"" + hexDigest + "\""; + if (quotedHexDigest.equals(eTag)) + log.debug("eTag content : quoted md5 hex string"); + + HttpURLConnection c2 = (HttpURLConnection) url.openConnection(); + c2.addRequestProperty("If-None-Match", eTag); + c2.connect(); + int code = c2.getResponseCode(); + boolean supported = (code == 304); + ifNoneMatchSupported = supported; + // System.out.print("If-None-Match response: "); + // log.debug(b2s(supported) + " - " + code + " (" + + // c2.getResponseMessage() + ")"); + c2.disconnect(); + } + + private void testIfModified() throws IOException { + HttpURLConnection c2 = (HttpURLConnection) url.openConnection(); + c2.setIfModifiedSince(System.currentTimeMillis() + 1000); // future date + c2.connect(); + int code = c2.getResponseCode(); + boolean supported = (code == 304); + ifModifiedSinceSupported = supported; + // System.out.print("If-Modified-Since : "); + // log.debug(b2s(supported) + " - " + code + " (" + + // c2.getResponseMessage() + ")"); + } + + protected void printHeaders() { + log.trace("\nHeaders:"); + for (Map.Entry> entry : c.getHeaderFields().entrySet()) { + String key = entry.getKey(); + for (String elem : entry.getValue()) { + if (key != null) + log.debug(key + " = "); + log.debug(elem); + } + } + } + + @Override + public String toString() { + StringWriter sw = new StringWriter(); + sw.append("Mapsource.........: " + mapSource.getName() + "\n"); + sw.append("Current TileUpdate: " + mapSource.getTileUpdate() + "\n"); + sw.append("If-None-Match.....: " + b2s(ifNoneMatchSupported) + "\n"); + sw.append("ETag..............: " + b2s(eTagPresent) + "\n"); + sw.append("If-Modified-Since.: " + b2s(ifModifiedSinceSupported) + "\n"); + sw.append("LastModified......: " + b2s(lastModifiedTimePresent) + "\n"); + sw.append("Expires...........: " + b2s(expirationTimePresent) + "\n"); + + return sw.toString(); + } + + public int getZoom() { + return zoom; + } + + public boolean iseTagPresent() { + return eTagPresent; + } + + public boolean isExpirationTimePresent() { + return expirationTimePresent; + } + + public boolean isLastModifiedTimePresent() { + return lastModifiedTimePresent; + } + + public boolean isIfModifiedSinceSupported() { + return ifModifiedSinceSupported; + } + + public boolean isIfNoneMatchSupported() { + return ifNoneMatchSupported; + } + + public String getContentType() { + return contentType; + } + + private static String b2s(boolean b) { + if (b) + return "supported"; + else + return "-"; + } + + static final byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f' }; + + public static String getHexString(byte[] raw) throws UnsupportedEncodingException { + byte[] hex = new byte[2 * raw.length]; + int index = 0; + + for (byte b : raw) { + int v = b & 0xFF; + hex[index++] = HEX_CHAR_TABLE[v >>> 4]; + hex[index++] = HEX_CHAR_TABLE[v & 0xF]; + } + return new String(hex, "ASCII"); + } +} diff --git a/src/test/java/mobac/tools/MapSourceCapabilityGUI.java b/src/test/java/mobac/tools/MapSourceCapabilityGUI.java new file mode 100644 index 0000000..c8230fb --- /dev/null +++ b/src/test/java/mobac/tools/MapSourceCapabilityGUI.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.HeadlessException; +import java.util.List; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; + +public class MapSourceCapabilityGUI extends JFrame { + + private final List result; + + public MapSourceCapabilityGUI(List result) throws HeadlessException { + super("Map source capabilities"); + setLayout(new BorderLayout()); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.result = result; + JTable table = new JTable(new Model()); + table.setDefaultRenderer(Object.class, new Renderer()); + add(table.getTableHeader(), BorderLayout.NORTH); + add(table, BorderLayout.CENTER); + pack(); + } + + private class Model extends AbstractTableModel { + + public int getRowCount() { + return result.size(); + } + + public Object getValueAt(int rowIndex, int columnIndex) { + + MapSourceCapabilityDetector mscd = result.get(rowIndex); + + switch (columnIndex) { + case 0: + return mscd.getZoom(); + case 1: + return mscd.iseTagPresent(); + case 2: + return mscd.isLastModifiedTimePresent(); + case 3: + return mscd.isIfNoneMatchSupported(); + case 4: + return mscd.isIfModifiedSinceSupported(); + case 5: + return mscd.getContentType(); + } + return null; + } + + public int getColumnCount() { + return 6; + } + + public String getColumnName(int column) { + switch (column) { + case 0: + return "Zoom"; + case 1: + return "eTag"; + case 2: + return "LastModified"; + case 3: + return "IfNoneMatch"; + case 4: + return "IfModifiedSince"; + case 5: + return "Content type"; + } + return null; + } + + } + + private class Renderer extends DefaultTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + this.setHorizontalAlignment(JLabel.CENTER); + Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if ((value != null) && (value instanceof Boolean) && ((Boolean) value)) + c.setBackground(Color.GREEN); + else + c.setBackground(Color.WHITE); + return c; + } + } +} diff --git a/src/test/java/mobac/tools/MapUpdateTypeLister.java b/src/test/java/mobac/tools/MapUpdateTypeLister.java new file mode 100644 index 0000000..2670f48 --- /dev/null +++ b/src/test/java/mobac/tools/MapUpdateTypeLister.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools; + +import java.util.Vector; + +import mobac.mapsources.MapSourcesManager; +import mobac.program.Logging; +import mobac.program.interfaces.HttpMapSource; +import mobac.program.interfaces.MapSource; + +public class MapUpdateTypeLister { + + /** + * @param args + */ + public static void main(String[] args) { + Logging.configureConsoleLogging(); + Vector mapSources = MapSourcesManager.getInstance().getAllMapSources(); + for (MapSource mapSource : mapSources) { + if (mapSource instanceof HttpMapSource) { + HttpMapSource httpMapSource = (HttpMapSource) mapSource; + String name = mapSource.getName(); + name = name.substring(0, Math.min(25, name.length())); + System.out.println(String.format("%25s %s", name, httpMapSource.getTileUpdate())); + } + } + } +} diff --git a/src/test/java/mobac/tools/testtileserver/TestTileServer.java b/src/test/java/mobac/tools/testtileserver/TestTileServer.java new file mode 100644 index 0000000..a8d70fc --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/TestTileServer.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver; + +import java.io.FileInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.Properties; + +import javax.servlet.Servlet; +import javax.swing.JOptionPane; + +import mobac.program.Logging; +import mobac.tools.testtileserver.servlets.AbstractTileServlet; +import mobac.tools.testtileserver.servlets.JpgTileGeneratorServlet; +import mobac.tools.testtileserver.servlets.PngFileTileServlet; +import mobac.tools.testtileserver.servlets.PngTileGeneratorServlet; +import mobac.tools.testtileserver.servlets.ShutdownServlet; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import Acme.Serve.Serve; + +/** + * + *

+ * Provides a dummy HTTP server that returns a png for each request. + *

+ * + * @author r_x + */ +public class TestTileServer extends Serve { + + private static final Logger log = Logger.getLogger(TestTileServer.class); + + private final int port; + + public TestTileServer(int port) { + this.port = port; + // setting aliases, for an optional file servlet + PathTreeDictionary aliases = new PathTreeDictionary(); + setMappingTable(aliases); + // setting properties for the server, and exchangable Acceptors + Properties properties = new Properties(); + + properties.put("port", port); + properties.put("z", "20"); // max number of created threads in a thread + properties.put("keep-alive", Boolean.TRUE); + properties.put("bind-address", "127.0.0.1"); + // pool + properties.setProperty(Acme.Serve.Serve.ARG_NOHUP, "nohup"); + this.arguments = properties; + + Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + } + + public void start() { + Thread t = new Thread() { + + @Override + public void run() { + TestTileServer.this.serve(); + } + + }; + t.setDaemon(true); + t.start(); + } + + public void stop() { + try { + notifyStop(); + } catch (Exception e) { + } + destroyAllServlets(); + } + + @Override + public void log(String message) { + log.debug(message); + } + + @Override + public void log(Exception e, String message) { + log.error(message, e); + } + + @Override + public void log(String message, Throwable t) { + log.error(message, t); + } + + public void setTileServlet(AbstractTileServlet tileServlet) { + Servlet oldServlet = getServlet("/"); + if (oldServlet != null) + unloadServlet(oldServlet); + addServlet("/", tileServlet); + } + + protected class ShutdownHook extends Thread { + public void run() { + TestTileServer.this.stop(); + } + } + + public int getPort() { + return port; + } + + public static void stopOtherTileServer(int port) { + try { + HttpURLConnection c = (HttpURLConnection) new URL("http://127.0.0.1:" + port + "/shutdown") + .openConnection(); + c.setConnectTimeout(100); + c.setRequestMethod("DELETE"); + c.connect(); + if (c.getResponseCode() == 202) + Thread.sleep(1000); + c.disconnect(); + } catch (SocketTimeoutException e) { + // port is unused -> OK + } catch (Exception e) { + log.error("failed to stop other tile server instance on port " + port + ": " + e.getMessage()); + } + } + + public static void main(String[] args) { + Logging.configureConsoleLogging(Level.DEBUG); + try { + Properties prop = new Properties(); + FileInputStream fi = new FileInputStream("TestTileServer.properties"); + prop.load(fi); + fi.close(); + System.getProperties().putAll(prop); + } catch (IOException e) { + JOptionPane.showMessageDialog(null, "Unable to load file DebugTileServer.properties: " + e.getMessage() + + "\nUsing default values", "Error loading properties", JOptionPane.ERROR_MESSAGE); + } + int port = Integer.getInteger("TestHttpServer.port", 80); + TestTileServer server = new TestTileServer(port); + + int errorRate = Integer.getInteger("TestHttpServer.errorRate", 0); + boolean errorOnUrl = Boolean.getBoolean("TestHttpServer.errorOnSpecificUrls"); + int delay = Integer.getInteger("TestHttpServer.delay", 0); + + boolean generatePngForEachRequest = Boolean.getBoolean("TestHttpServer.generatePNGperRequest"); + + server.addServlet("/shutdown", new ShutdownServlet(server)); + AbstractTileServlet tileServlet; + if (generatePngForEachRequest) { + int pngCompression = Integer.getInteger("TestHttpServer.generatedPNGcompression", 1); + tileServlet = new PngTileGeneratorServlet(pngCompression); + } else { + int imageNum = Integer.getInteger("TestHttpServer.staticImage", 0); + tileServlet = new PngFileTileServlet(imageNum); + } + tileServlet.setErrorRate(errorRate); + tileServlet.setErrorOnUrl(errorOnUrl); + tileServlet.setDelay(delay); + // server.setTileServlet(tileServlet); + + server.addServlet("/", new JpgTileGeneratorServlet(90)); + + stopOtherTileServer(port); + server.serve(); + } +} diff --git a/src/test/java/mobac/tools/testtileserver/servlets/AbstractTileGeneratorServlet.java b/src/test/java/mobac/tools/testtileserver/servlets/AbstractTileGeneratorServlet.java new file mode 100644 index 0000000..41731bc --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/servlets/AbstractTileGeneratorServlet.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver.servlets; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; + +import javax.servlet.http.HttpServletRequest; + +public abstract class AbstractTileGeneratorServlet extends AbstractTileServlet { + + private static final byte[] COLORS = { 0,// + (byte) 0xff, (byte) 0xff, (byte) 0xff, // white + (byte) 0xff, (byte) 0x00, (byte) 0x00 // red + }; + + private static final IndexColorModel COLORMODEL = new IndexColorModel(8, 2, COLORS, 1, false); + + private static final Font FONT_LARGE = new Font("Sans Serif", Font.BOLD, 30); + private static final Font FONT_SMALL = new Font("Sans Serif", Font.BOLD, 20); + + protected BufferedImage generateImage(HttpServletRequest request) { + BufferedImage tile = new BufferedImage(256, 256, BufferedImage.TYPE_BYTE_INDEXED, COLORMODEL); + Graphics2D g2 = tile.createGraphics(); + try { + g2.setColor(Color.WHITE); + g2.fillRect(0, 0, 255, 255); + g2.setColor(Color.RED); + g2.drawRect(0, 0, 255, 255); + g2.drawLine(0, 0, 255, 255); + g2.drawLine(255, 0, 0, 255); + String url = request.getRequestURL().toString(); + String query = request.getQueryString(); + if (query != null) + url += "?" + query; + log.debug(url); + String[] strings = url.split("[\\&\\?]"); + int y = 40; + g2.setFont(FONT_SMALL); + for (String s : strings) { + g2.drawString(s, 8, y); + g2.setFont(FONT_LARGE); + y += 35; + } + } finally { + g2.dispose(); + } + return tile; + } +} diff --git a/src/test/java/mobac/tools/testtileserver/servlets/AbstractTileServlet.java b/src/test/java/mobac/tools/testtileserver/servlets/AbstractTileServlet.java new file mode 100644 index 0000000..76f47f3 --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/servlets/AbstractTileServlet.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver.servlets; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.log4j.Logger; + +public abstract class AbstractTileServlet extends HttpServlet { + + protected static final SecureRandom RND = new SecureRandom(); + + protected final Logger log; + + /** + * Error rate in percent [0..100] + */ + protected int errorRate = 0; + + protected boolean errorOnUrl = true; + + protected int delay = 0; + + private final MessageDigest md5; + + public AbstractTileServlet() { + log = Logger.getLogger(this.getClass()); + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, + IOException { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + } + if (errorResponse(request, response)) + return; + super.service(request, response); + } + + public boolean errorResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { + if (errorRate == 0) + return false; + if (errorOnUrl) { + String url = request.getRequestURL() + request.getQueryString(); + byte[] digest = md5.digest(url.getBytes()); + int hash = Math.abs(digest[4] % 100); + log.debug(url.toString() + " -> " + hash + ">" + errorRate + "?"); + if (hash > errorRate) + return false; + } else { + int rnd = RND.nextInt(100); + if (rnd > errorRate) + return false; + } + response.sendError(404); + log.debug("Error response sent"); + return true; + } + + public int getErrorRate() { + return errorRate; + } + + public void setErrorRate(int errorRate) { + this.errorRate = errorRate; + } + + public boolean isErrorOnUrl() { + return errorOnUrl; + } + + public void setErrorOnUrl(boolean errorOnUrl) { + this.errorOnUrl = errorOnUrl; + } + + public int getDelay() { + return delay; + } + + public void setDelay(int delay) { + this.delay = delay; + } + +} diff --git a/src/test/java/mobac/tools/testtileserver/servlets/JpgTileGeneratorServlet.java b/src/test/java/mobac/tools/testtileserver/servlets/JpgTileGeneratorServlet.java new file mode 100644 index 0000000..2777762 --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/servlets/JpgTileGeneratorServlet.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver.servlets; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import mobac.program.tiledatawriter.TileImageJpegDataWriter; + +/** + * + * Generates for each request a png tile of size 256x256 containing the url request broken down to multiple lines. + * + * @author r_x + * + */ +public class JpgTileGeneratorServlet extends AbstractTileGeneratorServlet { + + private final TileImageJpegDataWriter jpgWriter; + + /** + * @param compressionLevel + * [0..100] + */ + public JpgTileGeneratorServlet(int compressionLevel) { + super(); + jpgWriter = new TileImageJpegDataWriter(compressionLevel / 100d); + jpgWriter.initialize(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + BufferedImage tile = generateImage(request); + response.setContentType("image/jpeg"); + ServletOutputStream out = response.getOutputStream(); + ByteArrayOutputStream bout = new ByteArrayOutputStream(32000); + try { + synchronized (jpgWriter) { + jpgWriter.processImage(tile, bout); + } + byte[] buf = bout.toByteArray(); + response.setContentLength(buf.length); + out.write(buf); + } finally { + out.close(); + response.flushBuffer(); + } + } +} diff --git a/src/test/java/mobac/tools/testtileserver/servlets/PngFileTileServlet.java b/src/test/java/mobac/tools/testtileserver/servlets/PngFileTileServlet.java new file mode 100644 index 0000000..ebd51d6 --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/servlets/PngFileTileServlet.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver.servlets; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * Returns for each an every request the same static png file. For performance reasons the file "tile.png" is cached on + * servlet initialization. + * + * @author r_x + * + */ +public class PngFileTileServlet extends AbstractTileServlet { + + public static final String[] IMAGE_NAMES = { "tile.png", "gradient.png", "cross.png" }; + + private byte[] fileContent = null; + + private final int imageNum; + + public PngFileTileServlet(int imageNum) { + super(); + this.imageNum = imageNum; + } + + @Override + public void init() throws ServletException { + super.init(); + try { + String image = IMAGE_NAMES[imageNum]; + InputStream in = this.getClass().getResourceAsStream("images/" + image); + fileContent = new byte[in.available()]; + in.read(fileContent); + in.close(); + log.info("Static png file " + image + " loaded successfully"); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("image/png"); + response.setContentLength(fileContent.length); + OutputStream out = response.getOutputStream(); + out.write(fileContent); + out.close(); + response.flushBuffer(); + } + +} diff --git a/src/test/java/mobac/tools/testtileserver/servlets/PngTileGeneratorServlet.java b/src/test/java/mobac/tools/testtileserver/servlets/PngTileGeneratorServlet.java new file mode 100644 index 0000000..c7b5c9d --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/servlets/PngTileGeneratorServlet.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver.servlets; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import mobac.utilities.imageio.Png4BitWriter; + +/** + * + * Generates for each request a png tile of size 256x256 containing the url request broken down to multiple lines. + * + * @author r_x + * + */ +public class PngTileGeneratorServlet extends AbstractTileGeneratorServlet { + + private int pngCompressionLevel; + + public PngTileGeneratorServlet(int pngCompressionLevel) { + super(); + this.pngCompressionLevel = pngCompressionLevel; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + BufferedImage tile = generateImage(request); + response.setContentType("image/png"); + OutputStream out = response.getOutputStream(); + ByteArrayOutputStream bout = new ByteArrayOutputStream(16000); + Png4BitWriter.writeImage(bout, tile, pngCompressionLevel, request.getRequestURL().toString()); + byte[] buf = bout.toByteArray(); + response.setContentLength(buf.length); + out.write(buf); + out.close(); + response.flushBuffer(); + } +} diff --git a/src/test/java/mobac/tools/testtileserver/servlets/ShutdownServlet.java b/src/test/java/mobac/tools/testtileserver/servlets/ShutdownServlet.java new file mode 100644 index 0000000..6bc5945 --- /dev/null +++ b/src/test/java/mobac/tools/testtileserver/servlets/ShutdownServlet.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package mobac.tools.testtileserver.servlets; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import Acme.Serve.Serve; + +public class ShutdownServlet extends HttpServlet { + + private final Serve server; + + public ShutdownServlet(Serve server) { + super(); + this.server = server; + } + + @Override + protected void doDelete(HttpServletRequest arg0, HttpServletResponse response) throws ServletException, IOException { + response.setStatus(202); + response.flushBuffer(); + server.notifyStop(); + } + +} diff --git a/src/test/java/unittests/AbstractAtlasCreatorTestCase.java b/src/test/java/unittests/AbstractAtlasCreatorTestCase.java new file mode 100644 index 0000000..1fcaa3b --- /dev/null +++ b/src/test/java/unittests/AbstractAtlasCreatorTestCase.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.swing.UIManager; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; + +import junit.framework.TestCase; +import junit.framework.TestResult; +import mobac.exceptions.AtlasTestException; +import mobac.program.AtlasThread; +import mobac.program.Logging; +import mobac.program.ProgramInfo; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.interfaces.AtlasInterface; +import mobac.program.model.Atlas; +import mobac.program.model.Profile; +import mobac.program.model.TileImageType; +import mobac.program.tilestore.TileStore; +import mobac.tools.testtileserver.TestTileServer; +import mobac.tools.testtileserver.servlets.JpgTileGeneratorServlet; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; + +import unittests.helper.TestMapSourcesManager; + +/** + * Base {@link TestCase} used for testing a specific {@link AtlasCreator} implementation and/or downloading of a Atlas. + */ +public abstract class AbstractAtlasCreatorTestCase extends TestCase { + + protected final Logger log; + protected final File testAtlasDir; + protected final SecureRandom rnd; + + protected static final TestTileServer TEST_TILE_SERVER; + protected static final TestMapSourcesManager TEST_TILE_SERVER_MANAGER; + + static { + Logging.configureConsoleLogging(Level.TRACE,Logging.ADVANCED_LAYOUT); + ProgramInfo.initialize(); + //Logger.getLogger("mobac").setLevel(Level.INFO); + TEST_TILE_SERVER = new TestTileServer(18888); + // TEST_TILE_SERVER.setTileServlet(new PngFileTileServlet(0)); + TEST_TILE_SERVER.setTileServlet(new JpgTileGeneratorServlet(90)); + TEST_TILE_SERVER.start(); + TEST_TILE_SERVER_MANAGER = new TestMapSourcesManager(TEST_TILE_SERVER.getPort(), TileImageType.JPG); + } + + public AbstractAtlasCreatorTestCase() { + super(); + log = Logger.getLogger(this.getClass()); + log.setLevel(Level.DEBUG); + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + } + TileStore.initialize(); + testAtlasDir = new File("target/test-atlases"); + rnd = new SecureRandom(); + } + + @Override + protected void runTest() throws Throwable { + log.info("running test \"" + getName() + "\""); + super.runTest(); + } + + protected void createAtlas(String profileName, Class atlasCreatorClass) + throws InstantiationException, IllegalAccessException, JAXBException, AtlasTestException, + InterruptedException, IOException { + AtlasCreator atlasCreator = atlasCreatorClass.newInstance(); + createAtlas(profileName, atlasCreator); + } + + protected void createAtlas(String profileName, AtlasCreator atlasCreator) throws JAXBException, AtlasTestException, + InterruptedException, IOException { + String profileFile = "profiles/" + Profile.getProfileFileName(profileName); + InputStream in = ClassLoader.getSystemResourceAsStream(profileFile); + assertNotNull(in); + AtlasInterface atlas = loadAtlas(in); + createAtlas(atlas, atlasCreator); + } + + protected AtlasInterface loadAtlas(String profileName) throws JAXBException { + String profileFile = "profiles/" + Profile.getProfileFileName(profileName); + InputStream in = ClassLoader.getSystemResourceAsStream(profileFile); + assertNotNull(in); + return loadAtlas(in); + } + + protected AtlasInterface loadAtlas(InputStream in) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(Atlas.class); + Unmarshaller um = context.createUnmarshaller(); + return (AtlasInterface) um.unmarshal(in); + } + + /** + * + * @param atlas + * @param atlasCreator + * @return directory in which the atlas has been created + * @throws AtlasTestException + * @throws InterruptedException + * @throws IOException + */ + protected File createAtlas(AtlasInterface atlas, AtlasCreator atlasCreator) throws AtlasTestException, + InterruptedException, IOException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HHmmss_SSSS"); + File customAtlasDir = new File(testAtlasDir, atlasCreator.getClass().getSimpleName() + "_" + atlas.getName() + + "_" + sdf.format(new Date())); + createAtlas(atlas, atlasCreator, customAtlasDir); + return customAtlasDir; + } + + protected void createAtlas(AtlasInterface atlas, AtlasCreator atlasCreator, File customAtlasDir) + throws AtlasTestException, InterruptedException, IOException { + log.debug("Creating atlas " + atlas.getName() + " using " + atlasCreator.getClass().getSimpleName() + " to \"" + + customAtlasDir + "\""); + AtlasThread atlasThread = new AtlasThread(atlas, atlasCreator); + atlasThread.setCustomAtlasDir(customAtlasDir); + atlasThread.start(); + atlasThread.join(); + } + + @Override + public TestResult run() { + TestResult result = super.run(); + return result; + } + +} diff --git a/src/test/java/unittests/AtlasDownloadTestCase.java b/src/test/java/unittests/AtlasDownloadTestCase.java new file mode 100644 index 0000000..d8250e0 --- /dev/null +++ b/src/test/java/unittests/AtlasDownloadTestCase.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests; + +import java.io.File; + +import mobac.mapsources.DefaultMapSourcesManager; +import mobac.program.atlascreators.AlpineQuestMap; +import mobac.program.interfaces.AtlasInterface; + +public class AtlasDownloadTestCase extends AbstractAtlasCreatorTestCase { + + public AtlasDownloadTestCase() { + super(); + } + + public void testAtlasCreation() throws Exception { + AtlasInterface atlas; + // atlas = loadAtlas("Germany10-12"); + // atlas = loadAtlas("HamburgPark"); + DefaultMapSourcesManager.initialize(); + atlas = loadAtlas("Munich6-16"); + File dir = createAtlas(atlas, new AlpineQuestMap()); + assertNotNull(dir); + } + +} diff --git a/src/test/java/unittests/CordinateTestCase.java b/src/test/java/unittests/CordinateTestCase.java new file mode 100644 index 0000000..8463a32 --- /dev/null +++ b/src/test/java/unittests/CordinateTestCase.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests; + +import junit.framework.TestCase; +import mobac.utilities.Utilities; +import mobac.utilities.geo.CoordinateDm2Format; +import mobac.utilities.geo.CoordinateDms2Format; + +public class CordinateTestCase extends TestCase { + + public CordinateTestCase() { + super(); + } + + public void testCoordinateDm2Format() throws Exception { + CoordinateDm2Format cf = new CoordinateDm2Format(Utilities.DFS_ENG); + + assertEquals("03° 30.00'", cf.format(3.5d)); + assertEquals("-03° 30.00'", cf.format(-3.5d)); + + assertEquals("-20° 06.00'", cf.format(-20.1d)); + assertEquals("20° 06.00'", cf.format(20.1d)); + + assertEquals("-13° 54.00'", cf.format(-13.9d)); + assertEquals("13° 54.00'", cf.format(13.9d)); + + assertEquals("00° 06.00'", cf.format(0.1d)); + assertEquals("-00° 06.00'", cf.format(-0.1d)); + + assertEquals(-4.25, cf.parse("-04° 15.0'")); + assertEquals(+4.25, cf.parse("+04° 15.0'")); + + assertEquals(-20.1, cf.parse("-20° 6'")); + assertEquals(+20.1, cf.parse("+20° 6'")); + + assertEquals(-13.9, cf.parse("-13° 54'")); + assertEquals(+13.9, cf.parse("+13° 54'")); + + assertEquals(-0.1, cf.parse("-00° 06'")); + assertEquals(+0.1, cf.parse("+00° 06'")); + + } + + public void testCoordinateDms2Format() throws Exception { + CoordinateDms2Format cf = new CoordinateDms2Format(Utilities.DFS_ENG); + + assertEquals("03° 32' 59.99\"", cf.format(3.55d)); + assertEquals("-03° 32' 59.99\"", cf.format(-3.55d)); + + assertEquals("-20° 06' 35.99\"", cf.format(-20.11d)); + assertEquals("20° 06' 35.99\"", cf.format(20.11d)); + + assertEquals("-13° 59' 24.00\"", cf.format(-13.99d)); + assertEquals("13° 59' 24.00\"", cf.format(13.99d)); + + assertEquals("00° 06' 00.00\"", cf.format(0.1d)); + assertEquals("-00° 06' 00.00\"", cf.format(-0.1d)); + + assertEquals(-3.55, cf.parse("-03° 32' 60.00\"")); + assertEquals(+3.55, cf.parse("+03° 32' 60.00\"")); + + assertEquals(-2011, (int) (cf.parse("-20° 6' 36\"").doubleValue() * 100d)); + assertEquals(+2011, (int) (cf.parse("+20° 6' 36\"").doubleValue() * 100d)); + + assertEquals(-1390, (int) (cf.parse("-13° 54' 24\"").doubleValue() * 100)); + assertEquals(+1390, (int) (cf.parse("+13° 54' 24\"").doubleValue() * 100)); + + assertEquals(-0.1, cf.parse("-00° 06' 0\"")); + assertEquals(+0.1, cf.parse("+00° 06' 0\"")); +} +} diff --git a/src/test/java/unittests/KMZTestCase.java b/src/test/java/unittests/KMZTestCase.java new file mode 100644 index 0000000..35bb2ef --- /dev/null +++ b/src/test/java/unittests/KMZTestCase.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests; + +import java.io.File; + +import mobac.program.atlascreators.GoogleEarthOverlay; +import mobac.program.interfaces.AtlasInterface; + +public class KMZTestCase extends AbstractAtlasCreatorTestCase { + + public KMZTestCase() { + super(); + } + + public void testGoogleEarchOverlay() throws Exception { + AtlasInterface atlas = loadAtlas("HamburgPark"); + File dir = createAtlas(atlas, new GoogleEarthOverlay()); + assertNotNull(dir); + } + +// public void testGarminCustom() throws Exception { +// AtlasInterface atlas = loadAtlas("HamburgPark"); +// File dir = createAtlas(atlas, new GarminCustom()); +// assertNotNull(dir); +// } + +} diff --git a/src/test/java/unittests/SQLiteTestCase.java b/src/test/java/unittests/SQLiteTestCase.java new file mode 100644 index 0000000..aa0150d --- /dev/null +++ b/src/test/java/unittests/SQLiteTestCase.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests; + +import mobac.program.atlascreators.RMapsSQLite; + +public class SQLiteTestCase extends AbstractAtlasCreatorTestCase { + + public SQLiteTestCase() { + super(); + } + + public void testRMaps() throws Exception { + log.info("Starting test testRMaps"); + createAtlas("HamburgPark", RMapsSQLite.class); + } + +} diff --git a/src/test/java/unittests/helper/DummyAtlasCreator.java b/src/test/java/unittests/helper/DummyAtlasCreator.java new file mode 100644 index 0000000..12b08b9 --- /dev/null +++ b/src/test/java/unittests/helper/DummyAtlasCreator.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests.helper; + + +import mobac.exceptions.MapCreationException; +import mobac.program.atlascreators.AtlasCreator; +import mobac.program.interfaces.MapSource; + +/** + * A simple {@link AtlasCreator} implementation that does nothing. Can be used + * in case only tile downloading and saving in the tile store is the test + * target. + */ +public class DummyAtlasCreator extends AtlasCreator { + + public DummyAtlasCreator() { + } + + @Override + public void createMap() throws MapCreationException, InterruptedException { + } + + @Override + public boolean testMapSource(MapSource mapSource) { + return true; + } + +} diff --git a/src/test/java/unittests/helper/DummyTileStore.java b/src/test/java/unittests/helper/DummyTileStore.java new file mode 100644 index 0000000..c23b748 --- /dev/null +++ b/src/test/java/unittests/helper/DummyTileStore.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests.helper; + +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import mobac.program.interfaces.MapSource; +import mobac.program.tilestore.TileStore; +import mobac.program.tilestore.TileStoreEntry; +import mobac.program.tilestore.TileStoreInfo; + +public class DummyTileStore extends TileStore { + + public static void initialize() { + INSTANCE = new DummyTileStore(); + } + + public DummyTileStore() { + } + + @Override + public void clearStore(String storeName) { + } + + @Override + public void closeAll() { + } + + @Override + public boolean contains(int x, int y, int zoom, MapSource mapSource) { + return false; + } + + @Override + public TileStoreEntry createNewEntry(int x, int y, int zoom, byte[] data, long timeLastModified, long timeExpires, + String eTag) { + return null; + } + + @Override + public TileStoreEntry createNewEmptyEntry(int x, int y, int zoom) { + return null; + } + + @Override + public String[] getAllStoreNames() { + return null; + } + + @Override + public BufferedImage getCacheCoverage(MapSource mapSource, int zoom, Point tileNumMin, Point tileNumMax) + throws InterruptedException { + return null; + } + + @Override + public TileStoreInfo getStoreInfo(String mapSourceName) throws InterruptedException { + return null; + } + + @Override + public TileStoreEntry getTile(int x, int y, int zoom, MapSource mapSource) { + return null; + } + + @Override + public void prepareTileStore(MapSource mapSource) { + } + + @Override + public void putTile(TileStoreEntry tile, MapSource mapSource) { + } + + @Override + public void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource) throws IOException { + } + + @Override + public void putTileData(byte[] tileData, int x, int y, int zoom, MapSource mapSource, long timeLastModified, + long timeExpires, String eTag) throws IOException { + } + + @Override + public boolean storeExists(MapSource mapSource) { + return false; + } + +} diff --git a/src/test/java/unittests/helper/TestMapSourcesManager.java b/src/test/java/unittests/helper/TestMapSourcesManager.java new file mode 100644 index 0000000..5855cba --- /dev/null +++ b/src/test/java/unittests/helper/TestMapSourcesManager.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests.helper; + +import java.util.Vector; + +import mobac.mapsources.MapSourcesManager; +import mobac.mapsources.impl.LocalhostTestSource; +import mobac.program.interfaces.MapSource; +import mobac.program.model.TileImageType; + +public class TestMapSourcesManager extends MapSourcesManager { + + private final MapSource theMapSource; + + public TestMapSourcesManager(int port, TileImageType tileType) { + super(); + theMapSource = new LocalhostTestSource("Localhost test", port, tileType); + install(); + } + + public TestMapSourcesManager(MapSource mapSource) { + super(); + theMapSource = mapSource; + install(); + } + + public void install() { + INSTANCE = this; + } + + @Override + public Vector getAllMapSources() { + Vector v = new Vector(1); + v.add(theMapSource); + return v; + } + + @Override + public Vector getAllLayerMapSources() { + return getAllMapSources(); + } + + @Override + public MapSource getDefaultMapSource() { + return theMapSource; + } + + @Override + public Vector getEnabledOrderedMapSources() { + return getAllMapSources(); + } + + @Override + public Vector getDisabledMapSources() { + return new Vector(); + } + + @Override + public MapSource getSourceByName(String name) { + return theMapSource; + } + + @Override + public void addMapSource(MapSource mapSource) { + throw new RuntimeException("not implemented"); + } + + @Override + public Vector getAllAvailableMapSources() { + return getAllMapSources(); + } + + + +} diff --git a/src/test/java/unittests/methods/MyMathTests.java b/src/test/java/unittests/methods/MyMathTests.java new file mode 100644 index 0000000..4c8aea0 --- /dev/null +++ b/src/test/java/unittests/methods/MyMathTests.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests.methods; + +import junit.framework.TestCase; +import junit.textui.TestRunner; +import mobac.utilities.MyMath; + +public class MyMathTests extends TestCase { + + public void testRoundDownToNearest() { + assertEquals(0, MyMath.roundDownToNearest(0, 10)); + assertEquals(0, MyMath.roundDownToNearest(1, 10)); + assertEquals(0, MyMath.roundDownToNearest(9, 10)); + assertEquals(10, MyMath.roundDownToNearest(10, 10)); + assertEquals(10, MyMath.roundDownToNearest(11, 10)); + assertEquals(12340, MyMath.roundDownToNearest(12345, 10)); + assertEquals(1024, MyMath.roundDownToNearest(1025, 16)); + assertEquals(1024, MyMath.roundDownToNearest(1024, 256)); + assertEquals(1024, MyMath.roundDownToNearest(1025, 256)); + } + + public void testRoundUpToNearest() { + assertEquals(0, MyMath.roundUpToNearest(0, 10)); + assertEquals(10, MyMath.roundUpToNearest(1, 10)); + assertEquals(10, MyMath.roundUpToNearest(9, 10)); + assertEquals(20, MyMath.roundUpToNearest(11, 10)); + assertEquals(12350, MyMath.roundUpToNearest(12345, 10)); + assertEquals(1040, MyMath.roundUpToNearest(1025, 16)); + assertEquals(1024, MyMath.roundUpToNearest(1024, 256)); + assertEquals(1280, MyMath.roundUpToNearest(1025, 256)); + } + + public static void main(String[] args) { + TestRunner.run(MyMathTests.class); + } + +} diff --git a/src/test/java/unittests/methods/UtilitiesTests.java b/src/test/java/unittests/methods/UtilitiesTests.java new file mode 100644 index 0000000..86e2482 --- /dev/null +++ b/src/test/java/unittests/methods/UtilitiesTests.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) MOBAC developers + * + * 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 2 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 . + ******************************************************************************/ +package unittests.methods; + +import junit.framework.TestCase; +import junit.textui.TestRunner; +import mobac.utilities.Utilities; + +public class UtilitiesTests extends TestCase { + + public void testParseSVNRevision() { + assertEquals(4168, Utilities.parseSVNRevision("4168")); + assertEquals(4168, Utilities.parseSVNRevision("4123:4168")); + assertEquals(4168, Utilities.parseSVNRevision("4168M")); + assertEquals(4168, Utilities.parseSVNRevision("4212:4168MS")); + assertEquals(4168, Utilities.parseSVNRevision("$Revision: 4168$")); + assertEquals(4168, Utilities.parseSVNRevision("$Rev: 4212:4168MS$")); + assertEquals(-1, Utilities.parseSVNRevision("exported")); + } + + public static void main(String[] args) { + TestRunner.run(UtilitiesTests.class); + } + +} diff --git a/src/test/resources/mobac/tools/testtileserver/servlets/images/cross.png b/src/test/resources/mobac/tools/testtileserver/servlets/images/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..e191f8fa1098104d6686520741692822baa84d23 GIT binary patch literal 2582 zcmeHI{ZmtC8hze3_a-C(iGmPuOQM6Ux(ZnxcX73HK|nyHXj4Rs7_@%Xtw}2iqnP`q z+AZ!_>8`eQwpOBcOD&GD3JRj+GPZ-=*%1`^Sa6f99}q=IcKP&P?!8O@jh&vEGv~~i zd471#JTuSnoa{9rnkWqbgk+?rYyd!07D13=O7>pPGfxS%D`{O4z~9QXlZ7;Z0&=p< zsn_b|+M9A?v)pt?cKsm#cu#I^mmhY^J#P7#PYw*rqp##COpX7HpBwo9YoK`P)tITR z)VB0Z`vImk|3p;(r@ezy;jE(6yrMU)JBx}7KiB~|g?siD#bu|OjR%*;EsI;yHMD0n zfb!#vlq7RW!Lxt$@9WJB|7d+*Y}4^=x8>nKoX(GVzh}YWiUpH}94QpuUQ(Gub~Q>3 zS4y{Bmv4Bqiu~(?{Ph}chtRMkGNQy|FDNEE=#$cHF0Q7pZrp>e>>cy;xFGjSt$RFD zoNlRWu(;$36fs+EeImDSncJ~GLoJfGFu$tv^GyzUCfOYvSzq-&0GyVx0)$TLTZ zCp#*X-E`cW{*x|YDV)--9O;K|B1&Gi;;co1hZU+bx!wjBm@aW^S+yn8(Iw60j^Rpa z30H*sB@OouJ}yOZ=BmeLcBbXRSL?63rkOrrZn}_Vh9isBa^bi^iY01Fa#yhPuC%Yd z|8(Ogl09i`^o>!s{rRx-4D+g!}AU;U>B5nMD?P+x#KS%z&?0Wz^J<2#PkUlt&PQD(w`c za#aS*=xhTmz|oU@5?D}+Jsiy3HG>kuZG$a{qe=w_7*W4H22!|2g9daqe+#bHPK+N# zXK4_3!$y>n_zP}5JSbP$!nlzzI7=~NFhnAABF!a1XDc#ky?h3wAxol`iy<#7k)Cpj zE1?dqR|Gl3VSFhvW1oE?Nd|W-tr3 zJK2;Wp%?|4#BTqGXR1`>i4?k``MiP%6Or?Dp@HR+N_|XG+RFYK--q&Hwu~`x!~8Fk zjS=1hVx&>0u@%VwrRSmz1ME%4$Q+XIM3gD+4$60NA-odfHySn0TDej~q0<$tiQWe9 zwz6e-4%FHE+$QXFzY!h;yPx-~1E`1JgNsO~{d1V@yj>B)C_f$9Pu0U!`yXK@)6=T6 z;Jbnvr@*?z0-|ybe5`GQT5&guAWeov46Yg3PA9;1`?o-IJ>}sR=oTE9f^z#=h~_E{ zv7idPSfh5LsdK;ZVWDiY0) z;)7%r-+_0M0=^FC5lbX;Uk<$&Ivp4??{u7iWd^MFb_u&NuaTYOy_&)r?7qGx7sq4_S`!2Q+%S`Gv7$* zFS*t+N>eU-iUe6|r3j?vO2iP1$#h`=55o4bjr3no+F-T17N6+dI(;{#Z*VPYQ<@I4 z>z!)Li1cSh%T0*1LrG@w!}tJM&R@fCkxhK4W9?)8sVHuxILo2ovcx@(C0xFk;h4+) zUR>aq!Ig??2j)(Q{r@zDns&(AF*$T4P9$~ gUmE|et9LB4D7g6Fru&U|etOXvso5!4k}V(p5Ajel&Hw-a literal 0 HcmV?d00001 diff --git a/src/test/resources/mobac/tools/testtileserver/servlets/images/gradient.png b/src/test/resources/mobac/tools/testtileserver/servlets/images/gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..6f53eddb1cd77fc66fcd237673533bec56617f95 GIT binary patch literal 4163 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/mobac/tools/testtileserver/servlets/images/tile.png b/src/test/resources/mobac/tools/testtileserver/servlets/images/tile.png new file mode 100644 index 0000000000000000000000000000000000000000..8afa1a1210869f2c32d6ac2afcc9f3403bed6478 GIT binary patch literal 80765 zcmV)lK%c*fP)N2bPDNB8 zb~7$DE-_B$IQako|BXpRK~#8N?0p4zRB5s{k=6)72X}XO4}{0q|cv_>aB={{_H*^vD0-H~(J%{8tD5 zqwm0f0q`IF@&EVD{}%xN)q(%$JMdosz{meT|MNe8`qQ6&{q@%`U%q_$^l55pYJ7Zr zWMpJ;aImkhue-auqobp>wY90KskXMZva+(YwDkS^_c=K^>FMcj-@c8Fjg5$i2nh)Z z3=H)1^Yixh_VV)bz%Tc|(*=LZ)6*0FtdEb6zrTM_P*7-SXjD{GLPA1HN=jyCW`2Ht zQBhG@Sy^>;b$xw(b8~ZBTU%#mXHQShz`(%p@bK8!*yQBohYug%4Z=Hx_xtBR{~6x+ zf3)`hM`eNF!$uc;@bK{?jfd;==g*)Mpc!<%9~v6!@9zh_Z*Ol0y>D!61ii1Sssi-_ zB?DE<&d$!t$^x}Z&&Z65P4x5&wXpZnHg(c4uv5~plF>Am(KM6Rz(h*j6c>16CIw$& zVonovb14w|m%sZ&TGLz>M9W;^jisudt(LKauDOf7tA9X9Wa8UocqQ<9;8g)83knK~ zi;F8NDr#zK00{tEcng4p-riooGeW||#00$Aj~_q68wbpz`+y*UP=T)h2yFa^c!2L! ze}>+Vj*fzIfUyKc>FVkN^9ia`S67Gh9#jex3)CwkBO^IEIWZBmE;%(VJtHeCC?d|p z&PP(inqOR>Pel8ofaaNps%IXkaz9i%^FR%Ie5g*7N9t!DX`FeifjvIbIQ>wQCh!~+ z%`-I7I`bG4Zu}9xiT_CB>|+g{CmI)?XdL<|@4c}GEB%{vAnF&D{ENnGH`^mmR)&OIhM56N*U zT(Iwe%s(cv@R<0bV`9rrim&65-hN4b{|(j0vKEH6zR~e-fr%v~CIBNsyeK|C9#9J3 z1jK$%jTm9T=n%jgz-@t>!-#?Z0^r~H4lr^aI@>{d4;%pO-wcxvycsMdnEdkc@`8fG z)b#AgxMZK;XwQI1SD#Qv&mcQje=8?%Q(JdqYgcP$Uq|my_n_D}R(=m=(*^uU1Cg_h-0!_ zoEei>@C3fZem^~A2xadJu0!I#9G6k;Z366F&Z=Is~c<>T+; z7w8ul5)c#?9upa%(v7t!N{|I3W% z|D*?CBs@sP#sVgFq{cVDQ>>uI+mWIYBn!atc8`Wo-0~B zQ!sxbYx+pq_<@ws6M6IJN;V=IPPZg1j^5W^!y`ZEkU0B(QR40wv$i9pf5E)-CG$>E zX1ZpBHiJ&@0ksB^z+{&s?Fsg!B-0*A<~>rF?4=3Q9x3=H_M_br%zIw3>=7sKeZ_WA zlIw`{f)jGPZfIV9Ve~@XPQ}ng*V5g>!OPLj$JW`)(!tZr*4^1NFf=kQEh7uw5jZi3 z5Fk{@&CLZcfMSKy4gtY{5efgs7V(EK{r?^SW@!Hp z6FYR4&zRK#Eddn)4h>A&KPXJg#F_t92W8m~%Pr>5h6u1 zyutq|4E!Gh06ob=&uJpg1D`0EH)Qp{PiSUk=Om}3#>6E=M#Y3jM23cjIk-J|XJD#)d6e8>rChig;?iOL)1qS}btQ{{wn83{M6a!1ojI$Ro@VMUN z<{arduH3lDF@t{ZmuJ}rqVSFES7bQ=qQr7g346pukr`zF3{ha-4|CJR$|* z!RE^f7X)=*s@Z6ny2-w=6;U+3FRml|+Q!a3I58yyOg_Z!kT4Ak3!4!kfO7z+2d)ns zAVmD&3;_lxa|%&1xB`?mMXmtB;QtyO`gaGwjO6AFy+<)F5FQ9{;iE-t2fRP9;ljc~ z&f_}X?BM46O3i}%f$E~eLh}y^a~%+7-zUPh{{_c^ zm$MIw0Sjl}FGk)Y#=29Kb(a`%k2rbXE6RQe@;(XTJ_*8JNy1(!*1gg!`(#*<((jYU zwH8-pnsO_%?8oJR3J5F6K~+o+sWA%6K{Z@dkuVGp3jEYZtSMVP4qv?453j7;*Iq(4B?&z@rga_zC zWdw*2z(#v-x>hlbA9mF^z;tBk_EXPi21<6f$D?71MiQe@YB%I*l}2o#l`^>md!+!X!>?Hm!=KV5 z9M&Ws)uJAM&2bW!6IvW6G!H$pkkIw@4t<-OS5R13TwYNL0S^Sea4R6w1?eDQKA`IW z0ML85C(+T-5F|i70dfF<6YvJ``zTq&NQ(ZiVc_2j0E~nO<@Fhp`Y5@H2oJLQD6Lsi zS_<|LN~J*bW8)G+!y`%0Ldcc4S*Cu?wB!QO3xHAR+0amqU3)g0Q|6;%hq-W{gCh1xAD3DLU-{|p&eJG?@1WFw(zoW30BZ^c_3|e z`hn)rJL>yxsP4F`bnuSmWf7BSN{*5`UQd;r_{6QwJT*G;$l%yRgQE`&4)YuAzpJ+&5jA!aEmyc5pg&r>~K=ZVfO=@RoBdCa~lzk84!+x z7!pnx5{?^9i!tGZG4Z4cO-{g<*pFCofnO#b(<2?zp&WZnJ_Z`EOFXVeIId5`M2~V( zfAIyw%{Pq?KQcQjY;#q@>8_&Z6LmiUtpFkIpqF}~ie_QD_Tg6U5#Ay3ac?v73ks{M zDggtiwFy)o1qQ&8=*d$=iolMb*kA@JqDRRnTl(+Ei~d~zFhl#1;fK2qRv(4AD8mDW z3Wd2y^^sx2%ePTMA>qFM!Ok9jmJS{UmQEH9o_;}*Nhui_S@{Nb0asrbEI9IF^Cj6E zVn(Vao<mzqH58P7c5ik%`b=I~CbMSquYabzN;QLJ3iC@<4 zmZa@9F`G*-tj-Hro)s`Z_0;Ud6Vv05O^-e@IU{I!P15n9il@k%0HN3Z&o%v?Y4~1} zbl?5Ze&Iz6BBtCXgp(kq3}Qw+WllP60Rm5mr_2Z^K}=aePME^)lTH{=j_a}=e@#81 z0~$|0X+%0@Ogaf-#D2zj-3{|&&#Z4r+g%a2JO12u2fy|DTUHxxTW!2!wfU~qNg>-i zGWOzHP6oEVzF`Sjc?I?Lz>9zs0SvxgaQ6TVzz-2Af-EY)0DcS%3iu>FVM_0@V=Nlb zUBSPHANV%{0OCBTeGTq7Xg}imP{BdTP4Lt}aN%=8_6!oR9`WYdPO~Wqy=j z`|#a22kyMycT0;`$VB{&m$^rLP;9PCP^!6m>>Ha971KaDLw^Y!-Ks-Ba2yNXs_L*getEH9_AwUe_`9g>J|>anCh3?bx}^DQ7TUC!C^- zCGoT+@r)Jej1`&NitU^g2ah$^d23D{E4H)nO-q0T`IHIuqyfiC9kx^Y)YC?k)28Gz z=H%06q|;{XXU#U;wmv0fe_z4*nuNpQr?%^ESkC7)<>WRZpVVhQqO;_J-d=vg%P)+@ zwHz(o!eUdhE1=n_qOzo{qNt?2xU{^iyrKeccLe}|>Z3>rl@0&{5J!P=0q+VhKrIz0 zVG3qwhAa5DX*K=V2LQ7Dbk(P~D}&*ut3KW8qdG1`y6~Byt~Ib>F|o1H(J_$YkBE%1 zarRZxwS6d|E23m-;@}q*pPFA#aVy1#9+m)`Zj6 zgfrGe5N;b1w+)%wntaxp?VR<(%eJd-Ic$C4wEm9cl56&}dF?3N)>LjwHf}SHGbZpz z<+dPmTLM}D8z9uPmP@bMY`<@JLdfy#OXu_AZWkpyxnH_(zi+?jvN`iH{dqhF>#rLe zxNmq<(nit5&m}M+Aw4f9IXfaDBP1p@E;%bJ_kAe<0(YU1DuvV#n12)%psX=^g1iBW z4Io4Y4-6j%f&pVg(Z4~_;9nd7jLe7L!G_w`5Zy;spE1J&2@Rkb{<;}9vilE zHt?I|v$iB|TR;x+3kN}W?`v?w! zo+*Ne3p_9wBNQJaS3qy7`&Y-nzdiuy*&by35$!>REeOjX(uEHRJ`b?>;IE$LESBk`;w>6{~($BA;@iF)3Vdftx81FCGj?V;0o zNw4Q`0&gn%9~Smram$V4qRX_nvhg~@@54VNo^=2raN85O?Ldea9&C3#^?a@!Y3G;W z8JZ<-7uV@k@z5yBlenXMy6?LDrpuhmF2r-r zL>?E?c^5LTD}~nu)S7bMk>i5Hs=KcH1ijBo2OJai-~8D77d{WRi*C5MgHU+g$md;2 zJTCBW;V%%*IT6l+U~mFFti9)XUOGhFC{ErY;igjXo@Z`zE?CavHC@1K^vikuEw@ZA zh}ns}_Izy}q-z&!=n!J+9O~#B6BwD2keZvFUtCyHURi~a9KZlL1kiE_4Cr^8-cSU$ z6t&jDub~_fqC;>)=p{4ykj=khSp2UE06O!bhrFQu$nZn#1@{^mBryD-;-KrG`t%D7 zKiDT=JxKde*&moId{h7!XWuB$`+e6HA-n%n-qOfEC?q;HEH*7XJ|i+A)7~de(a86X zl@Qdd3O>ohzI$C2btHM%LV*ErBmfEgGNMCZsu)XV z|GF6XmjwV~K8W{&Ie{BW*M5k=;O>JF0;>=9J~%iSW@J&P7k~l22@U~Lc;Ff6+D|X* zxCg|FYdRdht$OH|>U}A5H4`r<-x&DFW8P+GKQmj)tF?&xHRp*ptcSD- zM|6qD^ob`7DW`x#o6bIGw(^GEGo5JfxYDk^Ax7HsL)C}ZkH3Wv?cGD5{iy{FRzZbN z^pdv;1hZfAq~dx^1SRJ6Bwp|$UGSb3FY-k%>SZ5}Yrb53{+!qS*sl1JFL{$L(#41V z%ft)bL|!lKFJNH6@xbHGcEMxL70+Mzyq4ecSaZj9<2}di{I)yqTkXDYzW2V#spsZ5 zrR_yDJk-pBpi&l_`o65HF{hwBG%nN5E6U0(JR~+P?|mu6xM0_T;eci%SAgavz?8r( z1-(a&)^MvK@c`I>pTaLg6%w*TKvoe3z>59r=0^W@0Py|%3O(Ti(G_Svd=ij=Lyj0s zJD7Pi0|le#fB_iPLZlzGKP3gm72(QAOV3P8&q_(lN=nI$dz%@RkfCoE^ibA%51;z3 z>uQ&TjD%Gkv@HA`eB&Y$v)>g}ct>PN==vYJZ@S>5#_SWXDaUkJk3i)Win2zKXy-g< zzT~Ry`nyg?M7-s!lf%-gdizJ{q@S++NaJT*;}cWWO}&YE4X%+z^0pb5l;T!B2*MPX zE*E`>7kx<={b+K@kBEss0h0jy(I5W~O-PsgFroiG_HTV?e~lN%!+B2x2I>X31y@~G z-E!K>@38&8?XCw_dmfmdcy7ffnTFm~J;fwAB5--xU92A{|NdW0GNFWj-CPBn2bV1*t3H{gbCjr>M#Sr3;{mqNM zm|URYfpXD{>x#!hKKJFfTvy+AT6f2O;~ne$Pi!x}a(brfp==W95}2BvSJBbg)6~*l zSXz^m`93Z+Klgokb!{WudB6Z@Gjat82B6=70r)t8h=MCXQ=P$@4l(IhF5*w36%vWRaf(B?BIKI`l*qWo{xJ-YH?W|B<#iX zB6i%fqa4#GVND9qePBX3VM;tNxLr4H7$YX$uxa5mbsVm-G*SzLmcVBefc^RMmwg(PpMO_{#dny?RKv6FvzoM(V zue+zOxwWIZuBo!7p}wi5tsUDX0T=*U4;VnM0KtGB9D}__lXO6bz&3#&00RV1z)>@X z#b^MZo*_k~=wBj3`mX?h86{rE`4zb7DANzx52;Mx_sHtQ5+30F!SH{f;UQs>;gK;> zG4ZkSN%2W3v5Cns@yStfZ~a4J9X&!#Y$55Uew%kTj#l&yN%D=ocA^E;qL^sBRAb~WIbxcdd!6NI5Z%blTMja&scDrwLI|L zOT{iVSGztA2J`5_JP26V)DcI;qa#UBKN!4>zpSDe@0bv`cac1PY*)*t}tdg=L1R{$OWLN6eq@CeW8{iN`3=@T|HBIE!4g7NaVd3Xm{I(WS?aZxvP)-ZO}H1*Ij^VT%;)-d%} zH}z4}_mFz+^isw4nXJVfag)nJ24@~=pBFT_B4%?#+V!r2$3tb$XKJ1zTHbOdp|9=Z z&Al_EOybT<_$|L?$9l|^^|%@933JwymR!6xP_Ej1&zVm#*w{NKr=%Ivemvj*Tg+!< zY!b_Ob`LeQ4^%hzmDTrTmbMuO6y4NHT*mLqdEJNYdI0%a2&U-Q!pYYoXu=msrpt{e zGA^Va3HlNF2KxO7t~-&-A4hKyjyWI^cT_t5w0t6ua?&yBxE;cgD<1?ex*f3OZou+; z0jur@EWPWu@TT|NYo5z)dF*=XaaP>(k(!?|q;#_@yZZ(peF>cNdl6hMH?m5prZ_Rq# zob`kS>q#rtQ`U>GIqrPye(sg8m{F`t#QUP^_G#Pyn*it=8UxiYuI%^2`HjGL;73&ak#aMd zd^7r&hp{`w5_nbKJ~l`dG0%{;℞De_@_+RW*K}Na%)#0b8F2>=q0>@FHmQ(?CGN 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 W{8A4$MQs4!A4OR;nJOuZp#K9Nm2GbT literal 0 HcmV?d00001 diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..3e7a098 --- /dev/null +++ b/start.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# If you see this file in an editor you may have to set the executable bit and execute it as script in a terminal + +# This file will start the Mobile Atlas Creator with custom memory settings for +# the JVM. With the below settings the heap size (Available memory for the application) +# will range from 64 megabyte up to 1024 megabyte. + +java -Xms64m -Xmx1024M -jar Mobile_Atlas_Creator.jar