001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.mappaint.mapcss;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.io.ByteArrayInputStream;
008import java.io.File;
009import java.io.IOException;
010import java.io.InputStream;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Map.Entry;
014import java.util.zip.ZipEntry;
015import java.util.zip.ZipFile;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.gui.mappaint.Cascade;
021import org.openstreetmap.josm.gui.mappaint.Environment;
022import org.openstreetmap.josm.gui.mappaint.MultiCascade;
023import org.openstreetmap.josm.gui.mappaint.Range;
024import org.openstreetmap.josm.gui.mappaint.StyleSource;
025import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
026import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
027import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
028import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
029import org.openstreetmap.josm.gui.preferences.SourceEntry;
030import org.openstreetmap.josm.io.MirroredInputStream;
031import org.openstreetmap.josm.tools.CheckParameterUtil;
032import org.openstreetmap.josm.tools.LanguageInfo;
033import org.openstreetmap.josm.tools.Utils;
034
035public class MapCSSStyleSource extends StyleSource {
036    final public List<MapCSSRule> rules;
037    private Color backgroundColorOverride;
038    private String css = null;
039    private ZipFile zipFile;
040
041    public MapCSSStyleSource(String url, String name, String shortdescription) {
042        super(url, name, shortdescription);
043        rules = new ArrayList<MapCSSRule>();
044    }
045
046    public MapCSSStyleSource(SourceEntry entry) {
047        super(entry);
048        rules = new ArrayList<MapCSSRule>();
049    }
050
051    /**
052     * <p>Creates a new style source from the MapCSS styles supplied in
053     * {@code css}</p>
054     *
055     * @param css the MapCSS style declaration. Must not be null.
056     * @throws IllegalArgumentException thrown if {@code css} is null
057     */
058    public MapCSSStyleSource(String css) throws IllegalArgumentException{
059        super(null, null, null);
060        CheckParameterUtil.ensureParameterNotNull(css);
061        this.css = css;
062        rules = new ArrayList<MapCSSRule>();
063    }
064
065    @Override
066    public void loadStyleSource() {
067        init();
068        rules.clear();
069        try {
070            InputStream in = getSourceInputStream();
071            try {
072                MapCSSParser parser = new MapCSSParser(in, "UTF-8");
073                parser.sheet(this);
074                loadMeta();
075                loadCanvas();
076            } finally {
077                closeSourceInputStream(in);
078            }
079        } catch (IOException e) {
080            Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
081            e.printStackTrace();
082            logError(e);
083        } catch (TokenMgrError e) {
084            Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
085            e.printStackTrace();
086            logError(e);
087        } catch (ParseException e) {
088            Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
089            e.printStackTrace();
090            logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream
091        }
092    }
093
094    @Override
095    public InputStream getSourceInputStream() throws IOException {
096        if (css != null) {
097            return new ByteArrayInputStream(css.getBytes("UTF-8"));
098        }
099        MirroredInputStream in = new MirroredInputStream(url);
100        if (isZip) {
101            File file = in.getFile();
102            Utils.close(in);
103            zipFile = new ZipFile(file);
104            zipIcons = file;
105            ZipEntry zipEntry = zipFile.getEntry(zipEntryPath);
106            return zipFile.getInputStream(zipEntry);
107        } else {
108            zipFile = null;
109            zipIcons = null;
110            return in;
111        }
112    }
113
114    @Override
115    public void closeSourceInputStream(InputStream is) {
116        super.closeSourceInputStream(is);
117        if (isZip) {
118            Utils.close(zipFile);
119        }
120    }
121
122    /**
123     * load meta info from a selector "meta"
124     */
125    private void loadMeta() {
126        Cascade c = constructSpecial("meta");
127        String pTitle = c.get("title", null, String.class);
128        if (title == null) {
129            title = pTitle;
130        }
131        String pIcon = c.get("icon", null, String.class);
132        if (icon == null) {
133            icon = pIcon;
134        }
135    }
136
137    private void loadCanvas() {
138        Cascade c = constructSpecial("canvas");
139        backgroundColorOverride = c.get("background-color", null, Color.class);
140    }
141
142    private Cascade constructSpecial(String type) {
143
144        MultiCascade mc = new MultiCascade();
145        Node n = new Node();
146        String code = LanguageInfo.getJOSMLocaleCode();
147        n.put("lang", code);
148        // create a fake environment to read the meta data block
149        Environment env = new Environment(n, mc, "default", this);
150
151        NEXT_RULE:
152        for (MapCSSRule r : rules) {
153            for (Selector s : r.selectors) {
154                if ((s instanceof GeneralSelector)) {
155                    GeneralSelector gs = (GeneralSelector) s;
156                    if (gs.getBase().equals(type)) {
157                        if (!gs.matchesConditions(env)) {
158                            continue NEXT_RULE;
159                        }
160                        r.execute(env);
161                    }
162                }
163            }
164        }
165        return mc.getCascade("default");
166    }
167
168    @Override
169    public Color getBackgroundColorOverride() {
170        return backgroundColorOverride;
171    }
172
173    @Override
174    public void apply(MultiCascade mc, OsmPrimitive osm, double scale, OsmPrimitive multipolyOuterWay, boolean pretendWayIsClosed) {
175        Environment env = new Environment(osm, mc, null, this);
176        for (MapCSSRule r : rules) {
177            for (Selector s : r.selectors) {
178                env.clearSelectorMatchingInformation();
179                if (s.matches(env)) { // as side effect env.parent will be set (if s is a child selector)
180                    if (s.getRange().contains(scale)) {
181                        mc.range = Range.cut(mc.range, s.getRange());
182                    } else {
183                        mc.range = mc.range.reduceAround(scale, s.getRange());
184                        continue;
185                    }
186
187                    String sub = s.getSubpart();
188                    if (sub == null) {
189                        sub = "default";
190                    }
191                    else if ("*".equals(sub)) {
192                        for (Entry<String, Cascade> entry : mc.getLayers()) {
193                            env.layer = entry.getKey();
194                            if (Utils.equal(env.layer, "*")) {
195                                continue;
196                            }
197                            r.execute(env);
198                        }
199                    }
200                    env.layer = sub;
201                    r.execute(env);
202                }
203            }
204        }
205    }
206
207    @Override
208    public String toString() {
209        return Utils.join("\n", rules);
210    }
211}