Merge tag 'upstream/0.0.0+svn3706'
Upstream version 0.0.0+svn3706
Bas Couwenberg
7 years ago
704 | 704 | <p> |
705 | 705 | ;--verbose |
706 | 706 | : Makes some operations more verbose. Mostly used with --list-styles. |
707 | <p> | |
708 | ;--order-by-decreasing-area | |
709 | : Puts area/polygons into the mapfile in decreasing size so | |
710 | that smaller features are rendered over larger ones | |
711 | (assuming _drawOrder is equal). | |
712 | The tag mkgmap:drawLevel can be used to override the | |
713 | natural area of a polygon, so forcing changes to the rendering order. | |
714 |
132 | 132 | | +mkgmap:highest-resolution-only+ | If set to +true+ the object will only be added for the highest resolution configured in the element type definition. |
133 | 133 | | +mkgmap:execute_finalize_rules+ | If set to +true+ mkgmap will execute the finalize rules even if no object is created fot the element. |
134 | 134 | | +mkgmap:numbers+ | If set to +false+ for a node or way mkgmap will ignore the object in the calculations for the --housenumber option |
135 | | +mkgmap:drawLevel+ | Set to a number from 1 to 100. Overrides the polygon area that is used by --order-by-decreasing-area. 1..50 are larger than typical polygons and be overwritten by them, 51..100 are smaller and will show. Higher drawLevels will show over lower values. | |
135 | 136 | |========================================================= |
136 | ||
137 |
698 | 698 | |
699 | 699 | --verbose |
700 | 700 | Makes some operations more verbose. Mostly used with --list-styles. |
701 | ||
702 | --order-by-decreasing-area | |
703 | Puts polygons/areas into the mapfile in decreasing size so that | |
704 | smaller features are rendered over larger ones (assuming _drawOrder | |
705 | is equal). The tag mkgmap:drawLevel can be used to override the | |
706 | natural area of a polygon, so forcing changes to the rendering order. |
0 | svn.version: 3701 | |
1 | build.timestamp: 2016-10-30T22:57:00+0000 | |
0 | svn.version: 3706 | |
1 | build.timestamp: 2016-11-28T13:14:14+0000 |
7 | 7 | natural=wetland [0x51 resolution 20] |
8 | 8 | natural=water [0x3c resolution 18] |
9 | 9 | natural=waterfall | waterway=waterfall [0x47 resolution 21] |
10 | natural=sea { add mkgmap:skipSizeFilter=true } [0x32 resolution 10] | |
10 | natural=sea { add mkgmap:skipSizeFilter=true; set mkgmap:drawLevel=2 } [0x32 resolution 10] | |
11 | 11 | |
12 | 12 | waterway=riverbank [0x46 resolution 20] |
18 | 18 | import java.util.ArrayList; |
19 | 19 | import java.util.List; |
20 | 20 | |
21 | import uk.me.parabola.imgfmt.MapFailedException; | |
21 | 22 | import uk.me.parabola.imgfmt.Utils; |
22 | 23 | import uk.me.parabola.log.Logger; |
23 | 24 | |
103 | 104 | } |
104 | 105 | |
105 | 106 | /** |
107 | * Round integer to nearest power of 2. | |
108 | * | |
109 | * @param val The number of be rounded. | |
110 | * @param shift The power of 2. | |
111 | * @return The rounded number (binary half rounds up). | |
112 | */ | |
113 | private static int roundPof2(int val, int shift) { | |
114 | if (shift <= 0) | |
115 | return val; | |
116 | return (((val >> (shift-1)) + 1) >> 1) << shift; | |
117 | } | |
118 | ||
119 | /** | |
106 | 120 | * Split this area up into a number of smaller areas. |
107 | 121 | * |
108 | 122 | * @param xsplit The number of pieces to split this area into in the x |
109 | 123 | * direction. |
110 | 124 | * @param ysplit The number of pieces to split this area into in the y |
111 | 125 | * direction. |
112 | * @return An area containing xsplit*ysplit areas. | |
113 | */ | |
114 | public Area[] split(int xsplit, int ysplit) { | |
126 | * @param resolutionShift round to this power of 2. | |
127 | * @return An array containing xsplit*ysplit areas or null if can't split in half. | |
128 | * @throws MapFailedException if more complex split operation couldn't be honoured. | |
129 | */ | |
130 | public Area[] split(int xsplit, int ysplit, int resolutionShift) { | |
115 | 131 | Area[] areas = new Area[xsplit * ysplit]; |
116 | 132 | |
117 | ||
118 | int xsize = getWidth() / xsplit; | |
119 | int ysize = getHeight() / ysplit; | |
120 | ||
121 | int xextra = getWidth() - xsize * xsplit; | |
122 | int yextra = getHeight() - ysize * ysplit; | |
123 | ||
133 | int xstart; | |
134 | int xend; | |
135 | int ystart; | |
136 | int yend; | |
137 | int nAreas = 0; | |
138 | ||
139 | xstart = minLong; | |
124 | 140 | for (int x = 0; x < xsplit; x++) { |
125 | int xstart = minLong + x * xsize; | |
126 | int xend = xstart + xsize; | |
127 | 141 | if (x == xsplit - 1) |
128 | xend += xextra; | |
129 | ||
142 | xend = maxLong; | |
143 | else | |
144 | xend = roundPof2(xstart + (maxLong - xstart) / (xsplit - x), | |
145 | resolutionShift); | |
146 | ystart = minLat; | |
130 | 147 | for (int y = 0; y < ysplit; y++) { |
131 | int ystart = minLat + y * ysize; | |
132 | int yend = ystart + ysize; | |
133 | 148 | if (y == ysplit - 1) |
134 | yend += yextra; | |
135 | Area a = new Area(ystart, xstart, yend, xend); | |
136 | log.debug(x, y, a); | |
137 | areas[x * ysplit + y] = a; | |
149 | yend = maxLat; | |
150 | else | |
151 | yend = roundPof2(ystart + (maxLat - ystart) / (ysplit - y), | |
152 | resolutionShift); | |
153 | if (xstart < xend && ystart < yend) { | |
154 | Area a = new Area(ystart, xstart, yend, xend); | |
155 | // log.debug(x, y, a); | |
156 | log.debug("Area.split", minLat, minLong, maxLat, maxLong, "res", resolutionShift, "to", ystart, xstart, yend, xend); | |
157 | areas[nAreas++] = a; | |
158 | } else | |
159 | log.warn("Area.split", minLat, minLong, maxLat, maxLong, "res", resolutionShift, "can't", xsplit, ysplit); | |
160 | ystart = yend; | |
138 | 161 | } |
162 | xstart = xend; | |
139 | 163 | } |
140 | 164 | |
141 | assert areas.length == xsplit * ysplit; | |
142 | return areas; | |
165 | // assert areas.length == xsplit * ysplit; | |
166 | if (nAreas == areas.length) // no problem | |
167 | return areas; | |
168 | // beware - MapSplitter.splitMaxSize requests split of 1/1 if the original area wasn't too big | |
169 | else if (nAreas == 1) // failed to split in half | |
170 | return null; | |
171 | else | |
172 | throw new MapFailedException("Area split shift align problems"); | |
143 | 173 | } |
144 | 174 | |
145 | 175 | /** |
19 | 19 | import java.util.Arrays; |
20 | 20 | import java.util.List; |
21 | 21 | |
22 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; | |
23 | ||
24 | import uk.me.parabola.imgfmt.Utils; | |
25 | import uk.me.parabola.util.Java2DConverter; | |
22 | 26 | import uk.me.parabola.imgfmt.app.Area; |
23 | 27 | import uk.me.parabola.imgfmt.app.Coord; |
24 | 28 | import uk.me.parabola.imgfmt.app.trergn.Overview; |
88 | 92 | /** The resolution that this area is at */ |
89 | 93 | private final int areaResolution; |
90 | 94 | |
95 | private Long2ObjectOpenHashMap<Coord> areasHashMap; | |
96 | ||
91 | 97 | /** |
92 | 98 | * Create a map area from the given map data source. This map |
93 | 99 | * area will have the same bounds as the map data source and |
174 | 180 | * Split this area into several pieces. All the map elements are reallocated |
175 | 181 | * to the appropriate subarea. Usually this instance would now be thrown |
176 | 182 | * away and the new sub areas used instead. |
183 | * <p> | |
184 | * if orderByDecreasingArea, the split is forced onto boundaries that can | |
185 | * be represented exactly with the relevant shift for the level. | |
186 | * This can cause the split to fail because all the lines/shapes that need | |
187 | * to be put at this level are here, but represented at the highest resolution | |
188 | * without any filtering relevant to the resolution and the logic to request | |
189 | * splitting considers this too much for a subDivision, even though it will | |
190 | * mostly will disappear when we come to write it and look meaningless - | |
191 | * the subDivision has been reduced to a single point at its shift level. | |
192 | * <p> | |
193 | * The lines/shapes should have been simplified much earlier in the process, | |
194 | * then they could appear as such in reasonably size subDivision. | |
195 | * The logic of levels, lines and shape placement, simplification, splitting and | |
196 | * other filtering, subDivision splitting etc needs a re-think and re-organisation. | |
177 | 197 | * |
178 | 198 | * @param nx The number of pieces in the x (longitude) direction. |
179 | 199 | * @param ny The number of pieces in the y direction. |
180 | 200 | * @param resolution The resolution of the level. |
181 | 201 | * @param bounds the bounding box that is used to create the areas. |
182 | * @return An array of the new MapArea's. | |
183 | */ | |
184 | public MapArea[] split(int nx, int ny, int resolution, Area bounds) { | |
185 | Area[] areas = bounds.split(nx, ny); | |
202 | * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas. | |
203 | * @return An array of the new MapArea's or null if can't split. | |
204 | */ | |
205 | public MapArea[] split(int nx, int ny, int resolution, Area bounds, boolean orderByDecreasingArea) { | |
206 | int resolutionShift = orderByDecreasingArea ? (24 - resolution) : 0; | |
207 | Area[] areas = bounds.split(nx, ny, resolutionShift); | |
208 | if (areas == null) { // Failed to split! | |
209 | if (log.isDebugEnabled()) { // see what is here | |
210 | for (MapLine e : this.lines) | |
211 | if (e.getMinResolution() <= areaResolution) | |
212 | log.debug("line. locn=", e.getPoints().get(0).toOSMURL(), | |
213 | " type=", uk.me.parabola.mkgmap.reader.osm.GType.formatType(e.getType()), | |
214 | " name=", e.getName(), " min=", e.getMinResolution(), " max=", e.getMaxResolution()); | |
215 | for (MapShape e : this.shapes) | |
216 | if (e.getMinResolution() <= areaResolution) | |
217 | log.debug("shape. locn=", e.getPoints().get(0).toOSMURL(), | |
218 | " type=", uk.me.parabola.mkgmap.reader.osm.GType.formatType(e.getType()), | |
219 | " name=", e.getName(), " min=", e.getMinResolution(), " max=", e.getMaxResolution(), | |
220 | " full=", e.getFullArea(), | |
221 | " calc=", uk.me.parabola.mkgmap.filters.ShapeMergeFilter.calcAreaSizeTestVal(e.getPoints())); | |
222 | // the main culprits are lots of bits of sea and coastline in an overview map (res 12) | |
223 | } | |
224 | return null; | |
225 | } | |
186 | 226 | MapArea[] mapAreas = new MapArea[nx * ny]; |
187 | 227 | log.info("Splitting area " + bounds + " into " + nx + "x" + ny + " pieces at resolution " + resolution); |
188 | 228 | boolean useNormalSplit = true; |
236 | 276 | } |
237 | 277 | |
238 | 278 | for (MapShape e : this.shapes) { |
279 | if (orderByDecreasingArea) { // need to treat shapes consistently, regardless of useNormalSplit | |
280 | splitIntoAreas(mapAreas, e, used); | |
281 | continue; | |
282 | } | |
239 | 283 | if (useNormalSplit){ |
240 | 284 | areaIndex = pickArea(mapAreas, e, xbase30, ybase30, nx, ny, dx30, dy30); |
241 | 285 | if (e.getBounds().getHeight() > maxHeight || e.getBounds().getWidth() > maxWidth){ |
259 | 303 | * them equally to the two areas. |
260 | 304 | */ |
261 | 305 | useNormalSplit = false; |
306 | log.warn("useNormalSplit false"); | |
262 | 307 | continue; |
263 | 308 | } |
264 | 309 | |
609 | 654 | } |
610 | 655 | |
611 | 656 | /** |
657 | * Spit the polygon into areas | |
658 | * | |
659 | * Using .intersect() here is expensive. The code should be changed to | |
660 | * use a simple rectangle clipping algorithm as in, say, | |
661 | * util/ShapeSplitter.java | |
662 | * | |
663 | * @param areas The available areas to choose from. | |
664 | * @param e The map element. | |
665 | * @param used flag vector to say area has been added to. | |
666 | */ | |
667 | private void splitIntoAreas(MapArea[] areas, MapShape e, boolean[] used) | |
668 | { | |
669 | // quick check if bbox of shape lies fully inside one of the areas | |
670 | Area shapeBounds = e.getBounds(); | |
671 | ||
672 | // this is worked out at standard precision, along with Area.contains() and so can get | |
673 | // tricky problems as it might not really be fully within the area. | |
674 | // so: pretend the shape is a touch bigger. Will get the optimisation most of the time | |
675 | // and in the boundary cases will fall into the precise code. | |
676 | shapeBounds = new Area(shapeBounds.getMinLat()-2, | |
677 | shapeBounds.getMinLong()-2, | |
678 | shapeBounds.getMaxLat()+2, | |
679 | shapeBounds.getMaxLong()+2); | |
680 | for (int areaIndex = 0; areaIndex < areas.length; ++areaIndex) { | |
681 | if (areas[areaIndex].getBounds().contains(shapeBounds)) { | |
682 | used[areaIndex] = true; | |
683 | areas[areaIndex].addShape(e); | |
684 | return; | |
685 | } | |
686 | } | |
687 | // Shape crosses area(s), we have to split it | |
688 | ||
689 | // Convert to a awt area | |
690 | List<Coord> coords = e.getPoints(); | |
691 | java.awt.geom.Area area = Java2DConverter.createArea(coords); | |
692 | // remember actual coord, so can re-use | |
693 | int origSize = coords.size(); | |
694 | Long2ObjectOpenHashMap<Coord> shapeHashMap = new Long2ObjectOpenHashMap<>(origSize); | |
695 | for (int i = 0; i < origSize; ++i) { | |
696 | Coord co = coords.get(i); | |
697 | shapeHashMap.put(Utils.coord2Long(co), co); | |
698 | } | |
699 | if (areasHashMap == null) | |
700 | areasHashMap = new Long2ObjectOpenHashMap<>(); | |
701 | ||
702 | for (int areaIndex = 0; areaIndex < areas.length; ++areaIndex) { | |
703 | java.awt.geom.Area clipper = Java2DConverter.createBoundsArea(areas[areaIndex].getBounds()); | |
704 | clipper.intersect(area); | |
705 | List<List<Coord>> subShapePoints = Java2DConverter.areaToShapes(clipper); | |
706 | for (List<Coord> subShape : subShapePoints) { | |
707 | // Use original or share newly created coords on clipped edge. | |
708 | // NB: .intersect()/areaToShapes can output flattened shapes, | |
709 | // normally triangles, in any orientation; check we haven't got one by calc area. | |
710 | long signedAreaSize = 0; | |
711 | int subSize = subShape.size(); | |
712 | int c1_highPrecLat = 0, c1_highPrecLon = 0; | |
713 | int c2_highPrecLat, c2_highPrecLon; | |
714 | for (int i = 0; i < subSize; ++i) { | |
715 | Coord co = subShape.get(i); | |
716 | c2_highPrecLat = co.getHighPrecLat(); | |
717 | c2_highPrecLon = co.getHighPrecLon(); | |
718 | if (i > 0) | |
719 | signedAreaSize += (long)(c2_highPrecLon + c1_highPrecLon) * | |
720 | (c1_highPrecLat - c2_highPrecLat); | |
721 | c1_highPrecLat = c2_highPrecLat; | |
722 | c1_highPrecLon = c2_highPrecLon; | |
723 | long hashVal = Utils.coord2Long(co); | |
724 | Coord replCoord = shapeHashMap.get(hashVal); | |
725 | if (replCoord != null) | |
726 | subShape.set(i, replCoord); | |
727 | else { // not an original coord | |
728 | replCoord = areasHashMap.get(hashVal); | |
729 | if (replCoord != null) | |
730 | subShape.set(i, replCoord); | |
731 | else | |
732 | areasHashMap.put(hashVal, co); | |
733 | } | |
734 | } | |
735 | if (signedAreaSize == 0) { | |
736 | log.warn("splitIntoAreas flat shape. id", e.getOsmid(), | |
737 | "type", uk.me.parabola.mkgmap.reader.osm.GType.formatType(e.getType()), subSize, | |
738 | "points, at", subShape.get(0).toOSMURL()); | |
739 | continue; | |
740 | } | |
741 | MapShape s = e.copy(); | |
742 | s.setPoints(subShape); | |
743 | s.setClipped(true); | |
744 | areas[areaIndex].addShape(s); | |
745 | used[areaIndex] = true; | |
746 | } | |
747 | } | |
748 | } | |
749 | ||
750 | ||
751 | /** | |
612 | 752 | * @return true if this area contains any data |
613 | 753 | */ |
614 | 754 | public boolean hasData() { |
20 | 20 | import java.util.ArrayList; |
21 | 21 | import java.util.Arrays; |
22 | 22 | import java.util.Collections; |
23 | import java.util.Comparator; | |
23 | 24 | import java.util.HashMap; |
24 | 25 | import java.util.IdentityHashMap; |
25 | 26 | import java.util.List; |
26 | 27 | import java.util.Set; |
27 | 28 | |
29 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; | |
30 | ||
28 | 31 | import uk.me.parabola.imgfmt.ExitException; |
32 | import uk.me.parabola.imgfmt.Utils; | |
29 | 33 | import uk.me.parabola.imgfmt.app.Coord; |
30 | 34 | import uk.me.parabola.imgfmt.app.Exit; |
31 | 35 | import uk.me.parabola.imgfmt.app.Label; |
146 | 150 | |
147 | 151 | private String licenseFileName; |
148 | 152 | |
153 | private boolean orderByDecreasingArea; | |
154 | ||
149 | 155 | public MapBuilder() { |
150 | 156 | regionName = null; |
151 | 157 | locationAutofill = Collections.emptySet(); |
187 | 193 | driveOnLeft = true; |
188 | 194 | if ("right".equals(driveOn)) |
189 | 195 | driveOnLeft = false; |
196 | orderByDecreasingArea = props.getProperty("order-by-decreasing-area", false); | |
190 | 197 | } |
191 | 198 | |
192 | 199 | /** |
684 | 691 | for (SourceSubdiv srcDivPair : srcList) { |
685 | 692 | |
686 | 693 | MapSplitter splitter = new MapSplitter(srcDivPair.getSource(), zoom); |
687 | MapArea[] areas = splitter.split(); | |
694 | MapArea[] areas = splitter.split(orderByDecreasingArea); | |
688 | 695 | log.info("Map region", srcDivPair.getSource().getBounds(), "split into", areas.length, "areas at resolution", zoom.getResolution()); |
689 | 696 | |
690 | 697 | for (MapArea area : areas) { |
711 | 718 | * @param shapes the list of shapes |
712 | 719 | */ |
713 | 720 | private void prepShapesForMerge(List<MapShape> shapes) { |
714 | HashMap<Coord,Coord> coordMap = new HashMap<>(); | |
721 | Long2ObjectOpenHashMap<Coord> coordMap = new Long2ObjectOpenHashMap<>(); | |
715 | 722 | for (MapShape s : shapes){ |
716 | 723 | List<Coord> points = s.getPoints(); |
717 | 724 | int n = points.size(); |
718 | 725 | for (int i = 0; i< n; i++){ |
719 | 726 | Coord co = points.get(i); |
720 | Coord repl = coordMap.get(co); | |
727 | Coord repl = coordMap.get(Utils.coord2Long(co)); | |
721 | 728 | if (repl == null) |
722 | coordMap.put(co, co); | |
723 | else | |
729 | coordMap.put(Utils.coord2Long(co), co); | |
730 | else | |
724 | 731 | points.set(i, repl); |
725 | 732 | } |
726 | 733 | } |
1114 | 1121 | config.setRoutable(doRoads); |
1115 | 1122 | |
1116 | 1123 | if (mergeShapes){ |
1117 | ShapeMergeFilter shapeMergeFilter = new ShapeMergeFilter(res); | |
1124 | ShapeMergeFilter shapeMergeFilter = new ShapeMergeFilter(res, orderByDecreasingArea); | |
1118 | 1125 | List<MapShape> mergedShapes = shapeMergeFilter.merge(shapes); |
1119 | 1126 | shapes = mergedShapes; |
1120 | 1127 | } |
1121 | 1128 | |
1129 | if (orderByDecreasingArea && shapes.size() > 1) { | |
1130 | // sort so that the shape with the largest area is processed first | |
1131 | Collections.sort(shapes, new Comparator<MapShape>() { | |
1132 | public int compare(MapShape s1, MapShape s2) { | |
1133 | return Long.compare(Math.abs(s2.getFullArea()), Math.abs(s1.getFullArea())); | |
1134 | } | |
1135 | }); | |
1136 | } | |
1137 | ||
1122 | 1138 | preserveHorizontalAndVerticalLines(res, shapes); |
1123 | 1139 | |
1124 | 1140 | LayerFilterChain filters = new LayerFilterChain(config); |
90 | 90 | * |
91 | 91 | * This routine is not called recursively. |
92 | 92 | * |
93 | * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas. | |
93 | 94 | * @return An array of map areas, each of which is within the size limit |
94 | 95 | * and the limit on the number of features. |
95 | 96 | */ |
96 | public MapArea[] split() { | |
97 | public MapArea[] split(boolean orderByDecreasingArea) { | |
97 | 98 | log.debug("orig area", mapSource.getBounds()); |
98 | 99 | |
99 | 100 | MapArea ma = initialArea(mapSource); |
100 | MapArea[] areas = splitMaxSize(ma); | |
101 | MapArea[] areas = splitMaxSize(ma, orderByDecreasingArea); | |
101 | 102 | |
102 | 103 | // Now step through each area and see if any have too many map features |
103 | 104 | // in them. For those that do, we further split them. This is done |
104 | 105 | // recursively until everything fits. |
105 | 106 | List<MapArea> alist = new ArrayList<>(); |
106 | addAreasToList(areas, alist, 0); | |
107 | addAreasToList(areas, alist, 0, orderByDecreasingArea); | |
107 | 108 | |
108 | 109 | MapArea[] results = new MapArea[alist.size()]; |
109 | 110 | return alist.toArray(results); |
117 | 118 | * @param areas The areas to add to the list (and possibly split up). |
118 | 119 | * @param alist The list that will finally contain the complete list of |
119 | 120 | * map areas. |
120 | */ | |
121 | private void addAreasToList(MapArea[] areas, List<MapArea> alist, int depth) { | |
121 | * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas. | |
122 | */ | |
123 | private void addAreasToList(MapArea[] areas, List<MapArea> alist, int depth, boolean orderByDecreasingArea) { | |
122 | 124 | int res = zoom.getResolution(); |
123 | 125 | for (MapArea area : areas) { |
124 | 126 | Area bounds = area.getBounds(); |
163 | 165 | log.debug("splitting area", area); |
164 | 166 | MapArea[] sublist; |
165 | 167 | if(bounds.getWidth() > bounds.getHeight()) |
166 | sublist = area.split(2, 1, res, bounds); | |
168 | sublist = area.split(2, 1, res, bounds, orderByDecreasingArea); | |
167 | 169 | else |
168 | sublist = area.split(1, 2, res, bounds); | |
169 | addAreasToList(sublist, alist, depth + 1); | |
170 | continue; | |
170 | sublist = area.split(1, 2, res, bounds, orderByDecreasingArea); | |
171 | if (sublist == null) { | |
172 | log.warn("SubDivision is single point at this resolution so can't split at " + | |
173 | area.getBounds().getCenter().toOSMURL() + " (probably harmless)"); | |
174 | } else { | |
175 | addAreasToList(sublist, alist, depth + 1, orderByDecreasingArea); | |
176 | continue; | |
177 | } | |
171 | 178 | } else { |
172 | 179 | log.error("Area too small to split at " + area.getBounds().getCenter().toOSMURL() + " (reduce the density of points, length of lines, etc.)"); |
173 | 180 | } |
191 | 198 | * If the area is already small enough then it will be returned unchanged. |
192 | 199 | * |
193 | 200 | * @param mapArea The area that needs to be split down. |
201 | * @param orderByDecreasingArea aligns subareas as powerOf2 and splits polygons into the subareas. | |
194 | 202 | * @return An array of map areas. Each will be below the max size. |
195 | 203 | */ |
196 | private MapArea[] splitMaxSize(MapArea mapArea) { | |
204 | private MapArea[] splitMaxSize(MapArea mapArea, boolean orderByDecreasingArea) { | |
197 | 205 | Area bounds = mapArea.getFullBounds(); |
198 | 206 | |
199 | 207 | int shift = zoom.getShiftValue(); |
213 | 221 | if (height > MAX_DIVISION_SIZE) |
214 | 222 | ysplit = height / MAX_DIVISION_SIZE + 1; |
215 | 223 | |
216 | return mapArea.split(xsplit, ysplit, zoom.getResolution(), bounds); | |
224 | return mapArea.split(xsplit, ysplit, zoom.getResolution(), bounds, orderByDecreasingArea); | |
217 | 225 | } |
218 | 226 | |
219 | 227 | /** |
40 | 40 | private static final Logger log = Logger.getLogger(ShapeMergeFilter.class); |
41 | 41 | private final int resolution; |
42 | 42 | private final ShapeHelper dupShape = new ShapeHelper(new ArrayList<Coord>(0)); |
43 | ||
44 | public ShapeMergeFilter(int resolution) { | |
43 | private final boolean orderByDecreasingArea; | |
44 | ||
45 | public ShapeMergeFilter(int resolution, boolean orderByDecreasingArea) { | |
45 | 46 | this.resolution = resolution; |
47 | this.orderByDecreasingArea = orderByDecreasingArea; | |
46 | 48 | } |
47 | 49 | |
48 | 50 | public List<MapShape> merge(List<MapShape> shapes) { |
83 | 85 | for (Map<MapShape, List<ShapeHelper>> lowMap : sameTypeList){ |
84 | 86 | boolean added = false; |
85 | 87 | for (MapShape ms: lowMap.keySet()){ |
88 | if (orderByDecreasingArea && ms.getFullArea() != shape.getFullArea()) | |
89 | // must not merge areas unless derived from same thing | |
90 | continue; | |
86 | 91 | // we do not use isSimilar() here, as it compares minRes and maxRes as well |
87 | 92 | String s1 = ms.getName(); |
88 | 93 | String s2 = shape.getName(); |
67 | 67 | try { |
68 | 68 | int key = Integer.parseInt(keyVal[0]); |
69 | 69 | if (key < 0 || key > 16) |
70 | throw new ExitException("Error: Level value out of range 0-16: " + s); | |
70 | throw new ExitException("Error: Level value out of range 0-16: " + s + " in levels specification " + levelSpec); | |
71 | 71 | int value = Integer.parseInt(keyVal[1]); |
72 | 72 | if (value <= 0 || value > 24) |
73 | throw new ExitException("Error: Resolution value out of range 0-24: " + s); | |
73 | throw new ExitException("Error: Resolution value out of range 0-24: " + s + " in levels specification " + levelSpec); | |
74 | 74 | levels[count] = new LevelInfo(key, value); |
75 | 75 | } catch (NumberFormatException e) { |
76 | 76 | throw new ExitException("Error: Levels specification not all numbers: " + levelSpec + " check " + s); |
14 | 14 | */ |
15 | 15 | package uk.me.parabola.mkgmap.general; |
16 | 16 | |
17 | import uk.me.parabola.imgfmt.app.Coord; | |
18 | import uk.me.parabola.mkgmap.filters.ShapeMergeFilter; | |
19 | ||
17 | 20 | /** |
18 | 21 | * A shape or polygon is just the same as a line really as far as I can tell. |
19 | 22 | * There are some things that you cannot do with them semantically. |
22 | 25 | */ |
23 | 26 | public class MapShape extends MapLine {// So top code can link objects from here |
24 | 27 | private long osmid; //TODO: remove debug aid |
28 | private long fullArea = Long.MAX_VALUE; // meaning unset | |
25 | 29 | public MapShape() { |
26 | 30 | osmid = 0; |
27 | 31 | } |
31 | 35 | MapShape(MapShape s) { |
32 | 36 | super(s); |
33 | 37 | this.osmid = s.osmid; |
38 | this.fullArea = s.getFullArea(); | |
34 | 39 | } |
35 | 40 | |
36 | 41 | public MapShape copy() { |
49 | 54 | public long getOsmid() { |
50 | 55 | return osmid; |
51 | 56 | } |
57 | ||
58 | public void setFullArea(long fullArea) { | |
59 | this.fullArea = fullArea; | |
60 | } | |
61 | ||
62 | public long getFullArea() { // this is unadulterated size, +ve if clockwise | |
63 | if (this.fullArea == Long.MAX_VALUE) { | |
64 | java.util.List<Coord> points = this.getPoints(); | |
65 | if (points.size() >= 4 && points.get(0).highPrecEquals(points.get(points.size()-1))) | |
66 | this.fullArea = ShapeMergeFilter.calcAreaSizeTestVal(points); | |
67 | } | |
68 | return this.fullArea; | |
69 | } | |
70 | ||
52 | 71 | } |
989 | 989 | elementSetup(shape, gt, way); |
990 | 990 | shape.setPoints(way.getPoints()); |
991 | 991 | |
992 | long areaVal = 0; | |
993 | String tagStringVal = way.getTag(drawLevelTagKey); | |
994 | if (tagStringVal != null) { | |
995 | try { | |
996 | areaVal = Integer.parseInt(tagStringVal); | |
997 | if (areaVal < 1 || areaVal > 100) { | |
998 | log.error("mkgmap:drawLevel must be in range 1..100, not", areaVal); | |
999 | areaVal = 0; | |
1000 | } else if (areaVal <= 50) | |
1001 | areaVal = Long.MAX_VALUE - areaVal; // 1 => MAX_VALUE-1, 50 => MAX_VALUE-50 | |
1002 | else | |
1003 | areaVal = 101 - areaVal; // 51 => 50, 100 => 1 | |
1004 | } catch (NumberFormatException e) { | |
1005 | log.error("mkgmap:drawLevel invalid integer:", tagStringVal); | |
1006 | } | |
1007 | } | |
1008 | if (areaVal == 0) | |
1009 | areaVal = way.getFullArea(); | |
1010 | shape.setFullArea(areaVal); | |
1011 | ||
992 | 1012 | clipper.clipShape(shape, collector); |
993 | 1013 | } |
994 | 1014 | |
1069 | 1089 | }; |
1070 | 1090 | private static final short highResOnlyTagKey = TagDict.getInstance().xlate("mkgmap:highest-resolution-only"); |
1071 | 1091 | private static final short skipSizeFilterTagKey = TagDict.getInstance().xlate("mkgmap:skipSizeFilter"); |
1092 | private static final short drawLevelTagKey = TagDict.getInstance().xlate("mkgmap:drawLevel"); | |
1072 | 1093 | |
1073 | 1094 | private static final short countryTagKey = TagDict.getInstance().xlate("mkgmap:country"); |
1074 | 1095 | private static final short regionTagKey = TagDict.getInstance().xlate("mkgmap:region"); |
101 | 101 | if (performChecks){ |
102 | 102 | boolean fromOverlays = false; |
103 | 103 | List<Integer> usedTypes = null; |
104 | if (gt.getMaxResolution() < levels[0].getBits()){ | |
104 | if (gt.getMaxResolution() < levels[0].getBits() || gt.getMaxResolution() > 24){ | |
105 | 105 | System.out.println("Warning: Object with max resolution of " + gt.getMaxResolution() + " is ignored. Check levels option and style file "+ ts.getFileName() + ", line " + ts.getLinenumber()); |
106 | } else if (gt.getMinResolution() > 24) { | |
107 | System.out.println("Warning: Object with min resolution of " + gt.getMinResolution() + " is ignored. Check levels option and style file "+ ts.getFileName() + ", line " + ts.getLinenumber()); | |
106 | 108 | } |
107 | 109 | if (overlays != null && kind == FeatureKind.POLYLINE){ |
108 | 110 | usedTypes = overlays.get(gt.getType()); |
3 | 3 | import java.text.DecimalFormatSymbols; |
4 | 4 | import java.util.Locale; |
5 | 5 | |
6 | import uk.me.parabola.imgfmt.app.Coord; | |
6 | 7 | import uk.me.parabola.mkgmap.reader.osm.Element; |
7 | 8 | import uk.me.parabola.mkgmap.reader.osm.MultiPolygonRelation; |
8 | 9 | import uk.me.parabola.mkgmap.reader.osm.Way; |
9 | 10 | |
10 | 11 | /** |
11 | 12 | * Calculates the area size of a polygon in garmin units ^ 2. |
13 | * | |
14 | * if orderByDecreasingArea then the area of the polygon has already been | |
15 | * calculated and we use it here. | |
16 | * To be totally consistent, ie no possible difference to mkgmap behaviour when | |
17 | * --order-by-decreasing-area is not set, this flag should be set from the option; | |
18 | * However it is now considered that 'fullArea' is what the user might expect, rather | |
19 | * than various different values for the same original because of clipping and cutting to | |
20 | * expose holes. | |
21 | * | |
12 | 22 | * @author WanMil |
13 | 23 | */ |
14 | 24 | public class AreaSizeFunction extends CachedFunction { |
15 | 25 | |
16 | 26 | private final DecimalFormat nf = new DecimalFormat("0.0#####################", DecimalFormatSymbols.getInstance(Locale.US)); |
27 | private final boolean orderByDecreasingArea = true; | |
17 | 28 | |
18 | 29 | public AreaSizeFunction() { |
19 | 30 | super(null); |
26 | 37 | if (w.hasEqualEndPoints() == false) { |
27 | 38 | return "0"; |
28 | 39 | } |
29 | return nf.format(MultiPolygonRelation.calcAreaSize(((Way) el).getPoints())); | |
40 | double areaSize; | |
41 | if (orderByDecreasingArea) { | |
42 | long fullArea = w.getFullArea(); | |
43 | if (fullArea == Long.MAX_VALUE) | |
44 | return "0"; | |
45 | // convert from high prec to value in map units | |
46 | areaSize = (double) fullArea / (2 * (1<<Coord.DELTA_SHIFT) * (1<<Coord.DELTA_SHIFT)); | |
47 | areaSize = Math.abs(areaSize); | |
48 | } else | |
49 | areaSize = MultiPolygonRelation.calcAreaSize(w.getPoints()); | |
50 | return nf.format(areaSize); | |
30 | 51 | } |
31 | 52 | return null; |
32 | 53 | } |
845 | 845 | |
846 | 846 | int wi = 0; |
847 | 847 | for (Way w : polygons) { |
848 | w.setFullArea(w.getFullArea()); // trigger setting area before start cutting... | |
849 | // do like this to disguise function with side effects | |
848 | 850 | String role = getRole(w); |
849 | 851 | if ("inner".equals(role)) { |
850 | 852 | innerPolygons.set(wi); |
1033 | 1035 | outmostPolygonProcessing = false; |
1034 | 1036 | } |
1035 | 1037 | |
1038 | long fullArea = currentPolygon.polygon.getFullArea(); | |
1036 | 1039 | for (Way mpWay : singularOuterPolygons) { |
1037 | 1040 | // put the cut out polygons to the |
1038 | 1041 | // final way map |
1039 | 1042 | if (log.isDebugEnabled()) |
1040 | 1043 | log.debug(mpWay.getId(),mpWay.toTagString()); |
1041 | 1044 | |
1045 | mpWay.setFullArea(fullArea); | |
1042 | 1046 | // mark this polygons so that only polygon style rules are applied |
1043 | 1047 | mpWay.addTag(STYLE_FILTER_TAG, STYLE_FILTER_POLYGON); |
1044 | 1048 | mpWay.addTag(MP_CREATED_TAG, "true"); |
102 | 102 | private static final int MIN_LON = Utils.toMapUnit(-180.0); |
103 | 103 | private static final int MAX_LON = Utils.toMapUnit(180.0); |
104 | 104 | private final static Pattern keySplitter = Pattern.compile(Pattern.quote("_")); |
105 | ||
105 | ||
106 | /** | |
107 | * When order-by-decreasing-area we need all bit of sea to be output consistently. | |
108 | * Unless _draworder changes things, having seaSize as BIG causes polygons beyond the | |
109 | * coastline to be shown. To hide these and have the sea show up to the high-tide | |
110 | * coastline, can set this to be very small instead (or use _draworder). | |
111 | * <p> | |
112 | * mkgmap:drawLevel can be used to override this value in the style - the default style has: | |
113 | * natural=sea { add mkgmap:skipSizeFilter=true; set mkgmap:drawLevel=2 } [0x32 resolution 10] | |
114 | * which is equivalent to Long.MAX_VALUE-2. | |
115 | */ | |
116 | private static final long seaSize = Long.MAX_VALUE-2; // sea is BIG | |
106 | 117 | |
107 | 118 | private static final List<Class<? extends LoadableMapDataSource>> precompSeaLoader; |
108 | 119 | |
708 | 719 | saver.addWay(w); |
709 | 720 | } |
710 | 721 | for (Way w : seaWays) { |
722 | w.setFullArea(seaSize); | |
711 | 723 | saver.addWay(w); |
712 | 724 | } |
713 | 725 | } else { |
717 | 729 | // first add the complete bounding box as sea |
718 | 730 | Way sea = new Way(FakeIdGenerator.makeFakeId(),bounds.toCoords()); |
719 | 731 | sea.addTag("natural", "sea"); |
732 | sea.setFullArea(seaSize); | |
720 | 733 | |
721 | 734 | for (Way w : landWays) { |
722 | 735 | saver.addWay(w); |
990 | 1003 | ne.getLongitude() + 1)); |
991 | 1004 | sea.addPoint(sea.getPoints().get(0)); // close shape |
992 | 1005 | sea.addTag("natural", "sea"); |
1006 | sea.setFullArea(seaSize); | |
993 | 1007 | |
994 | 1008 | log.info("sea: ", sea); |
995 | 1009 | saver.addWay(sea); |
23 | 23 | import uk.me.parabola.imgfmt.Utils; |
24 | 24 | import uk.me.parabola.imgfmt.app.Coord; |
25 | 25 | import uk.me.parabola.log.Logger; |
26 | import uk.me.parabola.mkgmap.filters.ShapeMergeFilter; | |
26 | 27 | |
27 | 28 | /** |
28 | 29 | * Represent a OSM way in the 0.5 api. A way consists of an ordered list of |
33 | 34 | public class Way extends Element { |
34 | 35 | private static final Logger log = Logger.getLogger(Way.class); |
35 | 36 | private final List<Coord> points; |
37 | private long fullArea = Long.MAX_VALUE; // meaning unset | |
36 | 38 | |
37 | 39 | // This will be set if a way is read from an OSM file and the first node is the same node as the last |
38 | 40 | // one in the way. This can be set to true even if there are missing nodes and so the nodes that we |
62 | 64 | dup.closedInOSM = this.closedInOSM; |
63 | 65 | dup.complete = this.complete; |
64 | 66 | dup.isViaWay = this.isViaWay; |
67 | dup.fullArea = this.getFullArea(); | |
65 | 68 | return dup; |
66 | 69 | } |
67 | 70 | |
251 | 254 | public void setViaWay(boolean isViaWay) { |
252 | 255 | this.isViaWay = isViaWay; |
253 | 256 | } |
257 | ||
258 | public void setFullArea(long fullArea) { | |
259 | this.fullArea = fullArea; | |
260 | } | |
261 | ||
262 | public long getFullArea() { // this is unadulterated size, +ve if clockwise | |
263 | if (this.fullArea == Long.MAX_VALUE && points.size() >= 4 && points.get(0).highPrecEquals(points.get(points.size()-1))) { | |
264 | this.fullArea = ShapeMergeFilter.calcAreaSizeTestVal(points); | |
265 | } | |
266 | return this.fullArea; | |
267 | } | |
268 | ||
254 | 269 | } |
364 | 364 | } |
365 | 365 | |
366 | 366 | void testOneVariant(String testId, MapShape s1, MapShape s2, int expectedNumShapes, int expectedNumPoints){ |
367 | ShapeMergeFilter smf = new ShapeMergeFilter(24); | |
367 | ShapeMergeFilter smf = new ShapeMergeFilter(24, false); | |
368 | 368 | List<MapShape> res = smf.merge(Arrays.asList(s1,s2)); |
369 | 369 | assertTrue(testId, res != null); |
370 | 370 | assertEquals(testId,expectedNumShapes, res.size() ); |