/*
* Copyright (C) 2007 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* Author: Steve Ratcliffe
* Create date: Nov 15, 2007
*/
package uk.me.parabola.mkgmap.combiners;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import uk.me.parabola.imgfmt.FileExistsException;
import uk.me.parabola.imgfmt.FileNotWritableException;
import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.mdr.MdrConfig;
import uk.me.parabola.imgfmt.app.srt.SRTFile;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.imgfmt.fs.FileSystem;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.imgfmt.mps.MapBlock;
import uk.me.parabola.imgfmt.mps.MpsFile;
import uk.me.parabola.imgfmt.mps.MpsFileReader;
import uk.me.parabola.imgfmt.mps.ProductBlock;
import uk.me.parabola.imgfmt.sys.FileImgChannel;
import uk.me.parabola.imgfmt.sys.FileLink;
import uk.me.parabola.imgfmt.sys.ImgFS;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
/**
* Create the gmapsupp file. There is nothing much special about this file
* (as far as I know - there's not a public official spec or anything) it is
* just a regular .img file which is why it works to rename a single .img file
* and send it to the device.
* <p/>
* Effectively we just 'unzip' the constituent .img files and then 'zip' them
* back into the gmapsupp.img file.
* <p/>
* In addition we need to create and add the MPS file, if we don't already
* have one.
*
* @author Steve Ratcliffe
*/
public class GmapsuppBuilder
implements Combiner
{
private static final String GMAPSUPP =
"gmapsupp.img";
private final Map<String, FileInfo
> files =
new LinkedHashMap<>();
// all these need to be set in the init routine from arguments.
private String areaName
;
private String mapsetName
;
private String overallDescription =
"Combined map";
private String outputDir
;
private MpsFile mpsFile
;
private boolean createIndex
; // True if we should create and add an index file
// There is a separate MDR and SRT file for each family id in the gmapsupp
private final Map<Integer, MdrBuilder
> mdrBuilderMap =
new LinkedHashMap<>();
private final Map<Integer, Sort
> sortMap =
new LinkedHashMap<>();
private MdrConfig mdrConfig
; // one base config for all
private boolean hideGmapsuppOnPC
;
private int productVersion
;
private FileSystem imgFs
;
public void init
(CommandArgs args
) {
areaName = args.
get("area-name",
null);
mapsetName = args.
get("mapset-name",
"OSM map set");
overallDescription = args.
getDescription();
outputDir = args.
getOutputDir();
hideGmapsuppOnPC = args.
get("hide-gmapsupp-on-pc",
false);
productVersion = args.
get("product-version",
100);
mdrConfig =
new MdrConfig
();
mdrConfig.
setIndexOptions(args
);
try {
imgFs = createGmapsupp
();
} catch (FileNotWritableException e
) {
throw new MapFailedException
("Could not create gmapsupp.img file");
}
}
/**
* Add or retrieve the MDR file for the given familyId.
* @param familyId The family id to create the mdr file for.
* @param sort The sort for this family id.
* @return If there is already an mdr file for this family then it is returned, else the newly created
* one.
*/
private MdrBuilder addMdrFile
(int familyId, Sort sort
) {
MdrBuilder mdrBuilder = mdrBuilderMap.
get(familyId
);
if (mdrBuilder
!=
null)
return mdrBuilder
;
mdrBuilder =
new MdrBuilder
();
try {
String imgname =
String.
format("%08d.MDR", familyId
);
ImgChannel chan = imgFs.
create(imgname
);
mdrBuilder.
initForDevice(chan, sort, mdrConfig
);
} catch (FileExistsException e
) {
Logger.
defaultLogger.
error("Could not create duplicate MDR file");
}
mdrBuilderMap.
put(familyId, mdrBuilder
);
return mdrBuilder
;
}
/**
* Add the sort file for the given family id.
*/
private void addSrtFile
(int familyId, FileInfo info
) {
Sort prevSort = sortMap.
get(familyId
);
Sort sort = info.
getSort();
if (prevSort ==
null) {
if (info.
getKind() == FileKind.
IMG_KIND) {
sortMap.
put(familyId, sort
);
}
} else {
if (prevSort.
getCodepage() != sort.
getCodepage())
Logger.
defaultLogger.
warn("Input file '" + info.
getFilename() +
"' has a different code page (" + sort.
getCodepage() +
" rather than " + prevSort.
getCodepage() +
")");
if (info.
hasSortOrder() && prevSort.
getSortOrderId() != sort.
getSortOrderId())
Logger.
defaultLogger.
warn("Input file '" + info.
getFilename() +
"' has a different sort order (" + sort.
getSortOrderId() +
" rather than " + prevSort.
getSortOrderId() +
")");
}
}
/**
* This is called when the map is complete. We collect information about the map to be used in the TDB file and for
* preparing the gmapsupp file.
*
* @param info Information about the img file.
*/
public void onMapEnd
(FileInfo info
) {
files.
put(info.
getFilename(), info
);
if (info.
isImg()) {
int familyId = info.
getFamilyId();
if (createIndex
) {
MdrBuilder mdrBuilder = addMdrFile
(familyId, info.
getSort());
mdrBuilder.
onMapEnd(info
);
}
addSrtFile
(familyId, info
);
}
}
/**
* The complete map set has been processed. Creates the gmapsupp file. This is done by stepping through each img file,
* reading all the sub files and copying them into the gmapsupp file.
*/
public void onFinish
() {
for (MdrBuilder mdrBuilder : mdrBuilderMap.
values()) {
mdrBuilder.
onFinishForDevice();
}
try {
addAllFiles
(imgFs
);
writeSrtFile
(imgFs
);
writeMpsFile
();
} catch (FileNotWritableException e
) {
Logger.
defaultLogger.
error("Could not create gmapsupp file");
} finally {
Utils.
closeFile(imgFs
);
}
}
/**
* Write the SRT file.
*
* @param imgFs The filesystem to create the SRT file in.
* @throws FileNotWritableException If it cannot be created.
*/
private void writeSrtFile
(FileSystem imgFs
) throws FileNotWritableException
{
for (Map.Entry<Integer, Sort
> ent : sortMap.
entrySet()) {
Sort sort = ent.
getValue();
int familyId = ent.
getKey();
if (sort.
getId1() ==
0 && sort.
getId2() ==
0)
return;
try {
ImgChannel channel = imgFs.
create(String.
format("%08d.SRT", familyId
));
SRTFile srtFile =
new SRTFile
(channel
);
srtFile.
setSort(sort
);
srtFile.
write();
// Do not close srtFile here
} catch (FileExistsException e
) {
// well it shouldn't exist!
Logger.
defaultLogger.
error("could not create SRT file as it exists already");
throw new FileNotWritableException
("already existed", e
);
}
}
}
/**
* Write the MPS file. The gmapsupp file will work without this, but it important if you want to include more than one
* map family and be able to turn them on and off separately.
*/
private void writeMpsFile
() throws FileNotWritableException
{
try {
mpsFile.
sync();
} catch (IOException e
) {
throw new FileNotWritableException
("Could not finish write to MPS file", e
);
}
}
private MapBlock makeMapBlock
(FileInfo info
) {
MapBlock mb =
new MapBlock
(info.
getCodePage());
mb.
setMapNumber(info.
getMapnameAsInt());
mb.
setHexNumber(info.
getHexname());
mb.
setMapDescription(info.
getDescription());
mb.
setAreaName(areaName
!=
null ? areaName :
"Area " + info.
getMapname());
mb.
setSeriesName(info.
getSeriesName());
mb.
setIds(info.
getFamilyId(), info.
getProductId());
return mb
;
}
private static ProductBlock makeProductBlock
(FileInfo info
) {
ProductBlock pb =
new ProductBlock
(info.
getCodePage());
pb.
setFamilyId(info.
getFamilyId());
pb.
setProductId(info.
getProductId());
pb.
setDescription(info.
getFamilyName());
return pb
;
}
private void addAllFiles
(FileSystem outfs
) {
for (FileInfo info : files.
values()) {
switch (info.
getKind()) {
case IMG_KIND:
addImg
(outfs, info
);
addMpsEntry
(info
);
break;
case GMAPSUPP_KIND:
addImg
(outfs, info
);
addMpsFile
(info
);
break;
case APP_KIND:
case TYP_KIND:
addFile
(outfs, info
);
break;
default:
break;
}
}
}
private static void addImg
(FileSystem outfs, FileInfo info
) {
FileCopier fc =
new FileCopier
(info.
getFilename());
List<SubFileInfo
> subFiles = info.
subFiles();
for (SubFileInfo sf : subFiles
) {
try {
ImgChannel chan = outfs.
create(sf.
getName());
Closeable sync = fc.
add(sf.
getName(), chan
);
((FileLink
)chan
).
link(sf, sync
);
} catch (FileExistsException e
) {
Logger.
defaultLogger.
warn("Could not copy " + sf.
getName(), e
);
}
}
}
private static void addFile
(FileSystem outfs, FileInfo info
) {
String filename = info.
getFilename();
FileCopier fc =
new FileCopier
(filename
);
try {
ImgChannel chan = outfs.
create(createImgFilename
(filename
));
((FileLink
) chan
).
link(info.
subFiles().
get(0), fc.
file(chan
));
} catch (FileExistsException e
) {
Logger.
defaultLogger.
warn("Could not copy " + filename, e
);
}
}
/**
* Add a complete pre-existing mps file to the mps file we are currently
* building for this gmapsupp.
* @param info The details of the gmapsupp file that we need to extract the
*/
private void addMpsFile
(FileInfo info
) {
String name = info.
getFilename();
try (FileSystem fs = ImgFS.
openFs(name
)) {
MpsFileReader mr =
new MpsFileReader
(fs.
open(info.
getMpsName(),
"r"), info.
getCodePage());
for (MapBlock block : mr.
getMaps())
mpsFile.
addMap(block
);
for (ProductBlock b : mr.
getProducts())
mpsFile.
addProduct(b
);
mr.
close();
} catch (IOException e
) {
Logger.
defaultLogger.
error("Could not read MPS file from gmapsupp", e
);
}
}
/**
* Add a single entry to the mps file.
* @param info The img file information.
*/
private void addMpsEntry
(FileInfo info
) {
mpsFile.
addMap(makeMapBlock
(info
));
// Add a new product block if we have found a new product
mpsFile.
addProduct(makeProductBlock
(info
));
}
private static MpsFile createMpsFile
(FileSystem outfs
) throws FileNotWritableException
{
try {
ImgChannel channel = outfs.
create("MAKEGMAP.MPS");
return new MpsFile
(channel
);
} catch (FileExistsException e
) {
// well it shouldn't exist!
Logger.
defaultLogger.
error("could not create MPS file as it already exists");
throw new FileNotWritableException
("already existed", e
);
}
}
/**
* Create a suitable filename for use in the .img file from the external
* file name.
*
* The external file name might look something like /home/steve/foo.typ
* or c:\maps\foo.typ and we need to take the filename part and make
* sure that it is no more than 8+3 characters.
*
* @param pathname The external filesystem path name.
* @return The filename part, will be restricted to 8+3 characters and all
* in upper case.
*/
private static String createImgFilename
(String pathname
) {
File f =
new File(pathname
);
String name = f.
getName().
toUpperCase(Locale.
ENGLISH);
int dot = name.
lastIndexOf('.');
String base = name.
substring(0, dot
);
if (base.
length() > 8)
base = base.
substring(0,
8);
String ext = name.
substring(dot +
1);
if (ext.
length() > 3)
ext = ext.
substring(0,
3);
return base +
'.' + ext
;
}
/**
* Create the output file.
*
* @return The gmapsupp file.
* @throws FileNotWritableException If it cannot be created for any reason.
*/
private FileSystem createGmapsupp
() throws FileNotWritableException
{
// Create this file, containing all the sub files
FileSystemParam params =
new FileSystemParam
();
params.
setMapDescription(overallDescription
);
params.
setGmapsupp(true);
params.
setHideGmapsuppOnPC(hideGmapsuppOnPC
);
params.
setProductVersion(productVersion
);
FileSystem outfs = ImgFS.
createFs(Utils.
joinPath(outputDir, GMAPSUPP
), params
);
mpsFile = createMpsFile
(outfs
);
mpsFile.
setMapsetName(mapsetName
);
return outfs
;
}
public void setCreateIndex
(boolean create
) {
this.
createIndex = create
;
}
}
/**
* Copies files from the source img to the gmapsupp.
*
* Each sub file has to be copied separately to a different 'file'. This class makes sure
* that the source file is only opened once.
*/
class FileCopier
{
private final String filename
;
private FileSystem fs
;
private int refCount
;
public FileCopier
(String filename
) {
this.
filename = filename
;
}
Closeable add
(String name, ImgChannel fout
) {
refCount++
;
return () -
> sync
(name, fout
);
}
Closeable file
(ImgChannel fout
) {
return () -
> sync
(fout
);
}
/**
* This version of sync() is used for single files.
*/
public void sync
(ImgChannel fout
) throws IOException {
try (ImgChannel fin =
new FileImgChannel
(filename,
"r")) {
copyFile
(fin, fout
);
}
}
/**
* This version of sync is used for subfiles within a .img file.
* @param name The sub file name.
* @param fout Where to copy the file
*/
void sync
(String name, ImgChannel fout
) throws IOException {
if (fs ==
null)
fs = ImgFS.
openFs(filename
);
copyFile
(name, fout
);
refCount--
;
if (refCount
<=
0) {
fs.
close();
}
}
private void copyFile
(String name, ImgChannel fout
) throws IOException {
try (ImgChannel fin = fs.
open(name,
"r")) {
copyFile
(fin, fout
);
}
}
private static void copyFile
(ImgChannel fin, ImgChannel fout
) throws IOException {
ByteBuffer buf =
ByteBuffer.
allocate(1024);
while (fin.
read(buf
) > 0) {
buf.
flip();
fout.
write(buf
);
buf.
compact();
}
}
}