source: josm/trunk/src/org/openstreetmap/josm/data/osm/visitor/paint/relations/Multipolygon.java@ 5176

Last change on this file since 5176 was 5176, checked in by Don-vip, 12 years ago

fix #7593 - IllegalPathStateException when attempting to purge an item in a Multipolygon

  • Property svn:eol-style set to native
File size: 21.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.visitor.paint.relations;
3
4import java.awt.geom.Path2D;
5import java.awt.geom.Path2D.Double;
6import java.awt.geom.PathIterator;
7import java.awt.geom.Point2D;
8import java.awt.geom.Rectangle2D;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.Iterator;
14import java.util.List;
15import java.util.Set;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
19import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.RelationMember;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
27import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
28import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
29
30public class Multipolygon {
31 /** preference key for a collection of roles which indicate that the respective member belongs to an
32 * <em>outer</em> polygon. Default is <tt>outer</tt>.
33 */
34 static public final String PREF_KEY_OUTER_ROLES = "mappaint.multipolygon.outer.roles";
35 /** preference key for collection of role prefixes which indicate that the respective
36 * member belongs to an <em>outer</em> polygon. Default is empty.
37 */
38 static public final String PREF_KEY_OUTER_ROLE_PREFIXES = "mappaint.multipolygon.outer.role-prefixes";
39 /** preference key for a collection of roles which indicate that the respective member belongs to an
40 * <em>inner</em> polygon. Default is <tt>inner</tt>.
41 */
42 static public final String PREF_KEY_INNER_ROLES = "mappaint.multipolygon.inner.roles";
43 /** preference key for collection of role prefixes which indicate that the respective
44 * member belongs to an <em>inner</em> polygon. Default is empty.
45 */
46 static public final String PREF_KEY_INNER_ROLE_PREFIXES = "mappaint.multipolygon.inner.role-prefixes";
47
48 /**
49 * <p>Kind of strategy object which is responsible for deciding whether a given
50 * member role indicates that the member belongs to an <em>outer</em> or an
51 * <em>inner</em> polygon.</p>
52 *
53 * <p>The decision is taken based on preference settings, see the four preference keys
54 * above.</p>
55 *
56 */
57 private static class MultipolygonRoleMatcher implements PreferenceChangedListener{
58 private final List<String> outerExactRoles = new ArrayList<String>();
59 private final List<String> outerRolePrefixes = new ArrayList<String>();
60 private final List<String> innerExactRoles = new ArrayList<String>();
61 private final List<String> innerRolePrefixes = new ArrayList<String>();
62
63 private void initDefaults() {
64 outerExactRoles.clear();
65 outerRolePrefixes.clear();
66 innerExactRoles.clear();
67 innerRolePrefixes.clear();
68 outerExactRoles.add("outer");
69 innerExactRoles.add("inner");
70 }
71
72 private void setNormalized(Collection<String> literals, List<String> target){
73 target.clear();
74 for(String l: literals) {
75 if (l == null) {
76 continue;
77 }
78 l = l.trim();
79 if (!target.contains(l)) {
80 target.add(l);
81 }
82 }
83 }
84
85 private void initFromPreferences() {
86 initDefaults();
87 if (Main.pref == null) return;
88 Collection<String> literals;
89 literals = Main.pref.getCollection(PREF_KEY_OUTER_ROLES);
90 if (literals != null && !literals.isEmpty()){
91 setNormalized(literals, outerExactRoles);
92 }
93 literals = Main.pref.getCollection(PREF_KEY_OUTER_ROLE_PREFIXES);
94 if (literals != null && !literals.isEmpty()){
95 setNormalized(literals, outerRolePrefixes);
96 }
97 literals = Main.pref.getCollection(PREF_KEY_INNER_ROLES);
98 if (literals != null && !literals.isEmpty()){
99 setNormalized(literals, innerExactRoles);
100 }
101 literals = Main.pref.getCollection(PREF_KEY_INNER_ROLE_PREFIXES);
102 if (literals != null && !literals.isEmpty()){
103 setNormalized(literals, innerRolePrefixes);
104 }
105 }
106
107 @Override
108 public void preferenceChanged(PreferenceChangeEvent evt) {
109 if (PREF_KEY_INNER_ROLE_PREFIXES.equals(evt.getKey()) ||
110 PREF_KEY_INNER_ROLES.equals(evt.getKey()) ||
111 PREF_KEY_OUTER_ROLE_PREFIXES.equals(evt.getKey()) ||
112 PREF_KEY_OUTER_ROLES.equals(evt.getKey())){
113 initFromPreferences();
114 }
115 }
116
117 public boolean isOuterRole(String role){
118 if (role == null) return false;
119 for (String candidate: outerExactRoles) {
120 if (role.equals(candidate)) return true;
121 }
122 for (String candidate: outerRolePrefixes) {
123 if (role.startsWith(candidate)) return true;
124 }
125 return false;
126 }
127
128 public boolean isInnerRole(String role){
129 if (role == null) return false;
130 for (String candidate: innerExactRoles) {
131 if (role.equals(candidate)) return true;
132 }
133 for (String candidate: innerRolePrefixes) {
134 if (role.startsWith(candidate)) return true;
135 }
136 return false;
137 }
138 }
139
140 /*
141 * Init a private global matcher object which will listen to preference
142 * changes.
143 */
144 private static MultipolygonRoleMatcher roleMatcher;
145 private static MultipolygonRoleMatcher getMultipolygonRoleMatcher() {
146 if (roleMatcher == null) {
147 roleMatcher = new MultipolygonRoleMatcher();
148 if (Main.pref != null){
149 roleMatcher.initFromPreferences();
150 Main.pref.addPreferenceChangeListener(roleMatcher);
151 }
152 }
153 return roleMatcher;
154 }
155
156 public static class JoinedWay {
157 private final List<Node> nodes;
158 private final Collection<Long> wayIds;
159 private final boolean selected;
160
161 public JoinedWay(List<Node> nodes, Collection<Long> wayIds, boolean selected) {
162 this.nodes = nodes;
163 this.wayIds = wayIds;
164 this.selected = selected;
165 }
166
167 public List<Node> getNodes() {
168 return nodes;
169 }
170
171 public Collection<Long> getWayIds() {
172 return wayIds;
173 }
174
175 public boolean isSelected() {
176 return selected;
177 }
178
179 public boolean isClosed() {
180 return nodes.isEmpty() || nodes.get(nodes.size() - 1).equals(nodes.get(0));
181 }
182 }
183
184 public static class PolyData {
185 public enum Intersection {INSIDE, OUTSIDE, CROSSING}
186
187 private final Path2D.Double poly;
188 public boolean selected;
189 private Rectangle2D bounds;
190 private final Collection<Long> wayIds;
191 private final List<Node> nodes;
192 private final List<PolyData> inners;
193
194 public PolyData(Way closedWay) {
195 this(closedWay.getNodes(), closedWay.isSelected(), Collections.singleton(closedWay.getUniqueId()));
196 }
197
198 public PolyData(JoinedWay joinedWay) {
199 this(joinedWay.getNodes(), joinedWay.isSelected(), joinedWay.getWayIds());
200 }
201
202 private PolyData(List<Node> nodes, boolean selected, Collection<Long> wayIds) {
203 this.wayIds = Collections.unmodifiableCollection(wayIds);
204 this.nodes = new ArrayList<Node>(nodes);
205 this.selected = selected;
206 this.inners = new ArrayList<Multipolygon.PolyData>();
207 this.poly = new Path2D.Double();
208 this.poly.setWindingRule(Path2D.WIND_EVEN_ODD);
209 buildPoly();
210 }
211
212 private void buildPoly() {
213 boolean initial = true;
214 for (Node n : nodes) {
215 Point2D p = n.getEastNorth();
216 if (initial) {
217 poly.moveTo(p.getX(), p.getY());
218 initial = false;
219 } else {
220 poly.lineTo(p.getX(), p.getY());
221 }
222 }
223 if (!initial) { // fix #7593
224 poly.closePath();
225 }
226 for (PolyData inner : inners) {
227 appendInner(inner.poly);
228 }
229 }
230
231 public PolyData(PolyData copy) {
232 this.selected = copy.selected;
233 this.poly = (Double) copy.poly.clone();
234 this.wayIds = Collections.unmodifiableCollection(copy.wayIds);
235 this.nodes = new ArrayList<Node>(copy.nodes);
236 this.inners = new ArrayList<Multipolygon.PolyData>(copy.inners);
237 }
238
239 public Intersection contains(Path2D.Double p) {
240 int contains = 0;
241 int total = 0;
242 double[] coords = new double[6];
243 for (PathIterator it = p.getPathIterator(null); !it.isDone(); it.next()) {
244 switch (it.currentSegment(coords)) {
245 case PathIterator.SEG_MOVETO:
246 case PathIterator.SEG_LINETO:
247 if (poly.contains(coords[0], coords[1])) {
248 contains++;
249 }
250 total++;
251 }
252 }
253 if (contains == total) return Intersection.INSIDE;
254 if (contains == 0) return Intersection.OUTSIDE;
255 return Intersection.CROSSING;
256 }
257
258 public void addInner(PolyData inner) {
259 inners.add(inner);
260 appendInner(inner.poly);
261 }
262
263 private void appendInner(Path2D.Double inner) {
264 poly.append(inner.getPathIterator(null), false);
265 }
266
267 public Path2D.Double get() {
268 return poly;
269 }
270
271 public Rectangle2D getBounds() {
272 if (bounds == null) {
273 bounds = poly.getBounds2D();
274 }
275 return bounds;
276 }
277
278 public Collection<Long> getWayIds() {
279 return wayIds;
280 }
281
282 private void resetNodes(DataSet dataSet) {
283 if (!nodes.isEmpty()) {
284 DataSet ds = dataSet;
285 // Find DataSet (can be null for several nodes when undoing nodes creation, see #7162)
286 for (Iterator<Node> it = nodes.iterator(); it.hasNext() && ds == null; ) {
287 ds = it.next().getDataSet();
288 }
289 nodes.clear();
290 if (ds == null) {
291 // DataSet still not found. This should not happen, but a warning does no harm
292 System.err.println("Warning: DataSet not found while resetting nodes in Multipolygon. This should not happen, you may report it to JOSM developers.");
293 } else if (wayIds.size() == 1) {
294 Way w = (Way) ds.getPrimitiveById(wayIds.iterator().next(), OsmPrimitiveType.WAY);
295 nodes.addAll(w.getNodes());
296 } else {
297 List<Way> waysToJoin = new ArrayList<Way>();
298 for (Iterator<Long> it = wayIds.iterator(); it.hasNext(); ) {
299 Way w = (Way) ds.getPrimitiveById(it.next(), OsmPrimitiveType.WAY);
300 if (w != null && w.getNodesCount() > 0) { // fix #7173 (empty ways on purge)
301 waysToJoin.add(w);
302 }
303 }
304 nodes.addAll(joinWays(waysToJoin).iterator().next().getNodes());
305 }
306 resetPoly();
307 }
308 }
309
310 private void resetPoly() {
311 poly.reset();
312 buildPoly();
313 bounds = null;
314 }
315
316 public void nodeMoved(NodeMovedEvent event) {
317 final Node n = event.getNode();
318 boolean innerChanged = false;
319 for (PolyData inner : inners) {
320 if (inner.nodes.contains(n)) {
321 inner.resetPoly();
322 innerChanged = true;
323 }
324 }
325 if (nodes.contains(n) || innerChanged) {
326 resetPoly();
327 }
328 }
329
330 public void wayNodesChanged(WayNodesChangedEvent event) {
331 final Long wayId = event.getChangedWay().getUniqueId();
332 boolean innerChanged = false;
333 for (PolyData inner : inners) {
334 if (inner.wayIds.contains(wayId)) {
335 inner.resetNodes(event.getDataset());
336 innerChanged = true;
337 }
338 }
339 if (wayIds.contains(wayId) || innerChanged) {
340 resetNodes(event.getDataset());
341 }
342 }
343 }
344
345 private final List<Way> innerWays = new ArrayList<Way>();
346 private final List<Way> outerWays = new ArrayList<Way>();
347 private final List<PolyData> innerPolygons = new ArrayList<PolyData>();
348 private final List<PolyData> outerPolygons = new ArrayList<PolyData>();
349 private final List<PolyData> combinedPolygons = new ArrayList<PolyData>();
350
351 private boolean incomplete;
352
353 public Multipolygon(Relation r) {
354 load(r);
355 }
356
357 private void load(Relation r) {
358 MultipolygonRoleMatcher matcher = getMultipolygonRoleMatcher();
359
360 // Fill inner and outer list with valid ways
361 for (RelationMember m : r.getMembers()) {
362 if (m.getMember().isIncomplete()) {
363 this.incomplete = true;
364 } else if (m.getMember().isDrawable()) {
365 if (m.isWay()) {
366 Way w = m.getWay();
367
368 if (w.getNodesCount() < 2) {
369 continue;
370 }
371
372 if (matcher.isInnerRole(m.getRole())) {
373 innerWays.add(w);
374 } else if (matcher.isOuterRole(m.getRole())) {
375 outerWays.add(w);
376 } else if (!m.hasRole()) {
377 outerWays.add(w);
378 } // Remaining roles ignored
379 } // Non ways ignored
380 }
381 }
382
383 createPolygons(innerWays, innerPolygons);
384 createPolygons(outerWays, outerPolygons);
385 if (!outerPolygons.isEmpty()) {
386 addInnerToOuters();
387 }
388 }
389
390 public final boolean isIncomplete() {
391 return incomplete;
392 }
393
394 private void createPolygons(List<Way> ways, List<PolyData> result) {
395 List<Way> waysToJoin = new ArrayList<Way>();
396 for (Way way: ways) {
397 if (way.isClosed()) {
398 result.add(new PolyData(way));
399 } else {
400 waysToJoin.add(way);
401 }
402 }
403
404 for (JoinedWay jw: joinWays(waysToJoin)) {
405 result.add(new PolyData(jw));
406 }
407 }
408
409 public static Collection<JoinedWay> joinWays(Collection<Way> waysToJoin)
410 {
411 final Collection<JoinedWay> result = new ArrayList<JoinedWay>();
412 final Way[] joinArray = waysToJoin.toArray(new Way[waysToJoin.size()]);
413 int left = waysToJoin.size();
414 while (left > 0) {
415 Way w = null;
416 boolean selected = false;
417 List<Node> nodes = null;
418 Set<Long> wayIds = new HashSet<Long>();
419 boolean joined = true;
420 while (joined && left > 0) {
421 joined = false;
422 for (int i = 0; i < joinArray.length && left != 0; ++i) {
423 if (joinArray[i] != null) {
424 Way c = joinArray[i];
425 if (w == null) {
426 w = c;
427 selected = w.isSelected();
428 joinArray[i] = null;
429 --left;
430 } else {
431 int mode = 0;
432 int cl = c.getNodesCount()-1;
433 int nl;
434 if (nodes == null) {
435 nl = w.getNodesCount()-1;
436 if (w.getNode(nl) == c.getNode(0)) {
437 mode = 21;
438 } else if (w.getNode(nl) == c.getNode(cl)) {
439 mode = 22;
440 } else if (w.getNode(0) == c.getNode(0)) {
441 mode = 11;
442 } else if (w.getNode(0) == c.getNode(cl)) {
443 mode = 12;
444 }
445 } else {
446 nl = nodes.size()-1;
447 if (nodes.get(nl) == c.getNode(0)) {
448 mode = 21;
449 } else if (nodes.get(0) == c.getNode(cl)) {
450 mode = 12;
451 } else if (nodes.get(0) == c.getNode(0)) {
452 mode = 11;
453 } else if (nodes.get(nl) == c.getNode(cl)) {
454 mode = 22;
455 }
456 }
457 if (mode != 0) {
458 joinArray[i] = null;
459 joined = true;
460 if (c.isSelected()) {
461 selected = true;
462 }
463 --left;
464 if (nodes == null) {
465 nodes = w.getNodes();
466 wayIds.add(w.getUniqueId());
467 }
468 nodes.remove((mode == 21 || mode == 22) ? nl : 0);
469 if (mode == 21) {
470 nodes.addAll(c.getNodes());
471 } else if (mode == 12) {
472 nodes.addAll(0, c.getNodes());
473 } else if (mode == 22) {
474 for (Node node : c.getNodes()) {
475 nodes.add(nl, node);
476 }
477 } else /* mode == 11 */ {
478 for (Node node : c.getNodes()) {
479 nodes.add(0, node);
480 }
481 }
482 wayIds.add(c.getUniqueId());
483 }
484 }
485 }
486 } /* for(i = ... */
487 } /* while(joined) */
488
489 if (nodes == null) {
490 nodes = w.getNodes();
491 wayIds.add(w.getUniqueId());
492 }
493
494 result.add(new JoinedWay(nodes, wayIds, selected));
495 } /* while(left != 0) */
496
497 return result;
498 }
499
500 public PolyData findOuterPolygon(PolyData inner, List<PolyData> outerPolygons) {
501
502 // First try to test only bbox, use precise testing only if we don't get unique result
503 Rectangle2D innerBox = inner.getBounds();
504 PolyData insidePolygon = null;
505 PolyData intersectingPolygon = null;
506 int insideCount = 0;
507 int intersectingCount = 0;
508
509 for (PolyData outer: outerPolygons) {
510 if (outer.getBounds().contains(innerBox)) {
511 insidePolygon = outer;
512 insideCount++;
513 } else if (outer.getBounds().intersects(innerBox)) {
514 intersectingPolygon = outer;
515 intersectingCount++;
516 }
517 }
518
519 if (insideCount == 1)
520 return insidePolygon;
521 else if (intersectingCount == 1)
522 return intersectingPolygon;
523
524 PolyData result = null;
525 for (PolyData combined : outerPolygons) {
526 Intersection c = combined.contains(inner.poly);
527 if (c != Intersection.OUTSIDE)
528 {
529 if (result == null || result.contains(combined.poly) != Intersection.INSIDE) {
530 result = combined;
531 }
532 }
533 }
534 return result;
535 }
536
537 private void addInnerToOuters() {
538
539 if (innerPolygons.isEmpty()) {
540 combinedPolygons.addAll(outerPolygons);
541 } else if (outerPolygons.size() == 1) {
542 PolyData combinedOuter = new PolyData(outerPolygons.get(0));
543 for (PolyData inner: innerPolygons) {
544 combinedOuter.addInner(inner);
545 }
546 combinedPolygons.add(combinedOuter);
547 } else {
548 for (PolyData outer: outerPolygons) {
549 combinedPolygons.add(new PolyData(outer));
550 }
551
552 for (PolyData pdInner: innerPolygons) {
553 PolyData o = findOuterPolygon(pdInner, combinedPolygons);
554 if (o == null) {
555 o = outerPolygons.get(0);
556 }
557 o.addInner(pdInner);
558 }
559 }
560
561 // Clear inner and outer polygons to reduce memory footprint
562 innerPolygons.clear();
563 outerPolygons.clear();
564 }
565
566 public List<Way> getOuterWays() {
567 return outerWays;
568 }
569
570 public List<Way> getInnerWays() {
571 return innerWays;
572 }
573/*
574 public List<PolyData> getInnerPolygons() {
575 return innerPolygons;
576 }
577
578 public List<PolyData> getOuterPolygons() {
579 return outerPolygons;
580 }
581*/
582 public List<PolyData> getCombinedPolygons() {
583 return combinedPolygons;
584 }
585}
Note: See TracBrowser for help on using the repository browser.