001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.mappaint.xml;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.io.InputStream;
007    import java.io.InputStreamReader;
008    import java.io.IOException;
009    import java.util.Collection;
010    import java.util.Collections;
011    import java.util.HashMap;
012    import java.util.LinkedList;
013    import java.util.List;
014    
015    import org.openstreetmap.josm.Main;
016    import org.openstreetmap.josm.data.osm.Node;
017    import org.openstreetmap.josm.data.osm.OsmPrimitive;
018    import org.openstreetmap.josm.data.osm.OsmUtils;
019    import org.openstreetmap.josm.data.osm.Relation;
020    import org.openstreetmap.josm.data.osm.Way;
021    import org.openstreetmap.josm.gui.mappaint.Cascade;
022    import org.openstreetmap.josm.gui.mappaint.Keyword;
023    import org.openstreetmap.josm.gui.mappaint.MultiCascade;
024    import org.openstreetmap.josm.gui.mappaint.Range;
025    import org.openstreetmap.josm.gui.mappaint.StyleKeys;
026    import org.openstreetmap.josm.gui.mappaint.StyleSource;
027    import org.openstreetmap.josm.gui.preferences.SourceEntry;
028    import org.openstreetmap.josm.io.MirroredInputStream;
029    import org.openstreetmap.josm.tools.Utils;
030    import org.openstreetmap.josm.tools.XmlObjectParser;
031    import org.xml.sax.SAXException;
032    import org.xml.sax.SAXParseException;
033    
034    public class XmlStyleSource extends StyleSource implements StyleKeys {
035    
036        protected final HashMap<String, IconPrototype> icons = new HashMap<String, IconPrototype>();
037        protected final HashMap<String, LinePrototype> lines = new HashMap<String, LinePrototype>();
038        protected final HashMap<String, LinemodPrototype> modifiers = new HashMap<String, LinemodPrototype>();
039        protected final HashMap<String, AreaPrototype> areas = new HashMap<String, AreaPrototype>();
040        protected final LinkedList<IconPrototype> iconsList = new LinkedList<IconPrototype>();
041        protected final LinkedList<LinePrototype> linesList = new LinkedList<LinePrototype>();
042        protected final LinkedList<LinemodPrototype> modifiersList = new LinkedList<LinemodPrototype>();
043        protected final LinkedList<AreaPrototype> areasList = new LinkedList<AreaPrototype>();
044    
045        public XmlStyleSource(String url, String name, String shortdescription) {
046            super(url, name, shortdescription);
047        }
048    
049        public XmlStyleSource(SourceEntry entry) {
050            super(entry);
051        }
052    
053        protected void init() {
054            super.init();
055            icons.clear();
056            lines.clear();
057            modifiers.clear();
058            areas.clear();
059            iconsList.clear();
060            linesList.clear();
061            modifiersList.clear();
062            areasList.clear();
063        }
064    
065        @Override
066        public void loadStyleSource() {
067            init();
068            try {
069                InputStreamReader reader = new InputStreamReader(getSourceInputStream());
070                XmlObjectParser parser = new XmlObjectParser(new XmlStyleSourceHandler(this));
071                parser.startWithValidation(reader,
072                        "http://josm.openstreetmap.de/mappaint-style-1.0",
073                        "resource://data/mappaint-style.xsd");
074                while(parser.hasNext()) {
075                }
076    
077            } catch(IOException e) {
078                System.err.println(tr("Warning: failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
079                e.printStackTrace();
080                logError(e);
081            } catch(SAXParseException e) {
082                System.err.println(tr("Warning: failed to parse Mappaint styles from ''{0}''. Error was: [{1}:{2}] {3}", url, e.getLineNumber(), e.getColumnNumber(), e.getMessage()));
083                e.printStackTrace();
084                logError(e);
085            } catch(SAXException e) {
086                System.err.println(tr("Warning: failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
087                e.printStackTrace();
088                logError(e);
089            }
090        }
091    
092        public InputStream getSourceInputStream() throws IOException {
093            MirroredInputStream in = new MirroredInputStream(url);
094            InputStream zip = in.getZipEntry("xml", "style");
095            if (zip != null) {
096                zipIcons = in.getFile();
097                return zip;
098            } else {
099                zipIcons = null;
100                return in;
101            }
102        }
103    
104        private static class WayPrototypesRecord {
105            public LinePrototype line;
106            public List<LinemodPrototype> linemods;
107            public AreaPrototype area;
108        }
109    
110        private <T extends Prototype> T update(T current, T candidate, Double scale, MultiCascade mc) {
111            if (requiresUpdate(current, candidate, scale, mc))
112                return candidate;
113            else
114                return current;
115        }
116    
117        /**
118         * checks whether a certain match is better than the current match
119         * @param current can be null
120         * @param candidate the new Prototype that could be used instead
121         * @param scale ignored if null, otherwise checks if scale is within the range of candidate
122         * @param mc side effect: update the valid region for the current MultiCascade
123         */
124        private boolean requiresUpdate(Prototype current, Prototype candidate, Double scale, MultiCascade mc) {
125            if (current == null || candidate.priority >= current.priority) {
126                if (scale == null)
127                    return true;
128    
129                if (candidate.range.contains(scale)) {
130                    mc.range = Range.cut(mc.range, candidate.range);
131                    return true;
132                } else {
133                    mc.range = mc.range.reduceAround(scale, candidate.range);
134                    return false;
135                }
136            }
137            return false;
138        }
139    
140        private IconPrototype getNode(OsmPrimitive primitive, Double scale, MultiCascade mc) {
141            IconPrototype icon = null;
142            for (String key : primitive.keySet()) {
143                String val = primitive.get(key);
144                IconPrototype p;
145                if ((p = icons.get("n" + key + "=" + val)) != null) {
146                    icon = update(icon, p, scale, mc);
147                }
148                if ((p = icons.get("b" + key + "=" + OsmUtils.getNamedOsmBoolean(val))) != null) {
149                    icon = update(icon, p, scale, mc);
150                }
151                if ((p = icons.get("x" + key)) != null) {
152                    icon = update(icon, p, scale, mc);
153                }
154            }
155            for (IconPrototype s : iconsList) {
156                if (s.check(primitive))
157                {
158                    icon = update(icon, s, scale, mc);
159                }
160            }
161            return icon;
162        }
163    
164        /**
165         * @param closed The primitive is a closed way or we pretend it is closed.
166         *  This is useful for multipolygon relations and outer ways of untagged
167         *  multipolygon relations.
168         */
169        private void get(OsmPrimitive primitive, boolean closed, WayPrototypesRecord p, Double scale, MultiCascade mc) {
170            String lineIdx = null;
171            HashMap<String, LinemodPrototype> overlayMap = new HashMap<String, LinemodPrototype>();
172            boolean isNotArea = OsmUtils.isFalse(primitive.get("area"));
173            for (String key : primitive.keySet()) {
174                String val = primitive.get(key);
175                AreaPrototype styleArea;
176                LinePrototype styleLine;
177                LinemodPrototype styleLinemod;
178                String idx = "n" + key + "=" + val;
179                if ((styleArea = areas.get(idx)) != null && (closed || !styleArea.closed) && !isNotArea) {
180                    p.area = update(p.area, styleArea, scale, mc);
181                }
182                if ((styleLine = lines.get(idx)) != null) {
183                    if (requiresUpdate(p.line, styleLine, scale, mc)) {
184                        p.line = styleLine;
185                        lineIdx = idx;
186                    }
187                }
188                if ((styleLinemod = modifiers.get(idx)) != null) {
189                    if (requiresUpdate(null, styleLinemod, scale, mc)) {
190                        overlayMap.put(idx, styleLinemod);
191                    }
192                }
193                idx = "b" + key + "=" + OsmUtils.getNamedOsmBoolean(val);
194                if ((styleArea = areas.get(idx)) != null && (closed || !styleArea.closed) && !isNotArea) {
195                    p.area = update(p.area, styleArea, scale, mc);
196                }
197                if ((styleLine = lines.get(idx)) != null) {
198                    if (requiresUpdate(p.line, styleLine, scale, mc)) {
199                        p.line = styleLine;
200                        lineIdx = idx;
201                    }
202                }
203                if ((styleLinemod = modifiers.get(idx)) != null) {
204                    if (requiresUpdate(null, styleLinemod, scale, mc)) {
205                        overlayMap.put(idx, styleLinemod);
206                    }
207                }
208                idx = "x" + key;
209                if ((styleArea = areas.get(idx)) != null && (closed || !styleArea.closed) && !isNotArea) {
210                    p.area = update(p.area, styleArea, scale, mc);
211                }
212                if ((styleLine = lines.get(idx)) != null) {
213                    if (requiresUpdate(p.line, styleLine, scale, mc)) {
214                        p.line = styleLine;
215                        lineIdx = idx;
216                    }
217                }
218                if ((styleLinemod = modifiers.get(idx)) != null) {
219                    if (requiresUpdate(null, styleLinemod, scale, mc)) {
220                        overlayMap.put(idx, styleLinemod);
221                    }
222                }
223            }
224            for (AreaPrototype s : areasList) {
225                if ((closed || !s.closed) && !isNotArea && s.check(primitive)) {
226                    p.area = update(p.area, s, scale, mc);
227                }
228            }
229            for (LinePrototype s : linesList) {
230                if (s.check(primitive)) {
231                    p.line = update(p.line, s, scale, mc);
232                }
233            }
234            for (LinemodPrototype s : modifiersList) {
235                if (s.check(primitive)) {
236                    if (requiresUpdate(null, s, scale, mc)) {
237                        overlayMap.put(s.getCode(), s);
238                    }
239                }
240            }
241            overlayMap.remove(lineIdx); // do not use overlay if linestyle is from the same rule (example: railway=tram)
242            if (!overlayMap.isEmpty()) {
243                List<LinemodPrototype> tmp = new LinkedList<LinemodPrototype>();
244                if (p.linemods != null) {
245                    tmp.addAll(p.linemods);
246                }
247                tmp.addAll(overlayMap.values());
248                Collections.sort(tmp);
249                p.linemods = tmp;
250            }
251        }
252    
253        public void add(XmlCondition c, Collection<XmlCondition> conditions, Prototype prot) {
254             if(conditions != null)
255             {
256                prot.conditions = conditions;
257                if (prot instanceof IconPrototype) {
258                    iconsList.add((IconPrototype) prot);
259                } else if (prot instanceof LinemodPrototype) {
260                    modifiersList.add((LinemodPrototype) prot);
261                } else if (prot instanceof LinePrototype) {
262                    linesList.add((LinePrototype) prot);
263                } else if (prot instanceof AreaPrototype) {
264                    areasList.add((AreaPrototype) prot);
265                } else
266                    throw new RuntimeException();
267             }
268             else {
269                 String key = c.getKey();
270                prot.code = key;
271                if (prot instanceof IconPrototype) {
272                    icons.put(key, (IconPrototype) prot);
273                } else if (prot instanceof LinemodPrototype) {
274                   modifiers.put(key, (LinemodPrototype) prot);
275                } else if (prot instanceof LinePrototype) {
276                    lines.put(key, (LinePrototype) prot);
277                } else if (prot instanceof AreaPrototype) {
278                    areas.put(key, (AreaPrototype) prot);
279                } else
280                    throw new RuntimeException();
281             }
282         }
283    
284        @Override
285        public void apply(MultiCascade mc, OsmPrimitive osm, double scale, OsmPrimitive multipolyOuterWay, boolean pretendWayIsClosed) {
286            Cascade def = mc.getOrCreateCascade("default");
287            boolean useMinMaxScale = Main.pref.getBoolean("mappaint.zoomLevelDisplay", false);
288    
289            if (osm instanceof Node || (osm instanceof Relation && "restriction".equals(osm.get("type")))) {
290                IconPrototype icon = getNode(osm, (useMinMaxScale ? scale : null), mc);
291                if (icon != null) {
292                    def.put(ICON_IMAGE, icon.icon);
293                    if (osm instanceof Node) {
294                        if (icon.annotate != null) {
295                            if (icon.annotate) {
296                                def.put(TEXT, Keyword.AUTO);
297                            } else {
298                                def.remove(TEXT);
299                            }
300                        }
301                    }
302                }
303            } else if (osm instanceof Way || (osm instanceof Relation && ((Relation)osm).isMultipolygon())) {
304                WayPrototypesRecord p = new WayPrototypesRecord();
305                get(osm, pretendWayIsClosed || !(osm instanceof Way) || ((Way) osm).isClosed(), p, (useMinMaxScale ? scale : null), mc);
306                if (p.line != null) {
307                    def.put(WIDTH, new Float(p.line.getWidth()));
308                    def.putOrClear(REAL_WIDTH, p.line.realWidth != null ? new Float(p.line.realWidth) : null);
309                    def.putOrClear(COLOR, p.line.color);
310                    if (p.line.color != null) {
311                        int alpha = p.line.color.getAlpha();
312                        if (alpha != 255) {
313                            def.put(OPACITY, Utils.color_int2float(alpha));
314                        }
315                    }
316                    def.putOrClear(DASHES, p.line.getDashed());
317                    def.putOrClear(DASHES_BACKGROUND_COLOR, p.line.dashedColor);
318                }
319                Float refWidth = def.get(WIDTH, null, Float.class);
320                if (refWidth != null && p.linemods != null) {
321                    int numOver = 0, numUnder = 0;
322    
323                    while (mc.hasLayer(String.format("over_%d", ++numOver))) {}
324                    while (mc.hasLayer(String.format("under_%d", ++numUnder))) {}
325    
326                    for (LinemodPrototype mod : p.linemods) {
327                        Cascade c;
328                        if (mod.over) {
329                            String layer = String.format("over_%d", numOver);
330                            c = mc.getOrCreateCascade(layer);
331                            c.put(OBJECT_Z_INDEX, new Float(numOver));
332                            ++numOver;
333                        } else {
334                            String layer = String.format("under_%d", numUnder);
335                            c = mc.getOrCreateCascade(layer);
336                            c.put(OBJECT_Z_INDEX, new Float(-numUnder));
337                            ++numUnder;
338                        }
339                        c.put(WIDTH, new Float(mod.getWidth(refWidth)));
340                        c.putOrClear(COLOR, mod.color);
341                        if (mod.color != null) {
342                            int alpha = mod.color.getAlpha();
343                            if (alpha != 255) {
344                                c.put(OPACITY, Utils.color_int2float(alpha));
345                            }
346                        }
347                        c.putOrClear(DASHES, mod.getDashed());
348                        c.putOrClear(DASHES_BACKGROUND_COLOR, mod.dashedColor);
349                    }
350                }
351                if (multipolyOuterWay != null) {
352                    WayPrototypesRecord p2 = new WayPrototypesRecord();
353                    get(multipolyOuterWay, true, p2, (useMinMaxScale ? scale : null), mc);
354                    if (Utils.equal(p.area, p2.area)) {
355                        p.area = null;
356                    }
357                }
358                if (p.area != null) {
359                    def.putOrClear(FILL_COLOR, p.area.color);
360                    def.putOrClear(TEXT_POSITION, Keyword.CENTER);
361                    def.putOrClear(TEXT, Keyword.AUTO);
362                    def.remove(FILL_IMAGE);
363                }
364            }
365        }
366    
367    }