2023-03-18 18:38:04 +00:00
package com.onthegomap.planetiler.archive ;
import static com.onthegomap.planetiler.util.LanguageUtils.nullIfEmpty ;
import com.onthegomap.planetiler.config.Arguments ;
2024-01-03 01:37:49 +00:00
import com.onthegomap.planetiler.files.FilesArchiveUtils ;
2024-01-10 10:21:03 +00:00
import com.onthegomap.planetiler.geo.TileOrder ;
2024-01-03 01:37:49 +00:00
import com.onthegomap.planetiler.stream.StreamArchiveUtils ;
2023-03-18 18:38:04 +00:00
import com.onthegomap.planetiler.util.FileUtils ;
2024-01-03 01:37:49 +00:00
import java.io.IOException ;
import java.io.UncheckedIOException ;
2023-03-18 18:38:04 +00:00
import java.net.URI ;
import java.net.URLDecoder ;
import java.nio.charset.StandardCharsets ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.util.HashMap ;
import java.util.Map ;
2024-01-03 01:37:49 +00:00
import java.util.stream.Stream ;
2023-03-18 18:38:04 +00:00
/ * *
* Definition for a tileset , parsed from a URI - like string .
* < p >
* { @link # from ( String ) } can accept :
* < ul >
* < li > A platform - specific absolute or relative path like { @code "./archive.mbtiles" } or
* { @code "C:\root\archive.mbtiles" } < / li >
* < li > A URI pointing at a file , like { @code "file:///root/archive.pmtiles" } or
* { @code "file:///C:/root/archive.pmtiles" } < / li >
* < / ul >
* < p >
* Both of these can also have archive - specific options added to the end , for example
* { @code "output.mbtiles?compact=false&page_size=16384" } .
*
* @param format The { @link Format format } of the archive , either inferred from the filename extension or the
* { @code ? format = } query parameter
* @param scheme Scheme for accessing the archive
* @param uri Full URI including scheme , location , and options
* @param options Parsed query parameters from the definition string
* /
public record TileArchiveConfig (
Format format ,
Scheme scheme ,
URI uri ,
Map < String , String > options
) {
2024-01-03 01:37:49 +00:00
// be more generous and encode some characters for the users
private static final Map < String , String > URI_ENCODINGS = Map . of (
"{" , "%7B" ,
"}" , "%7D"
) ;
2023-03-18 18:38:04 +00:00
private static TileArchiveConfig . Scheme getScheme ( URI uri ) {
String scheme = uri . getScheme ( ) ;
if ( scheme = = null ) {
return Scheme . FILE ;
}
for ( var value : TileArchiveConfig . Scheme . values ( ) ) {
if ( value . id ( ) . equals ( scheme ) ) {
return value ;
}
}
throw new IllegalArgumentException ( "Unsupported scheme " + scheme + " from " + uri ) ;
}
private static String getExtension ( URI uri ) {
String path = uri . getPath ( ) ;
if ( path ! = null & & ( path . contains ( "." ) ) ) {
return nullIfEmpty ( path . substring ( path . lastIndexOf ( "." ) + 1 ) ) ;
}
return null ;
}
private static Map < String , String > parseQuery ( URI uri ) {
String query = uri . getRawQuery ( ) ;
Map < String , String > result = new HashMap < > ( ) ;
if ( query ! = null ) {
for ( var part : query . split ( "&" ) ) {
var split = part . split ( "=" , 2 ) ;
result . put (
URLDecoder . decode ( split [ 0 ] , StandardCharsets . UTF_8 ) ,
split . length = = 1 ? "true" : URLDecoder . decode ( split [ 1 ] , StandardCharsets . UTF_8 )
) ;
}
}
return result ;
}
private static TileArchiveConfig . Format getFormat ( URI uri ) {
String format = parseQuery ( uri ) . get ( "format" ) ;
2024-01-03 01:37:49 +00:00
for ( var value : TileArchiveConfig . Format . values ( ) ) {
if ( value . isQueryFormatSupported ( format ) ) {
return value ;
}
2023-03-18 18:38:04 +00:00
}
2024-01-03 01:37:49 +00:00
if ( format ! = null ) {
throw new IllegalArgumentException ( "Unsupported format " + format + " from " + uri ) ;
2023-03-18 18:38:04 +00:00
}
for ( var value : TileArchiveConfig . Format . values ( ) ) {
2024-01-03 01:37:49 +00:00
if ( value . isUriSupported ( uri ) ) {
2023-03-18 18:38:04 +00:00
return value ;
}
}
2024-01-03 01:37:49 +00:00
throw new IllegalArgumentException ( "Unsupported format " + getExtension ( uri ) + " from " + uri ) ;
2023-03-18 18:38:04 +00:00
}
/ * *
* Parses a string definition of a tileset from a URI - like string .
* /
public static TileArchiveConfig from ( String string ) {
// unix paths parse fine as URIs, but need to explicitly parse windows paths with backslashes
if ( string . contains ( "\\" ) ) {
String [ ] parts = string . split ( "\\?" , 2 ) ;
string = Path . of ( parts [ 0 ] ) . toUri ( ) . toString ( ) ;
if ( parts . length > 1 ) {
string + = "?" + parts [ 1 ] ;
}
}
2024-01-03 01:37:49 +00:00
for ( Map . Entry < String , String > uriEncoding : URI_ENCODINGS . entrySet ( ) ) {
string = string . replace ( uriEncoding . getKey ( ) , uriEncoding . getValue ( ) ) ;
}
2023-03-18 18:38:04 +00:00
return from ( URI . create ( string ) ) ;
}
/ * *
* Parses a string definition of a tileset from a URI .
* /
public static TileArchiveConfig from ( URI uri ) {
if ( uri . getScheme ( ) = = null ) {
2024-01-03 01:37:49 +00:00
final String path = uri . getPath ( ) ;
String base = Path . of ( path ) . toAbsolutePath ( ) . toUri ( ) . normalize ( ) . toString ( ) ;
if ( path . endsWith ( "/" ) ) {
base = base + "/" ;
}
2023-03-18 18:38:04 +00:00
if ( uri . getRawQuery ( ) ! = null ) {
base + = "?" + uri . getRawQuery ( ) ;
}
uri = URI . create ( base ) ;
}
return new TileArchiveConfig (
getFormat ( uri ) ,
getScheme ( uri ) ,
uri ,
parseQuery ( uri )
) ;
}
/ * *
* Returns the local path on disk that this archive reads / writes to , or { @code null } if it is not on disk ( ie . an HTTP
* repository ) .
* /
public Path getLocalPath ( ) {
return scheme = = Scheme . FILE ? Path . of ( URI . create ( uri . toString ( ) . replaceAll ( "\\?.*$" , "" ) ) ) : null ;
}
2024-01-03 01:37:49 +00:00
/ * *
* Returns the local < b > base < / b > path for this archive , for which directories should be pre - created for .
* /
public Path getLocalBasePath ( ) {
Path p = getLocalPath ( ) ;
if ( format ( ) = = Format . FILES ) {
p = FilesArchiveUtils . cleanBasePath ( p ) ;
}
return p ;
}
2023-03-18 18:38:04 +00:00
/ * *
* Deletes the archive if possible .
* /
public void delete ( ) {
if ( scheme = = Scheme . FILE ) {
2024-01-03 01:37:49 +00:00
FileUtils . delete ( getLocalBasePath ( ) ) ;
2023-03-18 18:38:04 +00:00
}
}
/ * *
* Returns { @code true } if the archive already exists , { @code false } otherwise .
* /
public boolean exists ( ) {
2024-01-03 01:37:49 +00:00
return exists ( getLocalBasePath ( ) ) ;
}
/ * *
* @param p path to the archive
* @return { @code true } if the archive already exists , { @code false } otherwise .
* /
public boolean exists ( Path p ) {
if ( p = = null ) {
return false ;
}
if ( format ( ) ! = Format . FILES ) {
return Files . exists ( p ) ;
} else {
if ( ! Files . exists ( p ) ) {
return false ;
}
// file-archive exists only if it has any contents
try ( Stream < Path > paths = Files . list ( p ) ) {
return paths . findAny ( ) . isPresent ( ) ;
} catch ( IOException e ) {
throw new UncheckedIOException ( e ) ;
}
}
2023-03-18 18:38:04 +00:00
}
/ * *
* Returns the current size of this archive .
* /
public long size ( ) {
return getLocalPath ( ) = = null ? 0 : FileUtils . size ( getLocalPath ( ) ) ;
}
/ * *
* Returns an { @link Arguments } instance that returns the value for options directly from the query parameters in the
* URI , or from { @code arguments } prefixed by { @code "format_" } .
* /
public Arguments applyFallbacks ( Arguments arguments ) {
return Arguments . of ( options ) . orElse ( arguments . withPrefix ( format . id ) ) ;
}
2024-01-03 01:37:49 +00:00
public Path getPathForMultiThreadedWriter ( int index ) {
return switch ( format ) {
case CSV , TSV , JSON , PROTO , PBF - > StreamArchiveUtils . constructIndexedPath ( getLocalPath ( ) , index ) ;
case FILES - > getLocalPath ( ) ;
default - > throw new UnsupportedOperationException ( "not supported by " + format ) ;
} ;
}
2023-03-18 18:38:04 +00:00
public enum Format {
2023-08-24 00:24:27 +00:00
MBTILES ( "mbtiles" ,
false /* TODO mbtiles could support append in the future by using insert statements with an "on conflict"-clause (i.e. upsert) and by creating tables only if they don't exist, yet */ ,
2024-01-10 10:21:03 +00:00
false , TileOrder . TMS ) ,
PMTILES ( "pmtiles" , false , false , TileOrder . HILBERT ) ,
2023-08-24 00:24:27 +00:00
2024-01-03 01:37:49 +00:00
// should be before PBF in order to avoid collisions
2024-01-10 10:21:03 +00:00
FILES ( "files" , true , true , TileOrder . TMS ) {
2024-01-03 01:37:49 +00:00
@Override
boolean isUriSupported ( URI uri ) {
final String path = uri . getPath ( ) ;
return path ! = null & & ( path . endsWith ( "/" ) | | path . contains ( "{" ) /* template string */ | |
! path . contains ( "." ) /* no extension => assume files */ ) ;
}
} ,
2024-01-10 10:21:03 +00:00
CSV ( "csv" , true , true , TileOrder . TMS ) ,
2023-08-24 00:24:27 +00:00
/** identical to {@link Format#CSV} - except for the column separator */
2024-01-10 10:21:03 +00:00
TSV ( "tsv" , true , true , TileOrder . TMS ) ,
2023-08-24 00:24:27 +00:00
2024-01-10 10:21:03 +00:00
PROTO ( "proto" , true , true , TileOrder . TMS ) ,
2023-08-24 00:24:27 +00:00
/** identical to {@link Format#PROTO} */
2024-01-10 10:21:03 +00:00
PBF ( "pbf" , true , true , TileOrder . TMS ) ,
2023-08-24 00:24:27 +00:00
2024-01-10 10:21:03 +00:00
JSON ( "json" , true , true , TileOrder . TMS ) ;
2023-03-18 18:38:04 +00:00
private final String id ;
2023-08-24 00:24:27 +00:00
private final boolean supportsAppend ;
private final boolean supportsConcurrentWrites ;
2024-01-10 10:21:03 +00:00
private final TileOrder order ;
2023-03-18 18:38:04 +00:00
2024-01-10 10:21:03 +00:00
Format ( String id , boolean supportsAppend , boolean supportsConcurrentWrites , TileOrder order ) {
2023-03-18 18:38:04 +00:00
this . id = id ;
2023-08-24 00:24:27 +00:00
this . supportsAppend = supportsAppend ;
this . supportsConcurrentWrites = supportsConcurrentWrites ;
2024-01-10 10:21:03 +00:00
this . order = order ;
}
public TileOrder preferredOrder ( ) {
return order ;
2023-03-18 18:38:04 +00:00
}
public String id ( ) {
return id ;
}
2023-08-24 00:24:27 +00:00
public boolean supportsAppend ( ) {
return supportsAppend ;
}
public boolean supportsConcurrentWrites ( ) {
return supportsConcurrentWrites ;
}
2024-01-03 01:37:49 +00:00
boolean isUriSupported ( URI uri ) {
final String path = uri . getPath ( ) ;
return path ! = null & & path . endsWith ( "." + id ) ;
}
boolean isQueryFormatSupported ( String queryFormat ) {
return id . equals ( queryFormat ) ;
}
2023-03-18 18:38:04 +00:00
}
public enum Scheme {
FILE ( "file" ) ;
private final String id ;
Scheme ( String id ) {
this . id = id ;
}
public String id ( ) {
return id ;
}
}
}