/*
* Copyright (C) 2007 - 2012.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* 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.
*/
package uk.me.parabola.mkgmap.build;
import java.awt.Rectangle;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.MapTooBigException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.Exit;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.dem.DEMFile;
import uk.me.parabola.imgfmt.app.lbl.City;
import uk.me.parabola.imgfmt.app.lbl.Country;
import uk.me.parabola.imgfmt.app.lbl.ExitFacility;
import uk.me.parabola.imgfmt.app.lbl.Highway;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.imgfmt.app.lbl.POIRecord;
import uk.me.parabola.imgfmt.app.lbl.Region;
import uk.me.parabola.imgfmt.app.lbl.Zip;
import uk.me.parabola.imgfmt.app.map.Map;
import uk.me.parabola.imgfmt.app.net.NETFile;
import uk.me.parabola.imgfmt.app.net.NODFile;
import uk.me.parabola.imgfmt.app.net.Numbers;
import uk.me.parabola.imgfmt.app.net.RoadDef;
import uk.me.parabola.imgfmt.app.net.RoadNetwork;
import uk.me.parabola.imgfmt.app.net.RouteCenter;
import uk.me.parabola.imgfmt.app.trergn.ExtTypeAttributes;
import uk.me.parabola.imgfmt.app.trergn.Overview;
import uk.me.parabola.imgfmt.app.trergn.Point;
import uk.me.parabola.imgfmt.app.trergn.PointOverview;
import uk.me.parabola.imgfmt.app.trergn.Polygon;
import uk.me.parabola.imgfmt.app.trergn.PolygonOverview;
import uk.me.parabola.imgfmt.app.trergn.Polyline;
import uk.me.parabola.imgfmt.app.trergn.PolylineOverview;
import uk.me.parabola.imgfmt.app.trergn.RGNFile;
import uk.me.parabola.imgfmt.app.trergn.RGNHeader;
import uk.me.parabola.imgfmt.app.trergn.Subdivision;
import uk.me.parabola.imgfmt.app.trergn.TREFile;
import uk.me.parabola.imgfmt.app.trergn.TREHeader;
import uk.me.parabola.imgfmt.app.trergn.Zoom;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.Version;
import uk.me.parabola.mkgmap.filters.BaseFilter;
import uk.me.parabola.mkgmap.filters.DouglasPeuckerFilter;
import uk.me.parabola.mkgmap.filters.FilterConfig;
import uk.me.parabola.mkgmap.filters.LineMergeFilter;
import uk.me.parabola.mkgmap.filters.LinePreparerFilter;
import uk.me.parabola.mkgmap.filters.LineSplitterFilter;
import uk.me.parabola.mkgmap.filters.MapFilter;
import uk.me.parabola.mkgmap.filters.MapFilterChain;
import uk.me.parabola.mkgmap.filters.PolygonSplitterFilter;
import uk.me.parabola.mkgmap.filters.RemoveEmpty;
import uk.me.parabola.mkgmap.filters.RemoveObsoletePointsFilter;
import uk.me.parabola.mkgmap.filters.RoundCoordsFilter;
import uk.me.parabola.mkgmap.filters.ShapeMergeFilter;
import uk.me.parabola.mkgmap.filters.ShapeMergeFilter.MapShapeComparator;
import uk.me.parabola.mkgmap.filters.SizeFilter;
import uk.me.parabola.mkgmap.general.CityInfo;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.LoadableMapDataSource;
import uk.me.parabola.mkgmap.general.MapDataSource;
import uk.me.parabola.mkgmap.general.MapElement;
import uk.me.parabola.mkgmap.general.MapExitPoint;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.general.MapRoad;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.general.ZipCodeInfo;
import uk.me.parabola.mkgmap.reader.MapperBasedMapDataSource;
import uk.me.parabola.mkgmap.reader.hgt.HGTConverter;
import uk.me.parabola.mkgmap.reader.hgt.HGTConverter.InterpolationMethod;
import uk.me.parabola.mkgmap.reader.hgt.HGTReader;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.mkgmap.reader.osm.GeneralRelation;
import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.mkgmap.reader.overview.OverviewMapDataSource;
import uk.me.parabola.util.Configurable;
import uk.me.parabola.util.EnhancedProperties;
import uk.me.parabola.util.Java2DConverter;
import uk.me.parabola.util.ShapeSplitter;
/**
* This is the core of the code to translate from the general representation
* into the garmin representation.
*
* We need to go through the data several times, once for each level, filter
* out features that are not required at the level and simplify paths for
* lower resolutions if required.
*
* @author Steve Ratcliffe
*/
public class MapBuilder
implements Configurable
{
private static final Logger log =
Logger.
getLogger(MapBuilder.
class);
private static final int CLEAR_TOP_BITS =
(32 -
15);
private static final LocalDateTime now = LocalDateTime.
now();
private static final int MIN_SIZE_LINE =
1;
private final boolean isOverviewComponent
;
private final boolean isOverviewCombined
;
private final java.
util.
Map<MapPoint,POIRecord
> poimap =
new HashMap<>();
private final java.
util.
Map<MapPoint,City
> cityMap =
new HashMap<>();
private List<String> mapInfo =
new ArrayList<>();
private List<String> copyrights =
new ArrayList<>();
private boolean hasNet
;
private Boolean driveOnLeft
; // needs to be Boolean for later null test
private Locator locator
;
private final java.
util.
Map<String, Highway
> highways =
new HashMap<>();
/** name that is used for cities which name are unknown */
private static final String UNKNOWN_CITY_NAME =
"";
private Country defaultCountry
;
private String countryName =
"COUNTRY";
private String countryAbbr =
"ABC";
private String regionName
;
private String regionAbbr
;
private Set<String> locationAutofill
;
private int minSizePolygon
;
private String polygonSizeLimitsOpt
;
private TreeMap<Integer,
Integer> polygonSizeLimits
;
private TreeMap<Integer,
Double> dpFilterLineResMap
;
private TreeMap<Integer,
Double> dpFilterShapeResMap
;
private boolean mergeLines
;
private boolean mergeShapes
;
private boolean poiAddresses
;
private int poiDisplayFlags
;
private boolean enableLineCleanFilters =
true;
private boolean makePOIIndex
;
private int routeCenterBoundaryType
;
private LBLFile lblFile
;
private String licenseFileName
;
private boolean orderByDecreasingArea
;
private String pathsToHGT
;
private List<Integer> demDists
;
private short demOutsidePolygonHeight
;
private java.
awt.
geom.
Area demPolygon
;
private HGTConverter.
InterpolationMethod demInterpolationMethod
;
private boolean allowReverseMerge
;
private boolean improveOverview
;
/**
* Construct a new MapBuilder.
*
* @param overviewComponent set to {@code true} if the map is a work file that
* is later used as input for the OverviewBuilder
* @param overviewCombined set to {@code true} if the map is the combined
* overview map
*/
public MapBuilder
(boolean overviewComponent,
boolean overviewCombined
) {
regionName =
null;
locationAutofill =
Collections.
emptySet();
locator =
new Locator();
this.
isOverviewComponent = overviewComponent
;
this.
isOverviewCombined = overviewCombined
;
}
public void config
(EnhancedProperties props
) {
countryName = props.
getProperty("country-name", countryName
);
countryAbbr = props.
getProperty("country-abbr", countryAbbr
);
regionName = props.
getProperty("region-name",
null);
regionAbbr = props.
getProperty("region-abbr",
null);
minSizePolygon = props.
getProperty("min-size-polygon",
8);
polygonSizeLimitsOpt = props.
getProperty("polygon-size-limits",
null);
// options for DouglasPeuckerFilter
double reducePointError = props.
getProperty("reduce-point-density",
2.6);
double reducePointErrorPolygon = props.
getProperty("reduce-point-density-polygon", -
1);
if (reducePointErrorPolygon == -
1)
reducePointErrorPolygon = reducePointError
;
dpFilterLineResMap = parseLevelOption
(props,
"simplify-lines", reducePointError
);
dpFilterShapeResMap = parseLevelOption
(props,
"simplify-polygons", reducePointErrorPolygon
);
mergeLines = props.
containsKey("merge-lines");
allowReverseMerge = props.
getProperty("allow-reverse-merge",
false);
// undocumented option - usually used for debugging only
mergeShapes =
!props.
getProperty("no-mergeshapes",
false);
improveOverview = props.
getProperty("improve-overview",
false);
makePOIIndex = props.
getProperty("make-poi-index",
false);
if(props.
getProperty("poi-address") !=
null)
poiAddresses =
true;
routeCenterBoundaryType = props.
getProperty("route-center-boundary",
0);
licenseFileName = props.
getProperty("license-file",
null);
locationAutofill = LocatorUtil.
parseAutofillOption(props
);
locator =
new Locator(props
);
locator.
setDefaultCountry(countryName, countryAbbr
);
String driveOn = props.
getProperty("drive-on",
null);
if ("left".
equals(driveOn
))
driveOnLeft =
true;
if ("right".
equals(driveOn
))
driveOnLeft =
false;
orderByDecreasingArea = props.
getProperty("order-by-decreasing-area",
false);
pathsToHGT = props.
getProperty("dem",
null);
String demDistStr = props.
getProperty("dem-dists",
"-1");
demOutsidePolygonHeight =
(short) props.
getProperty("dem-outside-polygon", HGTReader.
UNDEF);
String demPolygonFile = props.
getProperty("dem-poly",
null);
if (demPolygonFile
!=
null) {
demPolygon = Java2DConverter.
readPolyFile(demPolygonFile
);
}
String ipm = props.
getProperty("dem-interpolation",
"auto");
switch (ipm
) {
case "auto":
demInterpolationMethod = InterpolationMethod.
AUTOMATIC;
break;
case "bicubic":
demInterpolationMethod = InterpolationMethod.
BICUBIC;
break;
case "bilinear":
demInterpolationMethod = InterpolationMethod.
BILINEAR;
break;
default:
throw new IllegalArgumentException("invalid argument for option dem-interpolation: '" + ipm +
"' supported are 'bilinear', 'bicubic', or 'auto'");
}
if (isOverviewCombined
) { // some alternate options, some invalid etc
demDistStr = props.
getProperty("overview-dem-dist",
"-1");
mergeLines =
true;
if (orderByDecreasingArea
) {
orderByDecreasingArea =
false;
mergeShapes =
false; // shape order in ovm_ imgs must be preserved to have the effect of above
} else {
mergeShapes =
true;
}
}
demDists = parseDemDists
(demDistStr
);
}
private static List<Integer> parseDemDists
(String demDists
) {
List<Integer> dists = CommandArgs.
stringToList(demDists,
"dem-dists")
.
stream().
map(Integer::parseInt
).
collect(Collectors.
toList());
if (dists.
isEmpty())
return Arrays.
asList(-
1);
return dists
;
}
/**
* Main method to create the map, just calls out to several routines
* that do the work.
*
* @param map The map.
* @param src The map data.
*/
public void makeMap
(Map map, LoadableMapDataSource src
) {
RGNFile rgnFile = map.
getRgnFile();
TREFile treFile = map.
getTreFile();
lblFile = map.
getLblFile();
NETFile netFile = map.
getNetFile();
hasNet = netFile
!=
null;
if (routeCenterBoundaryType
!=
0 && netFile
!=
null && src
instanceof MapperBasedMapDataSource
) {
for (RouteCenter rc : src.
getRoadNetwork().
getCenters()) {
((MapperBasedMapDataSource
) src
).
addBoundaryLine(rc.
getArea(), routeCenterBoundaryType,
rc.
reportSizes());
}
}
if (mapInfo.
isEmpty())
getMapInfo
();
if (map.
getNodFile() !=
null) {
// make sure that island detection is done before we write any map data so that NOD flags are properly set
src.
getRoadNetwork().
getCenters();
}
normalizeCountries
(src
);
processCities
(map, src
);
processRoads
(map,src
);
processPOIs
(map, src
);
processOverviews
(map, src
);
processInfo
(map, src
);
makeMapAreas
(map, src
);
if (driveOnLeft ==
null && src
instanceof MapperBasedMapDataSource
) {
// source can give info about driving side
driveOnLeft =
((MapperBasedMapDataSource
) src
).
getDriveOnLeft();
}
if (driveOnLeft ==
null)
driveOnLeft =
false;
treFile.
setDriveOnLeft(driveOnLeft
);
treFile.
setLastRgnPos(rgnFile.
position() - RGNHeader.
HEADER_LEN);
rgnFile.
write();
treFile.
write(rgnFile.
haveExtendedTypes());
lblFile.
write();
lblFile.
writePost();
if (netFile
!=
null) {
RoadNetwork network = src.
getRoadNetwork();
netFile.
setNetwork(network.
getRoadDefs());
NODFile nodFile = map.
getNodFile();
if (nodFile
!=
null) {
nodFile.
setNetwork(network.
getCenters(), network.
getRoadDefs(), network.
getBoundary());
nodFile.
setDriveOnLeft(driveOnLeft
);
nodFile.
write();
}
netFile.
write(lblFile.
numCities(), lblFile.
numZips());
if (nodFile
!=
null) {
nodFile.
writePost();
}
netFile.
writePost(rgnFile.
getWriter());
}
warnAbout3ByteImgRefs
();
buildDem
(map, src
);
treFile.
writePost();
}
private void buildDem
(Map map, LoadableMapDataSource src
) {
DEMFile demFile = map.
getDemFile();
if (demFile ==
null)
return;
if (demDists.
size() > src.
mapLevels().
length) {
throw new MapFailedException
("More dem-dist values than levels: \n\t" + demDists +
"\n\t"
+
Arrays.
toString(src.
mapLevels()) +
"\n");
}
try{
long t1 =
System.
currentTimeMillis();
java.
awt.
geom.
Area demArea =
null;
if (demPolygon
!=
null) {
Area bbox = src.
getBounds();
// the rectangle is a bit larger to avoid problems at tile boundaries
Rectangle2D r =
new Rectangle(bbox.
getMinLong() -
2, bbox.
getMinLat() -
2,
bbox.
getWidth() +
4, bbox.
getHeight() +
4);
if (demPolygon.
intersects(r
) && !demPolygon.
contains(r
)) {
demArea = demPolygon
;
}
}
if (demArea ==
null && isOverviewCombined
) {
Path2D demPoly =
((OverviewMapDataSource
) src
).
getTileAreaPath();
if (demPoly
!=
null) {
demArea =
new java.
awt.
geom.
Area(demPoly
);
}
}
Area treArea = demFile.
calc(src.
getBounds(), demArea, pathsToHGT, demDists, demOutsidePolygonHeight, demInterpolationMethod
);
map.
setBounds(treArea
);
long t2 =
System.
currentTimeMillis();
log.
info("DEM file calculation for", map.
getFilename(),
"took",
(t2 - t1
),
"ms");
demFile.
write();
} catch (MapTooBigException e
) {
throw new MapTooBigException
(e.
getMaxAllowedSize(),
"The DEM section of the map or tile is too big.",
"Try increasing the DEM distance.");
} catch (MapFailedException e
) {
throw new MapFailedException
("Error creating DEM File. " + e.
getMessage());
}
}
private void warnAbout3ByteImgRefs
() {
String mapContains =
"Map contains";
String infoMsg =
"- more than 65535 might cause indexing problems and excess size. Suggest splitter with lower --max-nodes";
int itemCount
;
itemCount = lblFile.
numCities();
if (itemCount
> 0xffff
)
log.
error(mapContains, itemCount,
"Cities", infoMsg
);
itemCount = lblFile.
numZips();
if (itemCount
> 0xffff
)
log.
error(mapContains, itemCount,
"Zips", infoMsg
);
itemCount = lblFile.
numHighways();
if (itemCount
> 0xffff
)
log.
error(mapContains, itemCount,
"Highways", infoMsg
);
itemCount = lblFile.
numExitFacilities();
if (itemCount
> 0xffff
)
log.
error(mapContains, itemCount,
"Exit facilities", infoMsg
);
} // warnAbout3ByteImgRefs
private Country getDefaultCountry
() {
if (defaultCountry ==
null && lblFile
!=
null) {
defaultCountry = lblFile.
createCountry(countryName, countryAbbr
);
}
return defaultCountry
;
}
/**
* Retrieves the region with the default name in the given country.
* @param country the country ({@code null} = use default country)
* @return the default region in the given country ({@code null} if not available)
*/
private Region getDefaultRegion
(Country country
) {
if (lblFile ==
null || regionName ==
null) {
return null;
}
if (country ==
null) {
if (getDefaultCountry
() ==
null) {
return null;
} else {
return lblFile.
createRegion(getDefaultCountry
(), regionName, regionAbbr
);
}
} else {
return lblFile.
createRegion(country, regionName, regionAbbr
);
}
}
/**
* Process the country names of all elements and normalize them
* so that one consistent country name is used for the same country
* instead of different spellings.
* @param src the source of elements
*/
private void normalizeCountries
(MapDataSource src
) {
for (MapPoint p : src.
getPoints()) {
String countryStr = p.
getCountry();
if (countryStr
!=
null) {
countryStr = locator.
normalizeCountry(countryStr
);
p.
setCountry(countryStr
);
}
}
for (MapLine l : src.
getLines()) {
String countryStr = l.
getCountry();
if (countryStr
!=
null) {
countryStr = locator.
normalizeCountry(countryStr
);
l.
setCountry(countryStr
);
}
}
// shapes do not have address information
}
/**
* Processing of Cities
*
* Fills the city list in lbl block that is required for find by name
* It also builds up information that is required to get address info
* for the POIs
*
* @param map The map.
* @param src The map data.
*/
private void processCities
(Map map, MapDataSource src
) {
LBLFile lbl = map.
getLblFile();
if (!locationAutofill.
isEmpty()) {
// collect the names of the cities
for (MapPoint p : src.
getPoints()) {
if(p.
isCity() && p.
getName() !=
null)
locator.
addCityOrPlace(p
); // Put the city info the map for missing info
}
locator.
autofillCities(); // Try to fill missing information that include search of next city
}
for (MapPoint p : src.
getPoints()) {
if (!p.
isCity() || p.
getName() ==
null)
continue;
String countryStr = p.
getCountry();
Country thisCountry
;
if (countryStr
!=
null) {
thisCountry = lbl.
createCountry(countryStr, locator.
getCountryISOCode(countryStr
));
} else {
thisCountry = getDefaultCountry
();
}
String regionStr = p.
getRegion();
Region thisRegion
;
if (regionStr
!=
null) {
thisRegion = lbl.
createRegion(thisCountry, regionStr,
null);
} else {
thisRegion = getDefaultRegion
(thisCountry
);
}
City thisCity
;
if (thisRegion
!=
null)
thisCity = lbl.
createCity(thisRegion, p.
getName(),
true);
else
thisCity = lbl.
createCity(thisCountry, p.
getName(),
true);
cityMap.
put(p, thisCity
);
}
}
private void processRoads
(Map map, MapDataSource src
) {
LBLFile lbl = map.
getLblFile();
MapPoint searchPoint =
new MapPoint
();
for (MapLine line : src.
getLines()) {
if(!line.
isRoad())
continue;
String cityName = line.
getCity();
String cityCountryName = line.
getCountry();
String cityRegionName = line.
getRegion();
String zipStr = line.
getZip();
if(cityName ==
null && locationAutofill.
contains("nearest")) {
// Get name of next city if untagged
searchPoint.
setLocation(line.
getLocation());
MapPoint nextCity = locator.
findNextPoint(searchPoint
);
if(nextCity
!=
null) {
cityName = nextCity.
getCity();
// city/region/country fields should match to the found city
cityCountryName = nextCity.
getCountry();
cityRegionName = nextCity.
getRegion();
// use the zip code only if no zip code is known
if(zipStr ==
null)
zipStr = nextCity.
getZip();
}
}
MapRoad road =
(MapRoad
) line
;
road.
resetImgData();
City roadCity = calcCity
(lbl, cityName, cityRegionName, cityCountryName
);
if (roadCity
!=
null)
road.
addRoadCity(roadCity
);
if (zipStr
!=
null) {
road.
addRoadZip(lbl.
createZip(zipStr
));
}
processRoadNumbers
(road, lbl
);
}
}
private void processRoadNumbers
(MapRoad road, LBLFile lbl
) {
List<Numbers
> numbers = road.
getRoadDef().
getNumbersList();
if (numbers ==
null)
return;
for (Numbers num : numbers
) {
for (int i =
0; i
< 2; i++
) {
boolean leftRightFlag =
(i ==
0);
ZipCodeInfo zipInfo = num.
getZipCodeInfo(leftRightFlag
);
if (zipInfo
!=
null && zipInfo.
getZipCode() !=
null) {
Zip zip = zipInfo.
getImgZip();
if (zip ==
null) {
zip = lbl.
createZip(zipInfo.
getZipCode());
zipInfo.
setImgZip(zip
);
}
if (zip
!=
null)
road.
addRoadZip(zip
);
}
CityInfo cityInfo = num.
getCityInfo(leftRightFlag
);
if (cityInfo
!=
null) {
City city = cityInfo.
getImgCity();
if (city ==
null) {
city = calcCity
(lbl, cityInfo.
getCity(), cityInfo.
getRegion(), cityInfo.
getCountry());
cityInfo.
setImgCity(city
);
}
if (city
!=
null)
road.
addRoadCity(city
);
}
}
}
}
private City calcCity
(LBLFile lbl,
String city,
String region,
String country
) {
if (city ==
null && region ==
null && country ==
null)
return null;
Country cc =
(country ==
null) ? getDefaultCountry
()
: lbl.
createCountry(locator.
normalizeCountry(country
), locator.
getCountryISOCode(country
));
Region cr =
(region ==
null) ? getDefaultRegion
(cc
) : lbl.
createRegion(cc, region,
null);
if (city ==
null) {
// if city name is unknown and region and/or country is known
// use empty name for the city
city = UNKNOWN_CITY_NAME
;
}
if (cr
!=
null) {
return lbl.
createCity(cr, city,
false);
} else {
return lbl.
createCity(cc, city,
false);
}
}
private void processPOIs
(Map map, MapDataSource src
) {
LBLFile lbl = map.
getLblFile();
boolean checkedForPoiDispFlag =
false;
for (MapPoint p : src.
getPoints()) {
// special handling for highway exits
if(p.
isExit()) {
processExit
(map,
(MapExitPoint
)p
);
}
// do not process:
// * cities (already processed)
// * extended types (address information not shown in MapSource and on GPS)
// * all POIs except roads in case the no-poi-address option is set
else if (!p.
isCity() && !p.
hasExtendedType() && poiAddresses
) {
String countryStr = p.
getCountry();
String regionStr = p.
getRegion();
String zipStr = p.
getZip();
String cityStr = p.
getCity();
if (locationAutofill.
contains("nearest")
&& (countryStr ==
null || regionStr ==
null ||
(zipStr ==
null && cityStr ==
null))) {
MapPoint nextCity = locator.
findNearbyCityByName(p
);
if (nextCity ==
null)
nextCity = locator.
findNextPoint(p
);
if (nextCity
!=
null) {
if (countryStr ==
null)
countryStr = nextCity.
getCountry();
if (regionStr ==
null)
regionStr = nextCity.
getRegion();
if (zipStr ==
null) {
String cityZipStr = nextCity.
getZip();
// Ignore list of Zips separated by ,
if (cityZipStr
!=
null && cityZipStr.
indexOf(',') < 0)
zipStr = cityZipStr
;
}
if (cityStr ==
null)
cityStr = nextCity.
getCity();
}
}
if (countryStr
!=
null && !checkedForPoiDispFlag
) {
// Different countries require different address notation
poiDisplayFlags = locator.
getPOIDispFlag(countryStr
);
checkedForPoiDispFlag =
true;
}
POIRecord r = lbl.
createPOI(p.
getName());
if (cityStr
!=
null || regionStr
!=
null || countryStr
!=
null) {
r.
setCity(calcCity
(lbl, cityStr, regionStr, countryStr
));
}
if (zipStr
!=
null) {
Zip zip = lbl.
createZip(zipStr
);
r.
setZip(zip
);
}
if (p.
getStreet() !=
null) {
Label streetName = lbl.
newLabel(p.
getStreet());
r.
setStreetName(streetName
);
}
String houseNumber = p.
getHouseNumber();
if (houseNumber
!=
null && !houseNumber.
isEmpty() && !r.
setSimpleStreetNumber(houseNumber
)) {
r.
setComplexStreetNumber(lbl.
newLabel(houseNumber
));
}
String phone = p.
getPhone();
if (phone
!=
null && !phone.
isEmpty() && !r.
setSimplePhoneNumber(phone
)) {
r.
setComplexPhoneNumber(lbl.
newLabel(phone
));
}
poimap.
put(p, r
);
}
}
lbl.
allPOIsDone();
}
private void processExit
(Map map, MapExitPoint mep
) {
LBLFile lbl = map.
getLblFile();
String exitName = mep.
getName();
String ref = mep.
getMotorwayRef();
String osmId = mep.
getOSMId();
if (ref ==
null)
log.
warn("Can't create exit", exitName,
"(OSM id", osmId,
") doesn't have exit:road_ref tag");
else {
Highway hw = highways.
get(ref
);
if (hw ==
null) {
String countryStr = mep.
getCountry();
Country thisCountry = countryStr
!=
null ? lbl.
createCountry(locator.
normalizeCountry(countryStr
), locator.
getCountryISOCode(countryStr
)) : getDefaultCountry
();
String regionStr = regionName
!=
null ? regionName : mep.
getRegion(); // use --region-name if set because highway will likely span regions
Region thisRegion = regionStr
!=
null ? lbl.
createRegion(thisCountry, regionStr,
null) : getDefaultRegion
(thisCountry
);
hw = lbl.
createHighway(thisRegion, ref
);
log.
info("creating highway", ref,
"region:", regionStr,
"country:", countryStr,
"for exit:", exitName
);
highways.
put(ref, hw
);
}
String exitTo = mep.
getTo();
Exit exit =
new Exit
(hw
);
String facilityDescription = mep.
getFacilityDescription();
log.
info("Creating", ref,
"exit", exitName,
"(OSM id", osmId +
") to", exitTo,
"with facility",
((facilityDescription ==
null)? "(none)" : facilityDescription
));
if(facilityDescription
!=
null) {
// description is TYPE,DIR,FACILITIES,LABEL
// (same as Polish Format)
String[] atts = facilityDescription.
split(",");
int type =
0;
if(atts.
length > 0)
type =
Integer.
decode(atts
[0]);
char direction =
' ';
if(atts.
length > 1) {
direction = atts
[1].
charAt(0);
if(direction ==
'\'' && atts
[1].
length() > 1)
direction = atts
[1].
charAt(1);
}
int facilities = 0x0
;
if(atts.
length > 2)
facilities =
Integer.
decode(atts
[2]);
String description =
"";
if(atts.
length > 3)
description = atts
[3];
boolean last =
true;
// FIXME - handle multiple facilities?
ExitFacility ef = lbl.
createExitFacility(type, direction, facilities, description, last
);
exit.
addFacility(ef
);
}
mep.
setExit(exit
);
POIRecord r = lbl.
createExitPOI(exitName, exit
);
if(exitTo
!=
null) {
Label ed = lbl.
newLabel(exitTo
);
exit.
setDescription(ed
);
}
poimap.
put(mep, r
);
// FIXME - set bottom bits of type to reflect facilities available?
}
}
/**
* Drive the map generation by stepping through the levels, generating the
* subdivisions for the level and filling in the map elements that should
* go into the area.
*
* This is fairly complex: you need to divide into subdivisions depending on
* their size and the number of elements that will be contained.
*
* @param map The map.
* @param src The data for the map.
*/
private void makeMapAreas
(Map map, LoadableMapDataSource src
) {
// The top level has to cover the whole map without subdividing, so
// do a special check to make sure.
LevelInfo
[] levels =
null;
if (isOverviewCombined
) {
if (mergeShapes
)
prepShapesForMerge
(src.
getShapes());
levels = src.
mapLevels();
} else if (isOverviewComponent
) {
levels = src.
overviewMapLevels();
} else {
levels = src.
mapLevels();
}
if (levels ==
null) {
throw new ExitException
("no info about levels available.");
}
LevelInfo levelInfo = levels
[0];
// If there is already a top level zoom, then we shouldn't add our own
Subdivision topdiv
;
if (levelInfo.
isTop()) {
// There is already a top level definition. So use the values from it and
// then remove it from the levels definition.
levels =
Arrays.
copyOfRange(levels,
1, levels.
length);
Zoom zoom = map.
createZoom(levelInfo.
getLevel(), levelInfo.
getBits());
topdiv = makeTopArea
(src, map, zoom
);
} else {
// We have to automatically create the definition for the top zoom level.
int maxBits = getMaxBits
(src
);
// If the max is larger than the top-most data level then we
// decrease it so that it is less.
if (levelInfo.
getBits() <= maxBits
)
maxBits = levelInfo.
getBits() -
1;
// Create the empty top level
Zoom zoom = map.
createZoom(levelInfo.
getLevel() +
1, maxBits
);
topdiv = makeTopArea
(src, map, zoom
);
}
// We start with one map data source.
List<SourceSubdiv
> srcList =
Collections.
singletonList(new SourceSubdiv
(src, topdiv
));
if (mergeShapes
&& improveOverview
&& isOverviewComponent
) {
recalcMultipolygons
(src, levels
);
}
src.
getShapes().
forEach(s -
> s.
setMpRel(null)); // free memory for MultipolygonRelations
// Now the levels filled with features.
for (LevelInfo linfo : levels
) {
List<SourceSubdiv
> nextList =
new ArrayList<>();
Zoom zoom = map.
createZoom(linfo.
getLevel(), linfo.
getBits());
for (SourceSubdiv srcDivPair : srcList
) {
MapSplitter splitter =
new MapSplitter
(srcDivPair.
getSource(), zoom
);
MapArea
[] areas = splitter.
split(orderByDecreasingArea
);
log.
info("Map region", srcDivPair.
getSource().
getBounds(),
"split into", areas.
length,
"areas at resolution", zoom.
getResolution());
for (MapArea area : areas
) {
Subdivision parent = srcDivPair.
getSubdiv();
Subdivision div = makeSubdivision
(map, parent, area, zoom
);
if (log.
isDebugEnabled())
log.
debug("ADD parent-subdiv", parent, srcDivPair.
getSource(),
", z=", zoom,
"new=", div
);
nextList.
add(new SourceSubdiv
(area, div
));
}
if (!nextList.
isEmpty()) {
Subdivision lastdiv = nextList.
get(nextList.
size() -
1).
getSubdiv();
lastdiv.
setLast(true);
}
}
srcList = nextList
;
}
}
/**
* for the overview map:
* Make sure that all {@link Coord} instances are
* identical when they are equal.
* @param shapes the list of shapes
*/
private static void prepShapesForMerge
(List<MapShape
> shapes
) {
Long2ObjectOpenHashMap
<Coord
> coordMap =
new Long2ObjectOpenHashMap
<>();
for (MapShape s : shapes
) {
List<Coord
> points = s.
getPoints();
int n = points.
size();
for (int i =
0; i
< n
; i++
) {
Coord co = points.
get(i
);
long key = Utils.
coord2Long(co
);
Coord repl = coordMap.
get(key
);
if (repl ==
null)
coordMap.
put(key, co
);
else
points.
set(i, repl
);
}
}
}
/**
* Create the top level subdivision.
*
* There must be an empty zoom level at the least detailed level. As it
* covers the whole area in one it must be zoomed out enough so that
* this can be done.
*
* Note that the width is a 16 bit quantity, but the top bit is a
* flag and so that leaves only 15 bits into which the actual width
* can fit.
*
* @param src The source of map data.
* @param map The map being created.
* @param zoom The zoom level.
* @return The new top level subdivision.
*/
private static Subdivision makeTopArea
(MapDataSource src,
Map map, Zoom zoom
) {
Subdivision topdiv = map.
topLevelSubdivision(src.
getBounds(), zoom
);
topdiv.
setLast(true);
return topdiv
;
}
/**
* Make an individual subdivision for the map. To do this we need a link
* to its parent and the zoom level that we are working at.
*
* @param map The map to add this subdivision into.
* @param parent The parent division.
* @param ma The area of the map that we are fitting into this division.
* @param z The zoom level.
* @return The new subdivsion.
*/
private Subdivision makeSubdivision
(Map map, Subdivision parent, MapArea ma, Zoom z
) {
List<MapPoint
> points = ma.
getPoints();
List<MapLine
> lines = ma.
getLines();
List<MapShape
> shapes = ma.
getShapes();
Subdivision div = map.
createSubdivision(parent, ma.
getFullBounds(), z
);
if (ma.
hasPoints())
div.
setHasPoints(true);
if (ma.
hasIndPoints())
div.
setHasIndPoints(true);
if (ma.
hasLines())
div.
setHasPolylines(true);
if (ma.
hasShapes())
div.
setHasPolygons(true);
div.
startDivision();
processPoints
(map, div, points
);
final int res = z.
getResolution();
lines = lines.
stream().
filter(l -
> l.
getMinResolution() <= res
).
collect(Collectors.
toList());
shapes = shapes.
stream().
filter(s -
> s.
getMinResolution() <= res
).
collect(Collectors.
toList());
if (mergeLines
) {
LineMergeFilter merger =
new LineMergeFilter
();
lines = merger.
merge(lines, res,
!hasNet, allowReverseMerge
);
}
if (mergeShapes
) {
ShapeMergeFilter shapeMergeFilter =
new ShapeMergeFilter
(res, orderByDecreasingArea
);
shapes = shapeMergeFilter.
merge(shapes
);
}
// recalculate preserved status for all points in lines and shapes
shapes.
forEach(e -
> e.
getPoints().
forEach(p -
> p.
preserved(false)));
if (z.
getLevel() ==
0 && hasNet
) {
lines.
forEach(e -
> e.
getPoints().
forEach(p -
> p.
preserved(p.
isNumberNode())));
} else {
lines.
forEach(e -
> e.
getPoints().
forEach(p -
> p.
preserved(false)));
}
preserveFirstLast
(lines
);
if (res
< 24) {
preserveHorizontalAndVerticalLines
(res, shapes
);
}
processLines
(map, div, lines
);
processShapes
(map, div, shapes
);
div.
endDivision();
return div
;
}
/**
* Mark first and last point of each line as preserved
* @param the lines
*/
private static void preserveFirstLast
(List<MapLine
> lines
) {
for (MapLine l : lines
) {
l.
getPoints().
get(0).
preserved(true);
l.
getPoints().
get(l.
getPoints().
size()-
1).
preserved(true);
}
}
/**
* Create the overview sections.
*
* @param map The map details.
* @param src The map data source.
*/
protected void processOverviews
(Map map, MapDataSource src
) {
List<Overview
> features = src.
getOverviews();
for (Overview ov : features
) {
switch (ov.
getKind()) {
case Overview.
POINT_KIND:
map.
addPointOverview((PointOverview
) ov
);
break;
case Overview.
LINE_KIND:
map.
addPolylineOverview((PolylineOverview
) ov
);
break;
case Overview.
SHAPE_KIND:
map.
addPolygonOverview((PolygonOverview
) ov
);
break;
default:
break;
}
}
}
/**
* Set all the information that appears in the header.
*/
private void getMapInfo
() {
if (licenseFileName
!=
null) {
List<String> licenseArray =
new ArrayList<>();
try {
File file =
new File(licenseFileName
);
licenseArray = Files.
readAllLines(file.
toPath(), StandardCharsets.
UTF_8);
}
catch (Exception e
) {
throw new ExitException
("Error reading license file " + licenseFileName, e
);
}
if ((!licenseArray.
isEmpty()) && licenseArray.
get(0).
startsWith("\ufeff"))
licenseArray.
set(0, licenseArray.
get(0).
substring(1));
UnaryOperator
<String> replaceVariables = s -
> s.
replace("$MKGMAP_VERSION$", Version.
VERSION)
.
replace("$JAVA_VERSION$",
System.
getProperty("java.version"))
.
replace("$YEAR$",
Integer.
toString(now.
getYear()))
.
replace("$LONGDATE$", now.
format(DateTimeFormatter.
ofLocalizedDate(FormatStyle.
LONG)))
.
replace("$SHORTDATE$", now.
format(DateTimeFormatter.
ofLocalizedDate(FormatStyle.
SHORT)))
.
replace("$TIME$", now.
toLocalTime().
toString().
substring(0,
5));
licenseArray.
replaceAll(replaceVariables
);
mapInfo.
addAll(licenseArray
);
} else {
mapInfo.
add("Map data (c) OpenStreetMap and its contributors");
mapInfo.
add("http://www.openstreetmap.org/copyright");
mapInfo.
add("");
mapInfo.
add("This map data is made available under the Open Database License:");
mapInfo.
add("http://opendatacommons.org/licenses/odbl/1.0/");
mapInfo.
add("Any rights in individual contents of the database are licensed under the");
mapInfo.
add("Database Contents License: http://opendatacommons.org/licenses/dbcl/1.0/");
mapInfo.
add("");
// Pad the version number with spaces so that version
// strings that are different lengths do not change the size and
// offsets of the following sections.
mapInfo.
add("Map created with mkgmap-r"
+
String.
format("%-10s", Version.
VERSION));
mapInfo.
add("Program released under the GPL");
}
}
public void setMapInfo
(List<String> msgs
) {
mapInfo = msgs
;
}
public void setCopyrights
(List<String> msgs
) {
copyrights = msgs
;
}
/**
* Set all the information that appears in the header.
*
* @param map The map to write to.
* @param src The source of map information.
*/
private void processInfo
(Map map, LoadableMapDataSource src
) {
// The bounds of the map.
map.
setBounds(src.
getBounds());
if (!isOverviewCombined
)
poiDisplayFlags |= TREHeader.
POI_FLAG_DETAIL;
poiDisplayFlags |= src.
getPoiDispFlag();
if(poiDisplayFlags
!=
0)
map.
addPoiDisplayFlags(poiDisplayFlags
);
// You can add anything here.
// But there has to be something, otherwise the map does not show up.
//
// We use it to add copyright information that there is no room for
// elsewhere
StringBuilder info =
new StringBuilder();
for (String s : mapInfo
) {
info.
append(s.
trim()).
append('\n');
}
if (info.
length() > 0)
map.
addInfo(info.
toString());
if (copyrights.
isEmpty()) {
// There has to be (at least) two copyright messages or else the map
// does not show up. The second and subsequent ones will be displayed
// at startup, although the conditions where that happens are not known.
// All copyright messages are displayed in BaseCamp.
String[] copyrightMessages = src.
copyrightMessages();
if (copyrightMessages.
length < 2)
map.
addCopyright("program licenced under GPL v2");
for (String cm : copyrightMessages
)
map.
addCopyright(cm
);
} else {
for (String cm : copyrights
)
map.
addCopyright(cm
);
}
}
/**
* Step through the points, filter and create a map point which is then added
* to the map.
*
* Note that the location and resolution of map elements is relative to the
* subdivision that they occur in.
*
* @param map The map to add points to.
* @param div The subdivision that the points belong to.
* @param points The points to be added.
*/
private void processPoints
(Map map, Subdivision div,
List<MapPoint
> points
) {
LBLFile lbl = map.
getLblFile();
div.
startPoints();
int res = div.
getResolution();
boolean haveIndPoints =
false;
int pointIndex =
1;
// although the non-indexed points are output first,
// pointIndex must be initialized to the number of indexed
// points (not 1)
for (MapPoint point : points
) {
if (point.
isCity() && point.
getMinResolution() <= res
) {
++pointIndex
;
haveIndPoints =
true;
}
}
for (MapPoint point : points
) {
if (point.
isCity() || point.
getMinResolution() > res
)
continue;
String name = point.
getName();
Point p = div.
createPoint(name
);
p.
setType(point.
getType());
if (point.
hasExtendedType()) {
ExtTypeAttributes eta = point.
getExtTypeAttributes();
if (eta
!=
null) {
eta.
processLabels(lbl
);
p.
setExtTypeAttributes(eta
);
}
}
Coord coord = point.
getLocation();
try {
p.
setLatitude(coord.
getLatitude());
p.
setLongitude(coord.
getLongitude());
} catch (AssertionError ae
) {
log.
error("Problem with point of type 0x" +
Integer.
toHexString(point.
getType()) +
" at " + coord.
toOSMURL());
log.
error(" Subdivision shift is " + div.
getShift() +
" and its centre is at " + div.
getCenter().
toOSMURL());
log.
error(" " + ae.
getMessage());
continue;
}
POIRecord r = poimap.
get(point
);
if (r
!=
null)
p.
setPOIRecord(r
);
map.
addMapObject(p
);
if (!point.
hasExtendedType()) {
if (name
!=
null && div.
getZoom().
getLevel() ==
0) {
if (pointIndex
> 255) {
log.
error("Too many POIs near location", div.
getCenter().
toOSMURL(),
"-", name,
"will be ignored");
} else if (point.
isExit()) {
Exit e =
((MapExitPoint
) point
).
getExit();
if (e
!=
null)
e.
getHighway().
addExitPoint(name, pointIndex, div
);
} else if (makePOIIndex
) {
lbl.
createPOIIndex(name, pointIndex, div, point.
getType());
}
}
++pointIndex
;
}
}
if (haveIndPoints
) {
div.
startIndPoints();
pointIndex =
1; // reset to 1
for (MapPoint point : points
) {
if (!point.
isCity() || point.
getMinResolution() > res
)
continue;
String name = point.
getName();
Point p = div.
createPoint(name
);
int fullType = point.
getType();
assert (fullType
& 0xff
) ==
0 :
"indPoint " + GType.
formatType(fullType
) +
" has subtype";
p.
setType(fullType
);
Coord coord = point.
getLocation();
try {
p.
setLatitude(coord.
getLatitude());
p.
setLongitude(coord.
getLongitude());
} catch (AssertionError ae
) {
log.
error("Problem with point of type 0x" +
Integer.
toHexString(point.
getType()) +
" at " + coord.
toOSMURL());
log.
error(" Subdivision shift is " + div.
getShift() +
" and its centre is at " + div.
getCenter().
toOSMURL());
log.
error(" " + ae.
getMessage());
continue;
}
map.
addMapObject(p
);
if(name
!=
null && div.
getZoom().
getLevel() ==
0) {
// retrieve the City created earlier for this
// point and store the point info in it
City c = cityMap.
get(point
);
if(pointIndex
> 255) {
log.
error("Can't set city point index for", name,
"(too many indexed points in division)\n");
} else {
c.
setPointIndex(pointIndex
);
c.
setSubdivision(div
);
}
}
++pointIndex
;
}
}
}
/**
* Step through the lines, filter, simplify if necessary, and create a map
* line which is then added to the map.
*
* Note that the location and resolution of map elements is relative to the
* subdivision that they occur in.
*
* @param map The map to add points to.
* @param div The subdivision that the lines belong to.
* @param lines The lines to be added.
*/
private void processLines
(Map map, Subdivision div,
List<MapLine
> lines
) {
div.
startLines(); // Signal that we are beginning to draw the lines.
int res = div.
getResolution();
FilterConfig config =
new FilterConfig
();
config.
setResolution(res
);
config.
setLevel(div.
getZoom().
getLevel());
config.
setHasNet(hasNet
);
LayerFilterChain normalFilters =
new LayerFilterChain
(config
);
LayerFilterChain keepParallelFilters =
new LayerFilterChain
(config
);
DouglasPeuckerFilter dp =
null;
if (enableLineCleanFilters
&& (res
< 24)) {
MapFilter rounder =
new RoundCoordsFilter
();
MapFilter sizeFilter =
new SizeFilter
(MIN_SIZE_LINE
);
normalFilters.
addFilter(rounder
);
normalFilters.
addFilter(sizeFilter
);
double errorForRes = dpFilterLineResMap.
ceilingEntry(res
).
getValue();
if(errorForRes
> 0) {
dp =
new DouglasPeuckerFilter
(errorForRes
);
keepParallelFilters.
addFilter(dp
);
}
keepParallelFilters.
addFilter(rounder
);
keepParallelFilters.
addFilter(sizeFilter
);
}
RemoveObsoletePointsFilter removeObsolete =
new RemoveObsoletePointsFilter
();
normalFilters.
addFilter(removeObsolete
);
keepParallelFilters.
addFilter(removeObsolete
);
if (dp
!=
null)
normalFilters.
addFilter(dp
);
for (MapFilter filter :
Arrays.
asList(
new RemoveEmpty
(),
new LineSplitterFilter
(),
new LinePreparerFilter
(div
),
new LineAddFilter
(div, map
))) {
normalFilters.
addFilter(filter
);
keepParallelFilters.
addFilter(filter
);
}
for (MapLine line : lines
) {
if (line.
getMinResolution() <= res
) {
if (GType.
isContourLine(line
) || isOverviewComponent
)
keepParallelFilters.
startFilter(line
);
else
normalFilters.
startFilter(line
);
}
}
}
/**
* Step through the polygons, filter, simplify if necessary, and create a map
* shape which is then added to the map.
*
* Note that the location and resolution of map elements is relative to the
* subdivision that they occur in.
*
* @param map The map to add polygons to.
* @param div The subdivision that the polygons belong to.
* @param shapes The polygons to be added.
*/
private void processShapes
(Map map, Subdivision div,
List<MapShape
> shapes
) {
div.
startShapes(); // Signal that we are beginning to draw the shapes.
int res = div.
getResolution();
FilterConfig config =
new FilterConfig
();
config.
setResolution(res
);
config.
setLevel(div.
getZoom().
getLevel());
config.
setHasNet(hasNet
);
if (orderByDecreasingArea
&& shapes.
size() > 1) {
// sort so that the shape with the largest area is processed first
shapes.
sort((s1,s2
) -
> Long.
compare(Math.
abs(s2.
getFullArea()),
Math.
abs(s1.
getFullArea())));
}
LayerFilterChain filters =
new LayerFilterChain
(config
);
filters.
addFilter(new PolygonSplitterFilter
());
if (enableLineCleanFilters
&& (res
< 24)) {
filters.
addFilter(new RoundCoordsFilter
());
}
filters.
addFilter(new RemoveObsoletePointsFilter
());
if (enableLineCleanFilters
&& (res
< 24)) {
int sizefilterVal = getMinSizePolygonForResolution
(res
);
if (sizefilterVal
> 0)
filters.
addFilter(new SizeFilter
(sizefilterVal
));
//DouglasPeucker behaves at the moment not really optimal at low zooms, but acceptable.
//Is there a similar algorithm for polygons?
double errorForRes = dpFilterShapeResMap.
ceilingEntry(res
).
getValue();
if(errorForRes
> 0)
filters.
addFilter(new DouglasPeuckerFilter
(errorForRes
));
}
filters.
addFilter(new RemoveEmpty
());
filters.
addFilter(new LinePreparerFilter
(div
));
filters.
addFilter(new ShapeAddFilter
(div, map
));
for (MapShape shape : shapes
) {
if (shape.
getMinResolution() <= res
) {
filters.
startFilter(shape
);
}
}
}
/**
* Preserve shape points which a) lie on the shape boundary or
* b) which appear multiple times in the shape (excluding the start
* point which should always be identical to the end point).
* The preserved points are kept treated specially in the
* Line-Simplification-Filters, this should avoid artifacts like
* white triangles in the sea for lower resolutions.
* @param res the current resolution
* @param shapes list of shapes
*/
private static void preserveHorizontalAndVerticalLines
(int res,
List<MapShape
> shapes
) {
if (res ==
24)
return;
for (MapShape shape : shapes
) {
if (shape.
getMinResolution() > res
)
continue;
List<Coord
> points = shape.
getPoints();
int n = points.
size();
IdentityHashMap<Coord, Coord
> coords =
new IdentityHashMap<>(n
);
Coord prev = points.
get(0);
Coord last
;
for (int i =
1; i
< n
; ++i
) {
last = points.
get(i
);
// preserve coord instances which are used more than once,
// these are typically produced by the ShapeMergerFilter
// to connect holes
if (coords.
get(last
) ==
null) {
coords.
put(last, last
);
} else if (!last.
preserved()) {
last.
preserved(true);
}
// preserve the end points of horizontal and vertical lines
// they are very likely produced by cutting
if(last.
getHighPrecLat() == prev.
getHighPrecLat() || last.
getHighPrecLon() == prev.
getHighPrecLon()) {
last.
preserved(true);
prev.
preserved(true);
}
prev = last
;
}
}
}
/**
* It is not possible to represent large maps at the 24 bit resolution. This
* gets the largest resolution that can still cover the whole area of the
* map. It is used for the top most layer.
*
* @param src The map data.
* @return The largest number of bits where we can still represent the
* whole map.
*/
private static int getMaxBits
(MapDataSource src
) {
int topshift =
Integer.
numberOfLeadingZeros(src.
getBounds().
getMaxDimension());
int minShift =
Math.
max(CLEAR_TOP_BITS - topshift,
0);
return 24 - minShift
;
}
public void setEnableLineCleanFilters
(boolean enable
) {
this.
enableLineCleanFilters = enable
;
}
/**
* Determine the minimum size for a polygon for the given level.
* @param res the resolution
* @return the size filter value
*/
private int getMinSizePolygonForResolution
(int res
) {
if (polygonSizeLimitsOpt ==
null)
return minSizePolygon
;
if (polygonSizeLimits ==
null) {
polygonSizeLimits =
new TreeMap<>();
String[] desc = polygonSizeLimitsOpt.
split("[, \\t\\n]+");
for (String s : desc
) {
String[] keyVal = s.
split("[=:]");
if (keyVal ==
null || keyVal.
length !=
2) {
throw new ExitException
("incorrect polygon-size-limits specification " + polygonSizeLimitsOpt
);
}
try {
int key =
Integer.
parseInt(keyVal
[0]);
int value =
Integer.
parseInt(keyVal
[1]);
Integer testDup = polygonSizeLimits.
put(key, value
);
if (testDup
!=
null) {
throw new ExitException
("duplicate resolution value in polygon-size-limits specification "
+ polygonSizeLimitsOpt
);
}
} catch (NumberFormatException e
) {
throw new ExitException
("polygon-size-limits specification not all numbers: " + s
);
}
}
if (polygonSizeLimits.
get(24) ==
null)
polygonSizeLimits.
put(24,
0);
}
// return the value for the desired resolution or the next higher one
return polygonSizeLimits.
ceilingEntry(res
).
getValue();
}
/**
* Parse an option with pairs of resolution and double values.
*
* @param props the properties
* @param optionName the option name
* @param defaultValue the default value for all resolutions if the option is
* not given
* @return the map
*/
private TreeMap<Integer,
Double> parseLevelOption
(EnhancedProperties props,
String optionName,
double defaultValue
) {
String option = props.
getProperty(optionName
);
TreeMap<Integer,
Double> levelMap =
new TreeMap<>();
if (option
!=
null) {
String[] desc = option.
split("[, \\t\\n]+");
for (String s : desc
) {
String[] keyVal = s.
split("[=:]");
if (keyVal ==
null || keyVal.
length !=
2) {
throw new ExitException
("incorrect " + optionName +
" specification " + option +
" at " + s
);
}
try {
int key =
Integer.
parseInt(keyVal
[0]);
double value =
Double.
parseDouble(keyVal
[1]);
Double testDup = levelMap.
put(key, value
);
if (testDup
!=
null) {
throw new ExitException
(
"duplicate resolution value in " + optionName +
" specification " + optionName
);
}
} catch (NumberFormatException e
) {
throw new ExitException
(optionName +
" specification not all numbers: " + s
);
}
}
}
if (levelMap.
get(24) ==
null)
levelMap.
put(24, defaultValue
);
return levelMap
;
}
private static class SourceSubdiv
{
private final MapDataSource source
;
private final Subdivision subdiv
;
SourceSubdiv
(MapDataSource ds, Subdivision subdiv
) {
this.
source = ds
;
this.
subdiv = subdiv
;
}
public MapDataSource getSource
() {
return source
;
}
public Subdivision getSubdiv
() {
return subdiv
;
}
}
private static class LineAddFilter
extends BaseFilter
implements MapFilter
{
private final Subdivision div
;
private final Map map
;
LineAddFilter
(Subdivision div,
Map map
) {
this.
div = div
;
this.
map = map
;
}
@
Override
public void doFilter
(MapElement element, MapFilterChain next
) {
MapLine line =
(MapLine
) element
;
assert line.
getPoints().
size() < 255 :
"too many points";
Polyline pl = div.
createLine(line.
getLabels());
if (element.
hasExtendedType()) {
ExtTypeAttributes eta = element.
getExtTypeAttributes();
if (eta
!=
null) {
eta.
processLabels(map.
getLblFile());
pl.
setExtTypeAttributes(eta
);
}
} else {
div.
setPolylineNumber(pl
);
}
pl.
setDirection(line.
isDirection());
pl.
addCoords(line.
getPoints());
pl.
setType(line.
getType());
if (map.
getNetFile() !=
null && line
instanceof MapRoad
) {
if (log.
isDebugEnabled())
log.
debug("adding road def: " + line.
getName());
MapRoad road =
(MapRoad
) line
;
RoadDef roaddef = road.
getRoadDef();
pl.
setRoadDef(roaddef
);
if (road.
hasSegmentsFollowing())
pl.
setLastSegment(false);
roaddef.
addPolylineRef(pl
);
}
map.
addMapObject(pl
);
}
}
private static class ShapeAddFilter
extends BaseFilter
implements MapFilter
{
private final Subdivision div
;
private final Map map
;
ShapeAddFilter
(Subdivision div,
Map map
) {
this.
div = div
;
this.
map = map
;
}
@
Override
public void doFilter
(MapElement element, MapFilterChain next
) {
MapShape shape =
(MapShape
) element
;
assert shape.
getPoints().
size() < 255 :
"too many points";
Polygon pg = div.
createPolygon(shape.
getName());
pg.
addCoords(shape.
getPoints());
pg.
setType(shape.
getType());
if (element.
hasExtendedType()) {
ExtTypeAttributes eta = element.
getExtTypeAttributes();
if (eta
!=
null) {
eta.
processLabels(map.
getLblFile());
pg.
setExtTypeAttributes(eta
);
}
}
map.
addMapObject(pg
);
}
}
/**
* Find out shapes visible in the overview map which were created from a single multipolygon.
* Typically this is the sea polygon. Create a simplified version for each.
* @param src the map source
* @param levels levels for the overview map
*/
private void recalcMultipolygons
(LoadableMapDataSource src, LevelInfo
[] levels
) {
final int maxRes = levels
[levels.
length -
1].
getBits();
java.
util.
Map<MultiPolygonRelation,
List<MapShape
>> mpShapes =
new LinkedHashMap<>();
src.
getShapes().
stream().
filter(s -
> s.
getMpRel() !=
null && s.
getMinResolution() <= maxRes
)
.
forEach(s -
> mpShapes.
computeIfAbsent(s.
getMpRel(), k -
> new ArrayList<>()).
add(s
));
if (mpShapes.
isEmpty())
return;
MapShapeComparator comparator =
new MapShapeComparator
(orderByDecreasingArea
);
for (Entry
<MultiPolygonRelation,
List<MapShape
>> e : mpShapes.
entrySet()) {
if (e.
getKey().
isNoRecalc())
continue;
MapShape pattern = e.
getValue().
get(0);
boolean matches =
true;
for (MapShape s : e.
getValue()) {
if (s.
getMinResolution() != pattern.
getMinResolution()
|| s.
getMaxResolution() != pattern.
getMaxResolution()
|| comparator.
compare(s, pattern
) !=
0) {
matches =
false;
break;
}
}
if (matches
) {
buildMPRing
(src, maxRes, pattern, e.
getKey());
e.
getValue().
forEach(s -
> s.
setMinResolution(maxRes +
1));
}
}
}
/**
* Re-Render the multipolygon for the given max. resolution and create new
* shapes with this value as maxResolution.
*
* @param src the map source
* @param res the wanted maximum resolution
* @param pattern pattern for the new shapes
* @param origMp the multipolygon relation that contains the rings at full
* resolution
*/
private void buildMPRing
(LoadableMapDataSource src,
int res, MapShape pattern, MultiPolygonRelation origMp
) {
List<? extends Way
> rings = origMp.
getRings();
Way largest = origMp.
getLargestOuterRing();
int shift =
24 - res
;
int minSize = getMinSizePolygonForResolution
(res
) * (1 << shift
) /
2;
GeneralRelation gr =
new GeneralRelation
(FakeIdGenerator.
makeFakeId());
java.
util.
Map<Long, Way
> wayMap =
new LinkedHashMap<>();
final double dpError = dpFilterShapeResMap.
ceilingEntry(res
).
getValue() * (1 << shift
);
for (int i =
0; i
< rings.
size(); i++
) {
List<Coord
> poly =
new ArrayList<>(rings.
get(i
).
getPoints());
boolean isLargest = largest == rings.
get(i
);
boolean tooSmall = minSize
> 0 && Area.
getBBox(poly
).
getMaxDimension() < minSize
;
if (isLargest
&& tooSmall
&& !pattern.
isSkipSizeFilter())
return;
if (tooSmall
)
continue;
if (dpError
> 0 && !isLargest
) {
DouglasPeuckerFilter.
douglasPeucker(poly,
0, poly.
size() -
1, dpError
);
}
if (poly.
size() > 3) {
Way w =
new Way
(FakeIdGenerator.
makeFakeId(), poly
);
wayMap.
put(w.
getId(), w
);
gr.
addElement("", w
);
}
}
List<List<Coord
>> list =
new ArrayList<>();
if (gr.
getElements().
isEmpty()) {
return;
} else if (gr.
getElements().
size() ==
1) {
list.
addAll(ShapeSplitter.
clipToBounds(largest.
getPoints(), src.
getBounds(),
null));
} else {
final String codeValue = GType.
formatType(pattern.
getType());
gr.
addTag("code", codeValue
);
gr.
addTag("expect-self-intersection",
"true");
MultiPolygonRelation mp =
new MultiPolygonRelation
(gr, wayMap, src.
getBounds());
mp.
processElements();
for (Way w : wayMap.
values()) {
if (MultiPolygonRelation.
STYLE_FILTER_POLYGON.
equals(w.
getTag(MultiPolygonRelation.
STYLE_FILTER_TAG))
&& codeValue.
equals(w.
getTag("code"))) {
if (src.
getBounds().
contains(Area.
getBBox(w.
getPoints()))) {
list.
add(w.
getPoints());
} else {
// we must clip to tile bounds
list.
addAll(ShapeSplitter.
clipToBounds(w.
getPoints(), src.
getBounds(),
null));
}
}
}
}
for (int i =
0; i
< list.
size(); i++
) {
List<Coord
> poly = list.
get(i
);
MapShape newShape = pattern.
copy();
newShape.
setPoints(poly
);
newShape.
setOsmid(FakeIdGenerator.
makeFakeId());
newShape.
setMaxResolution(res
);
newShape.
setMpRel(null);
src.
getShapes().
add(newShape
);
}
}
}