source: josm/trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/MapCSSStyleSource.java@ 7056

Last change on this file since 7056 was 7056, checked in by bastiK, 10 years ago

mapcss: optimization by converting very hot double loop into single loop (gain ~ 20%)

  • Property svn:eol-style set to native
File size: 11.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint.mapcss;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.io.ByteArrayInputStream;
8import java.io.File;
9import java.io.IOException;
10import java.io.InputStream;
11import java.util.ArrayList;
12import java.util.List;
13import java.util.Map.Entry;
14import java.util.zip.ZipEntry;
15import java.util.zip.ZipFile;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.data.Version;
19import org.openstreetmap.josm.data.osm.Node;
20import org.openstreetmap.josm.data.osm.OsmPrimitive;
21import org.openstreetmap.josm.data.osm.Relation;
22import org.openstreetmap.josm.data.osm.Way;
23import org.openstreetmap.josm.gui.mappaint.Cascade;
24import org.openstreetmap.josm.gui.mappaint.Environment;
25import org.openstreetmap.josm.gui.mappaint.MultiCascade;
26import org.openstreetmap.josm.gui.mappaint.Range;
27import org.openstreetmap.josm.gui.mappaint.StyleSource;
28import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.ChildOrParentSelector;
29import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector;
30import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.OptimizedGeneralSelector;
31import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
32import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
33import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.TokenMgrError;
34import org.openstreetmap.josm.gui.preferences.SourceEntry;
35import org.openstreetmap.josm.io.MirroredInputStream;
36import org.openstreetmap.josm.tools.CheckParameterUtil;
37import org.openstreetmap.josm.tools.LanguageInfo;
38import org.openstreetmap.josm.tools.Utils;
39
40public class MapCSSStyleSource extends StyleSource {
41
42 /**
43 * The accepted MIME types sent in the HTTP Accept header.
44 * @since 6867
45 */
46 public static final String MAPCSS_STYLE_MIME_TYPES = "text/x-mapcss, text/mapcss, text/css; q=0.9, text/plain; q=0.8, application/zip, application/octet-stream; q=0.5";
47
48 // all rules
49 public final List<MapCSSRule> rules = new ArrayList<>();
50 // rules filtered by primitive type
51 public final List<MapCSSRule> nodeRules = new ArrayList<>();
52 public final List<MapCSSRule> wayRules = new ArrayList<>();
53 public final List<MapCSSRule> relationRules = new ArrayList<>();
54 public final List<MapCSSRule> multipolygonRules = new ArrayList<>();
55
56 private Color backgroundColorOverride;
57 private String css = null;
58 private ZipFile zipFile;
59
60 private static int usedId = 1;
61
62 public MapCSSStyleSource(String url, String name, String shortdescription) {
63 super(url, name, shortdescription);
64 }
65
66 public MapCSSStyleSource(SourceEntry entry) {
67 super(entry);
68 }
69
70 /**
71 * <p>Creates a new style source from the MapCSS styles supplied in
72 * {@code css}</p>
73 *
74 * @param css the MapCSS style declaration. Must not be null.
75 * @throws IllegalArgumentException thrown if {@code css} is null
76 */
77 public MapCSSStyleSource(String css) throws IllegalArgumentException{
78 super(null, null, null);
79 CheckParameterUtil.ensureParameterNotNull(css);
80 this.css = css;
81 }
82
83 @Override
84 public void loadStyleSource() {
85 init();
86 rules.clear();
87 nodeRules.clear();
88 wayRules.clear();
89 relationRules.clear();
90 multipolygonRules.clear();
91 try (InputStream in = getSourceInputStream()) {
92 try {
93 // evaluate @media { ... } blocks
94 MapCSSParser preprocessor = new MapCSSParser(in, "UTF-8", MapCSSParser.LexicalState.PREPROCESSOR);
95 String mapcss = preprocessor.pp_root(this);
96
97 // do the actual mapcss parsing
98 InputStream in2 = new ByteArrayInputStream(mapcss.getBytes(Utils.UTF_8));
99 MapCSSParser parser = new MapCSSParser(in2, "UTF-8", MapCSSParser.LexicalState.DEFAULT);
100 parser.sheet(this);
101
102 loadMeta();
103 loadCanvas();
104 } finally {
105 closeSourceInputStream(in);
106 }
107 } catch (IOException e) {
108 Main.warn(tr("Failed to load Mappaint styles from ''{0}''. Exception was: {1}", url, e.toString()));
109 Main.error(e);
110 logError(e);
111 } catch (TokenMgrError e) {
112 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
113 Main.error(e);
114 logError(e);
115 } catch (ParseException e) {
116 Main.warn(tr("Failed to parse Mappaint styles from ''{0}''. Error was: {1}", url, e.getMessage()));
117 Main.error(e);
118 logError(new ParseException(e.getMessage())); // allow e to be garbage collected, it links to the entire token stream
119 }
120 // optimization: filter rules for different primitive types
121 for (MapCSSRule r: rules) {
122 // find the rightmost selector, this must be a GeneralSelector
123 Selector selRightmost = r.selector;
124 while (selRightmost instanceof ChildOrParentSelector) {
125 selRightmost = ((ChildOrParentSelector) selRightmost).right;
126 }
127 MapCSSRule optRule = new MapCSSRule(r.selector.optimizedBaseCheck(), r.declaration);
128 switch (((GeneralSelector) selRightmost).getBase()) {
129 case "node":
130 nodeRules.add(optRule);
131 break;
132 case "way":
133 wayRules.add(optRule);
134 break;
135 case "area":
136 wayRules.add(optRule);
137 multipolygonRules.add(optRule);
138 break;
139 case "relation":
140 relationRules.add(optRule);
141 multipolygonRules.add(optRule);
142 break;
143 case "*":
144 nodeRules.add(optRule);
145 wayRules.add(optRule);
146 relationRules.add(optRule);
147 multipolygonRules.add(optRule);
148 break;
149 }
150 }
151 rules.clear();
152 }
153
154 @Override
155 public InputStream getSourceInputStream() throws IOException {
156 if (css != null) {
157 return new ByteArrayInputStream(css.getBytes(Utils.UTF_8));
158 }
159 MirroredInputStream in = new MirroredInputStream(url, null, MAPCSS_STYLE_MIME_TYPES);
160 if (isZip) {
161 File file = in.getFile();
162 Utils.close(in);
163 zipFile = new ZipFile(file);
164 zipIcons = file;
165 ZipEntry zipEntry = zipFile.getEntry(zipEntryPath);
166 return zipFile.getInputStream(zipEntry);
167 } else {
168 zipFile = null;
169 zipIcons = null;
170 return in;
171 }
172 }
173
174 @Override
175 public void closeSourceInputStream(InputStream is) {
176 super.closeSourceInputStream(is);
177 if (isZip) {
178 Utils.close(zipFile);
179 }
180 }
181
182 /**
183 * load meta info from a selector "meta"
184 */
185 private void loadMeta() {
186 Cascade c = constructSpecial("meta");
187 String pTitle = c.get("title", null, String.class);
188 if (title == null) {
189 title = pTitle;
190 }
191 String pIcon = c.get("icon", null, String.class);
192 if (icon == null) {
193 icon = pIcon;
194 }
195 }
196
197 private void loadCanvas() {
198 Cascade c = constructSpecial("canvas");
199 backgroundColorOverride = c.get("background-color", null, Color.class);
200 }
201
202 private Cascade constructSpecial(String type) {
203
204 MultiCascade mc = new MultiCascade();
205 Node n = new Node();
206 String code = LanguageInfo.getJOSMLocaleCode();
207 n.put("lang", code);
208 // create a fake environment to read the meta data block
209 Environment env = new Environment(n, mc, "default", this);
210
211 NEXT_RULE:
212 for (MapCSSRule r : rules) {
213 if ((r.selector instanceof GeneralSelector)) {
214 GeneralSelector gs = (GeneralSelector) r.selector;
215 if (gs.getBase().equals(type)) {
216 if (!gs.matchesConditions(env)) {
217 continue NEXT_RULE;
218 }
219 r.execute(env);
220 }
221 }
222 }
223 return mc.getCascade("default");
224 }
225
226 @Override
227 public Color getBackgroundColorOverride() {
228 return backgroundColorOverride;
229 }
230
231 @Override
232 public void apply(MultiCascade mc, OsmPrimitive osm, double scale, OsmPrimitive multipolyOuterWay, boolean pretendWayIsClosed) {
233 Environment env = new Environment(osm, mc, null, this);
234 List<MapCSSRule> matchingRules;
235 if (osm instanceof Node) {
236 matchingRules = nodeRules;
237 } else if (osm instanceof Way) {
238 matchingRules = wayRules;
239 } else {
240 if (((Relation) osm).isMultipolygon()) {
241 matchingRules = multipolygonRules;
242 } else {
243 matchingRules = relationRules;
244 }
245 }
246 usedId++;
247 RULE: for (MapCSSRule r : matchingRules) {
248 env.clearSelectorMatchingInformation();
249 if (r.selector.matches(env)) { // as side effect env.parent will be set (if s is a child selector)
250 Selector s = r.selector;
251 if (s.getRange().contains(scale)) {
252 mc.range = Range.cut(mc.range, s.getRange());
253 } else {
254 mc.range = mc.range.reduceAround(scale, s.getRange());
255 continue;
256 }
257
258 if (r.declaration.usedId == usedId) continue; // don't apply a declaration block more than once
259 r.declaration.usedId = usedId;
260 String sub = s.getSubpart();
261 if (sub == null) {
262 sub = "default";
263 }
264 else if ("*".equals(sub)) {
265 for (Entry<String, Cascade> entry : mc.getLayers()) {
266 env.layer = entry.getKey();
267 if (Utils.equal(env.layer, "*")) {
268 continue;
269 }
270 r.execute(env);
271 }
272 }
273 env.layer = sub;
274 r.execute(env);
275 continue RULE;
276 }
277 }
278 }
279
280 public boolean evalMediaExpression(String feature, Object val) {
281 if ("user-agent".equals(feature)) {
282 String s = Cascade.convertTo(val, String.class);
283 if ("josm".equals(s)) return true;
284 }
285 if ("min-josm-version".equals(feature)) {
286 Float v = Cascade.convertTo(val, Float.class);
287 if (v != null) return Math.round(v) <= Version.getInstance().getVersion();
288 }
289 if ("max-josm-version".equals(feature)) {
290 Float v = Cascade.convertTo(val, Float.class);
291 if (v != null) return Math.round(v) >= Version.getInstance().getVersion();
292 }
293 return false;
294 }
295
296 @Override
297 public String toString() {
298 return Utils.join("\n", rules);
299 }
300}
Note: See TracBrowser for help on using the repository browser.