From b695d43fee05671b096e588e612dd6b94fada91c Mon Sep 17 00:00:00 2001 From: Longri Date: Wed, 23 Aug 2017 12:22:52 +0200 Subject: [PATCH] Mapsforge themes compatibility (#388) #100 --- vtm-android-example/AndroidManifest.xml | 3 + vtm-android-example/res/menu/theme_menu.xml | 3 + vtm-android-example/res/values/strings.xml | 1 + .../android/filepicker/ValidRenderTheme.java | 13 +- .../android/test/MapsforgeMapActivity.java | 30 + .../oscim/android/canvas/AndroidCanvas.java | 9 + vtm-desktop/src/org/oscim/awt/AwtCanvas.java | 17 +- .../src/org/oscim/ios/backend/IosCanvas.java | 8 + .../comparator/logging/BaseAppender.java | 19 +- .../src/org/oscim/gdx/client/GwtCanvas.java | 6 + .../assets/shaders/linetex_layer_tex.glsl | 11 +- vtm/src/org/oscim/backend/canvas/Canvas.java | 2 + .../oscim/renderer/bucket/LineTexBucket.java | 2 +- vtm/src/org/oscim/theme/ThemeLoader.java | 25 +- vtm/src/org/oscim/theme/ThemeUtils.java | 78 ++ .../theme/XmlMapsforgeAtlasThemeBuilder.java | 126 ++ .../oscim/theme/XmlMapsforgeThemeBuilder.java | 1226 +++++++++++++++++ vtm/src/org/oscim/theme/styles/LineStyle.java | 6 +- 18 files changed, 1565 insertions(+), 20 deletions(-) create mode 100644 vtm/src/org/oscim/theme/ThemeUtils.java create mode 100644 vtm/src/org/oscim/theme/XmlMapsforgeAtlasThemeBuilder.java create mode 100644 vtm/src/org/oscim/theme/XmlMapsforgeThemeBuilder.java diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index 2c8f41d4..4321c70f 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -61,6 +61,9 @@ + diff --git a/vtm-android-example/res/menu/theme_menu.xml b/vtm-android-example/res/menu/theme_menu.xml index fb2b8dfb..319b5428 100644 --- a/vtm-android-example/res/menu/theme_menu.xml +++ b/vtm-android-example/res/menu/theme_menu.xml @@ -20,6 +20,9 @@ + Show nature Hide nature Grid + load theme extern diff --git a/vtm-android-example/src/org/oscim/android/filepicker/ValidRenderTheme.java b/vtm-android-example/src/org/oscim/android/filepicker/ValidRenderTheme.java index 93ff8f4d..ca88d0c4 100644 --- a/vtm-android-example/src/org/oscim/android/filepicker/ValidRenderTheme.java +++ b/vtm-android-example/src/org/oscim/android/filepicker/ValidRenderTheme.java @@ -1,6 +1,7 @@ /* * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2016 devemux86 + * Copyright 2017 Longri * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -17,12 +18,16 @@ package org.oscim.android.filepicker; import org.oscim.theme.ExternalRenderTheme; import org.oscim.theme.ThemeFile; +import org.oscim.theme.ThemeUtils; +import org.oscim.theme.XmlMapsforgeThemeBuilder; import org.oscim.theme.XmlThemeBuilder; import org.oscim.tiling.TileSource.OpenResult; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; import java.io.File; +import java.io.FileInputStream; import javax.xml.parsers.SAXParserFactory; @@ -34,9 +39,15 @@ public final class ValidRenderTheme implements ValidFileFilter { @Override public boolean accept(File file) { + try { ThemeFile theme = new ExternalRenderTheme(file.getAbsolutePath()); - XmlThemeBuilder renderThemeHandler = new XmlThemeBuilder(theme); + DefaultHandler renderThemeHandler; + if(ThemeUtils.isMapsforgeTheme(new FileInputStream(file))) { + renderThemeHandler = new XmlMapsforgeThemeBuilder(theme); + }else{ + renderThemeHandler = new XmlThemeBuilder(theme); + } XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); xmlReader.setContentHandler(renderThemeHandler); xmlReader.parse(new InputSource(theme.getRenderThemeAsStream())); diff --git a/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java b/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java index bd1785cc..74a4c458 100644 --- a/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java +++ b/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java @@ -1,6 +1,7 @@ /* * Copyright 2014 Hannes Janetzek * Copyright 2016-2017 devemux86 + * Copyright 2017 Longri * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -25,6 +26,7 @@ import android.view.MenuItem; import org.oscim.android.filepicker.FilePicker; import org.oscim.android.filepicker.FilterByFileExtension; import org.oscim.android.filepicker.ValidMapFile; +import org.oscim.android.filepicker.ValidRenderTheme; import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.layers.TileGridLayer; @@ -38,12 +40,14 @@ import org.oscim.scalebar.ImperialUnitAdapter; import org.oscim.scalebar.MapScaleBar; import org.oscim.scalebar.MapScaleBarLayer; import org.oscim.scalebar.MetricUnitAdapter; +import org.oscim.theme.ExternalRenderTheme; import org.oscim.theme.VtmThemes; import org.oscim.tiling.source.mapfile.MapFileTileSource; import org.oscim.tiling.source.mapfile.MapInfo; public class MapsforgeMapActivity extends MapActivity { private static final int SELECT_MAP_FILE = 0; + private static final int SELECT_THEME_FILE = 1; private TileGridLayer mGridLayer; private DefaultMapScaleBar mMapScaleBar; @@ -71,6 +75,13 @@ public class MapsforgeMapActivity extends MapActivity { } } + public static class ThemeFilePicker extends FilePicker { + public ThemeFilePicker() { + setFileDisplayFilter(new FilterByFileExtension(".xml")); + setFileSelectFilter(new ValidRenderTheme()); + } + } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.theme_menu, menu); @@ -106,6 +117,11 @@ public class MapsforgeMapActivity extends MapActivity { item.setChecked(true); return true; + case R.id.theme_load: + startActivityForResult(new Intent(MapsforgeMapActivity.this, ThemeFilePicker.class), + SELECT_THEME_FILE); + return true; + case R.id.gridlayer: if (item.isChecked()) { item.setChecked(false); @@ -163,6 +179,20 @@ public class MapsforgeMapActivity extends MapActivity { mPrefs.clear(); } + } else if (requestCode == SELECT_THEME_FILE) { + if (resultCode != RESULT_OK || intent == null || intent.getStringExtra(FilePicker.SELECTED_FILE) == null) { + finish(); + return; + } + + String themePath = intent.getStringExtra(FilePicker.SELECTED_FILE); + + ExternalRenderTheme externalRenderTheme = new ExternalRenderTheme(themePath); + try { + mMap.setTheme(externalRenderTheme, true); + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/vtm-android/src/org/oscim/android/canvas/AndroidCanvas.java b/vtm-android/src/org/oscim/android/canvas/AndroidCanvas.java index 407f581c..2151233a 100644 --- a/vtm-android/src/org/oscim/android/canvas/AndroidCanvas.java +++ b/vtm-android/src/org/oscim/android/canvas/AndroidCanvas.java @@ -21,6 +21,7 @@ package org.oscim.android.canvas; import android.graphics.Color; import android.graphics.PorterDuff; +import android.graphics.RectF; import org.oscim.backend.canvas.Bitmap; import org.oscim.backend.canvas.Canvas; @@ -96,4 +97,12 @@ public class AndroidCanvas implements Canvas { public int getWidth() { return canvas.getWidth(); } + + @Override + public void fillRectangle(int x, int y, int width, int height, int color) { + RectF rec = new RectF(x, y, x + width, y + height); + android.graphics.Paint paint = new android.graphics.Paint(); + paint.setColor(color); + canvas.drawRect(rec, paint); + } } diff --git a/vtm-desktop/src/org/oscim/awt/AwtCanvas.java b/vtm-desktop/src/org/oscim/awt/AwtCanvas.java index 054ba68d..57502a9a 100644 --- a/vtm-desktop/src/org/oscim/awt/AwtCanvas.java +++ b/vtm-desktop/src/org/oscim/awt/AwtCanvas.java @@ -183,12 +183,7 @@ public class AwtCanvas implements Canvas { @Override public void fillColor(int color) { - java.awt.Color awtColor = color == Color.TRANSPARENT ? TRANSPARENT : new java.awt.Color(color); - Composite originalComposite = this.canvas.getComposite(); - this.canvas.setComposite(AlphaComposite.getInstance(color == Color.TRANSPARENT ? AlphaComposite.CLEAR : AlphaComposite.SRC_OVER)); - this.canvas.setColor(awtColor); - this.canvas.fillRect(0, 0, getWidth(), getHeight()); - this.canvas.setComposite(originalComposite); + fillRectangle(0, 0, getWidth(), getHeight(), color); } @Override @@ -200,4 +195,14 @@ public class AwtCanvas implements Canvas { public int getWidth() { return this.bitmap != null ? this.bitmap.getWidth() : 0; } + + @Override + public void fillRectangle(int x, int y, int width, int height, int color) { + java.awt.Color awtColor = color == Color.TRANSPARENT ? TRANSPARENT : new java.awt.Color(color); + Composite originalComposite = this.canvas.getComposite(); + this.canvas.setComposite(AlphaComposite.getInstance(color == Color.TRANSPARENT ? AlphaComposite.CLEAR : AlphaComposite.SRC_OVER)); + this.canvas.setColor(awtColor); + this.canvas.fillRect(x, y, width, height); + this.canvas.setComposite(originalComposite); + } } diff --git a/vtm-ios/src/org/oscim/ios/backend/IosCanvas.java b/vtm-ios/src/org/oscim/ios/backend/IosCanvas.java index b42f17f8..6a8c3fae 100644 --- a/vtm-ios/src/org/oscim/ios/backend/IosCanvas.java +++ b/vtm-ios/src/org/oscim/ios/backend/IosCanvas.java @@ -159,4 +159,12 @@ public class IosCanvas implements Canvas { public int getWidth() { return this.cgBitmapContext != null ? (int) this.cgBitmapContext.getWidth() : 0; } + + @Override + public void fillRectangle(int x, int y, int width, int height, int color) { + CGRect rect = new CGRect(x, y, width, height); + setFillColor(this.cgBitmapContext, (color)); + this.cgBitmapContext.setBlendMode(CGBlendMode.Normal); + this.cgBitmapContext.fillRect(rect); + } } diff --git a/vtm-theme-comparator/src/org/oscim/theme/comparator/logging/BaseAppender.java b/vtm-theme-comparator/src/org/oscim/theme/comparator/logging/BaseAppender.java index 262d4ff1..18757e4f 100644 --- a/vtm-theme-comparator/src/org/oscim/theme/comparator/logging/BaseAppender.java +++ b/vtm-theme-comparator/src/org/oscim/theme/comparator/logging/BaseAppender.java @@ -51,13 +51,18 @@ public abstract class BaseAppender extends AppenderBase { } @Override - protected void append(ILoggingEvent eventObject) { - if (eventObject != null && canLogClass(eventObject.getLoggerName())) { - stringBuilder.append(doLayout(eventObject)); - String areaText = stringBuilder.toString(); - this.textArea.setText(areaText); - this.textArea.setCaretPosition(areaText.length()); - } + protected void append(final ILoggingEvent eventObject) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + if (eventObject != null && canLogClass(eventObject.getLoggerName())) { + textArea.append(doLayout(eventObject)); + textArea.setCaretPosition(textArea.getDocument().getLength()); + } + } + }); + thread.start(); + //TODO set Highlight for LogLevel [WARN], [ERROR] } diff --git a/vtm-web/src/org/oscim/gdx/client/GwtCanvas.java b/vtm-web/src/org/oscim/gdx/client/GwtCanvas.java index 2ef2ce10..c3425aa4 100644 --- a/vtm-web/src/org/oscim/gdx/client/GwtCanvas.java +++ b/vtm-web/src/org/oscim/gdx/client/GwtCanvas.java @@ -2,6 +2,7 @@ * Copyright 2013 Hannes Janetzek * Copyright 2016-2017 devemux86 * Copyright 2017 nebular + * Copyright 2017 Longri * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -124,4 +125,9 @@ public class GwtCanvas implements org.oscim.backend.canvas.Canvas { public int getWidth() { return this.bitmap != null ? this.bitmap.getWidth() : 0; } + + @Override + public void fillRectangle(int x, int y, int width, int height, int color) { + // TODO + } } diff --git a/vtm/resources/assets/shaders/linetex_layer_tex.glsl b/vtm/resources/assets/shaders/linetex_layer_tex.glsl index 9b1ab66d..98a400dc 100644 --- a/vtm/resources/assets/shaders/linetex_layer_tex.glsl +++ b/vtm/resources/assets/shaders/linetex_layer_tex.glsl @@ -43,10 +43,15 @@ uniform sampler2D tex; uniform float u_mode; void main(){ - if (u_mode == 1.0) { - vec4 c=texture2D(tex,vec2(abs(mod(v_st.s+1.0,2.0)),(v_st.t+1.0)*0.5)); + if (u_mode >= 1.0) { + + float step= 2.0; + if (u_mode == 3.0){// dashed texture + step =1.0; + } + vec4 c=texture2D(tex,vec2(abs(mod(v_st.s+1.0,step)),(v_st.t+1.0)*0.5)); float fuzz=fwidth(c.a); - gl_FragColor=(c * u_color) *smoothstep(0.5-fuzz,0.5+fuzz,c.a); + gl_FragColor=(c * u_color) * smoothstep(0.5-fuzz,0.5+fuzz,c.a); } else { /* distance on perpendicular to the line */ diff --git a/vtm/src/org/oscim/backend/canvas/Canvas.java b/vtm/src/org/oscim/backend/canvas/Canvas.java index 93f86ea2..c74eb39e 100644 --- a/vtm/src/org/oscim/backend/canvas/Canvas.java +++ b/vtm/src/org/oscim/backend/canvas/Canvas.java @@ -58,4 +58,6 @@ public interface Canvas { int getHeight(); int getWidth(); + + void fillRectangle(int x, int y, int width, int height, int color); } diff --git a/vtm/src/org/oscim/renderer/bucket/LineTexBucket.java b/vtm/src/org/oscim/renderer/bucket/LineTexBucket.java index 12443e5f..42fddf70 100644 --- a/vtm/src/org/oscim/renderer/bucket/LineTexBucket.java +++ b/vtm/src/org/oscim/renderer/bucket/LineTexBucket.java @@ -359,7 +359,7 @@ public final class LineTexBucket extends LineBucket { LineTexBucket lb = (LineTexBucket) b; LineStyle line = lb.line.current(); - gl.uniform1f(shader.uMode, line.texture != null ? 1 : 0); + gl.uniform1f(shader.uMode, line.dashTexture? 3 : line.texture != null ? 1 : 0); if (line.texture != null) line.texture.bind(); diff --git a/vtm/src/org/oscim/theme/ThemeLoader.java b/vtm/src/org/oscim/theme/ThemeLoader.java index be58129d..88347c0c 100644 --- a/vtm/src/org/oscim/theme/ThemeLoader.java +++ b/vtm/src/org/oscim/theme/ThemeLoader.java @@ -18,11 +18,21 @@ */ package org.oscim.theme; + import org.oscim.backend.CanvasAdapter; import org.oscim.theme.IRenderTheme.ThemeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; public class ThemeLoader { + private static final Logger log = LoggerFactory.getLogger(ThemeLoader.class); + public static boolean USE_ATLAS; public static boolean POT_TEXTURES; @@ -46,8 +56,21 @@ public class ThemeLoader { return load(theme, null); } + + public static IRenderTheme load(ThemeFile theme, ThemeCallback themeCallback) throws ThemeException { - IRenderTheme t = USE_ATLAS ? XmlAtlasThemeBuilder.read(theme, themeCallback) : XmlThemeBuilder.read(theme, themeCallback); + IRenderTheme t = null; + + try { + if(ThemeUtils.isMapsforgeTheme(theme.getRenderThemeAsStream())){ + t = USE_ATLAS ? XmlMapsforgeAtlasThemeBuilder.read(theme, themeCallback) : XmlMapsforgeThemeBuilder.read(theme, themeCallback); + }else{ + t = USE_ATLAS ? XmlAtlasThemeBuilder.read(theme, themeCallback) : XmlThemeBuilder.read(theme, themeCallback); + } + } catch (IOException | ParserConfigurationException | SAXException e) { + e.printStackTrace(); + } + if (t != null) t.scaleTextSize(CanvasAdapter.textScale + (CanvasAdapter.dpi / CanvasAdapter.DEFAULT_DPI - 1)); return t; diff --git a/vtm/src/org/oscim/theme/ThemeUtils.java b/vtm/src/org/oscim/theme/ThemeUtils.java new file mode 100644 index 00000000..75d227ca --- /dev/null +++ b/vtm/src/org/oscim/theme/ThemeUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Longri + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.theme; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +/** + * Created by Longri on 30.08.2017. + */ + +public class ThemeUtils { + + public static class SAXTerminatorException extends SAXException { + public SAXTerminatorException() { + super(); + } + } + + + /** + * Return true, if the given InputStream a Mapsforge render theme! + * + * @param stream + * @return TRUE or FALSE + * @throws IOException + * @throws SAXException + * @throws ParserConfigurationException + */ + public static boolean isMapsforgeTheme(InputStream stream) throws IOException, SAXException, ParserConfigurationException { + final AtomicBoolean isMapsforgeTheme = new AtomicBoolean(false); + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + + XMLReader xmlReader = factory.newSAXParser().getXMLReader(); + xmlReader.setContentHandler(new DefaultHandler() { + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + if (localName.equals("rendertheme")) { + isMapsforgeTheme.set(uri.equals("http://mapsforge.org/renderTheme")); + //we have all info's, break parsing + throw new SAXTerminatorException(); + } + } + }); + try { + xmlReader.parse(new InputSource(stream)); + } catch (SAXTerminatorException e) { + // do nothing + } + stream.close(); + return isMapsforgeTheme.get(); + } + +} diff --git a/vtm/src/org/oscim/theme/XmlMapsforgeAtlasThemeBuilder.java b/vtm/src/org/oscim/theme/XmlMapsforgeAtlasThemeBuilder.java new file mode 100644 index 00000000..c71666c5 --- /dev/null +++ b/vtm/src/org/oscim/theme/XmlMapsforgeAtlasThemeBuilder.java @@ -0,0 +1,126 @@ +/* + * Copyright 2017 Longri + * Copyright 2017 devemux86 + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.theme; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.Platform; +import org.oscim.backend.XMLReaderAdapter; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.renderer.atlas.TextureAtlas; +import org.oscim.renderer.atlas.TextureRegion; +import org.oscim.theme.IRenderTheme.ThemeException; +import org.oscim.theme.rule.Rule; +import org.oscim.theme.styles.RenderStyle; +import org.oscim.theme.styles.SymbolStyle; +import org.oscim.theme.styles.SymbolStyle.SymbolBuilder; +import org.oscim.utils.TextureAtlasUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class XmlMapsforgeAtlasThemeBuilder extends XmlMapsforgeThemeBuilder { + + /** + * @param theme an input theme containing valid render theme XML data. + * @return a new RenderTheme which is created by parsing the XML data from the input theme. + * @throws ThemeException if an error occurs while parsing the render theme XML. + */ + public static IRenderTheme read(ThemeFile theme) throws ThemeException { + return read(theme, null); + } + + /** + * @param theme an input theme containing valid render theme XML data. + * @param themeCallback the theme callback. + * @return a new RenderTheme which is created by parsing the XML data from the input theme. + * @throws ThemeException if an error occurs while parsing the render theme XML. + */ + public static IRenderTheme read(ThemeFile theme, ThemeCallback themeCallback) throws ThemeException { + Map outputMap = new HashMap<>(); + List atlasList = new ArrayList<>(); + XmlMapsforgeAtlasThemeBuilder renderThemeHandler = new XmlMapsforgeAtlasThemeBuilder(theme, themeCallback, outputMap, atlasList); + + try { + new XMLReaderAdapter().parse(renderThemeHandler, theme.getRenderThemeAsStream()); + } catch (Exception e) { + throw new ThemeException(e.getMessage()); + } + + TextureAtlasUtils.createTextureRegions(renderThemeHandler.bitmapMap, outputMap, atlasList, + true, CanvasAdapter.platform == Platform.IOS); + + return replaceThemeSymbols(renderThemeHandler.mRenderTheme, outputMap); + } + + private static IRenderTheme replaceThemeSymbols(RenderTheme renderTheme, Map regionMap) { + SymbolBuilder symbolBuilder = SymbolStyle.builder(); + for (Rule rule : renderTheme.getRules()) { + replaceRuleSymbols(rule, regionMap, symbolBuilder); + } + return renderTheme; + } + + private static void replaceRuleSymbols(Rule rule, Map regionMap, SymbolBuilder symbolBuilder) { + for (int i = 0, n = rule.styles.length; i < n; i++) { + RenderStyle style = rule.styles[i]; + if (style instanceof SymbolStyle) { + int hash = ((SymbolStyle) style).hash; + TextureRegion region = regionMap.get(hash); + if (region != null) { + SymbolBuilder b = symbolBuilder.reset(); + rule.styles[i] = b.texture(region).build(); + } + } + } + for (Rule subRule : rule.subRules) { + replaceRuleSymbols(subRule, regionMap, symbolBuilder); + } + } + + private final Map regionMap; + private final List atlasList; + + private final Map bitmapMap = new HashMap<>(); + + public XmlMapsforgeAtlasThemeBuilder(ThemeFile theme, + Map regionMap, List atlasList) { + this(theme, null, regionMap, atlasList); + } + + public XmlMapsforgeAtlasThemeBuilder(ThemeFile theme, ThemeCallback themeCallback, + Map regionMap, List atlasList) { + super(theme, themeCallback); + this.regionMap = regionMap; + this.atlasList = atlasList; + } + + @Override + RenderTheme createTheme(Rule[] rules) { + return new AtlasRenderTheme(mMapBackground, mTextScale, rules, mLevels, regionMap, atlasList); + } + + @Override + SymbolStyle buildSymbol(SymbolBuilder b, String src, Bitmap bitmap) { + // we need to hash with the width/height included as the same symbol could be required + // in a different size and must be cached with a size-specific hash + String absoluteName = CanvasAdapter.getAbsoluteFile(mTheme.getRelativePathPrefix(), src).getAbsolutePath(); + int hash = new StringBuilder().append(absoluteName).append(b.symbolWidth).append(b.symbolHeight).append(b.symbolPercent).toString().hashCode(); + bitmapMap.put(hash, bitmap); + return b.hash(hash).build(); + } +} diff --git a/vtm/src/org/oscim/theme/XmlMapsforgeThemeBuilder.java b/vtm/src/org/oscim/theme/XmlMapsforgeThemeBuilder.java new file mode 100644 index 00000000..22aae9ad --- /dev/null +++ b/vtm/src/org/oscim/theme/XmlMapsforgeThemeBuilder.java @@ -0,0 +1,1226 @@ +/* + * Copyright 2010, 2011, 2012 mapsforge.org + * Copyright 2013 Hannes Janetzek + * Copyright 2016-2017 devemux86 + * Copyright 2016-2017 Longri + * Copyright 2016 Andrey Novikov + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.theme; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.XMLReaderAdapter; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.backend.canvas.Canvas; +import org.oscim.backend.canvas.Color; +import org.oscim.backend.canvas.Paint; +import org.oscim.backend.canvas.Paint.Cap; +import org.oscim.backend.canvas.Paint.FontFamily; +import org.oscim.backend.canvas.Paint.FontStyle; +import org.oscim.renderer.atlas.TextureAtlas; +import org.oscim.renderer.atlas.TextureAtlas.Rect; +import org.oscim.renderer.atlas.TextureRegion; +import org.oscim.renderer.bucket.TextureItem; +import org.oscim.theme.IRenderTheme.ThemeException; +import org.oscim.theme.rule.Rule; +import org.oscim.theme.rule.Rule.Closed; +import org.oscim.theme.rule.Rule.Selector; +import org.oscim.theme.rule.RuleBuilder; +import org.oscim.theme.styles.AreaStyle; +import org.oscim.theme.styles.AreaStyle.AreaBuilder; +import org.oscim.theme.styles.CircleStyle; +import org.oscim.theme.styles.CircleStyle.CircleBuilder; +import org.oscim.theme.styles.ExtrusionStyle; +import org.oscim.theme.styles.ExtrusionStyle.ExtrusionBuilder; +import org.oscim.theme.styles.LineStyle; +import org.oscim.theme.styles.LineStyle.LineBuilder; +import org.oscim.theme.styles.RenderStyle; +import org.oscim.theme.styles.SymbolStyle; +import org.oscim.theme.styles.SymbolStyle.SymbolBuilder; +import org.oscim.theme.styles.TextStyle; +import org.oscim.theme.styles.TextStyle.TextBuilder; +import org.oscim.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Set; +import java.util.Stack; +import java.util.regex.Pattern; + +import static java.lang.Boolean.parseBoolean; +import static java.lang.Float.parseFloat; +import static java.lang.Integer.parseInt; + +public class XmlMapsforgeThemeBuilder extends DefaultHandler { + private static final Logger log = LoggerFactory.getLogger(XmlMapsforgeThemeBuilder.class); + + private static final int RENDER_THEME_VERSION = 4; + private static final Pattern SPLIT_PATTERN = Pattern.compile(","); + private static final float REPEAT_GAP_DEFAULT = 200f; + private static final float REPEAT_START_DEFAULT = 30f; + + + private enum Element { + RENDER_THEME, RENDERING_INSTRUCTION, RULE, STYLE, ATLAS, RENDERING_STYLE + } + + private static final String ELEMENT_NAME_RENDER_THEME = "rendertheme"; + private static final String ELEMENT_NAME_STYLE_MENU = "stylemenu"; + private static final String ELEMENT_NAME_MATCH = "rule"; + private static final String UNEXPECTED_ELEMENT = "unexpected element: "; + + private static final String LINE_STYLE = "L"; + private static final String OUTLINE_STYLE = "O"; + private static final String AREA_STYLE = "A"; + + /** + * @param theme an input theme containing valid render theme XML data. + * @return a new RenderTheme which is created by parsing the XML data from the input theme. + * @throws ThemeException if an error occurs while parsing the render theme XML. + */ + public static IRenderTheme read(ThemeFile theme) throws ThemeException { + return read(theme, null); + } + + /** + * @param theme an input theme containing valid render theme XML data. + * @param themeCallback the theme callback. + * @return a new RenderTheme which is created by parsing the XML data from the input theme. + * @throws ThemeException if an error occurs while parsing the render theme XML. + */ + public static IRenderTheme read(ThemeFile theme, ThemeCallback themeCallback) throws ThemeException { + XmlMapsforgeThemeBuilder renderThemeHandler = new XmlMapsforgeThemeBuilder(theme, themeCallback); + + try { + new XMLReaderAdapter().parse(renderThemeHandler, theme.getRenderThemeAsStream()); + } catch (Exception e) { + throw new ThemeException(e.getMessage()); + } + + return renderThemeHandler.mRenderTheme; + } + + /** + * Logs the given information about an unknown XML attribute. + * + * @param element the XML element name. + * @param name the XML attribute name. + * @param value the XML attribute value. + * @param attributeIndex the XML attribute index position. + */ + private static void logUnknownAttribute(String element, String name, + String value, int attributeIndex) { + log.debug("unknown attribute in element {} () : {} = {}", + element, attributeIndex, name, value); + } + + private final ArrayList mRulesList = new ArrayList<>(); + private final Stack mElementStack = new Stack<>(); + private final Stack mRuleStack = new Stack<>(); + private final HashMap mStyles = new HashMap<>(10); + + private final HashMap> mTextStyles = new HashMap<>(10); + + private final AreaBuilder mAreaBuilder = AreaStyle.builder(); + private final CircleBuilder mCircleBuilder = CircleStyle.builder(); + private final ExtrusionBuilder mExtrusionBuilder = ExtrusionStyle.builder(); + private final LineBuilder mLineBuilder = LineStyle.builder(); + private final SymbolBuilder mSymbolBuilder = SymbolStyle.builder(); + private final TextBuilder mTextBuilder = TextStyle.builder(); + + private RuleBuilder mCurrentRule; + private TextureAtlas mTextureAtlas; + + int mLevels = 0; + int mMapBackground = 0xffffffff; + float mTextScale = 1; + + final ThemeFile mTheme; + private final ThemeCallback mThemeCallback; + RenderTheme mRenderTheme; + + private final float mScale, mScale2; + + private Set mCategories; + private XmlRenderThemeStyleLayer mCurrentLayer; + private XmlRenderThemeStyleMenu mRenderThemeStyleMenu; + + public XmlMapsforgeThemeBuilder(ThemeFile theme) { + this(theme, null); + } + + public XmlMapsforgeThemeBuilder(ThemeFile theme, ThemeCallback themeCallback) { + mTheme = theme; + mThemeCallback = themeCallback; + mScale = CanvasAdapter.scale + (CanvasAdapter.dpi / CanvasAdapter.DEFAULT_DPI - 1); + mScale2 = CanvasAdapter.scale + (CanvasAdapter.dpi / CanvasAdapter.DEFAULT_DPI - 1) * 0.5f; + } + + @Override + public void endDocument() { + Rule[] rules = new Rule[mRulesList.size()]; + for (int i = 0, n = rules.length; i < n; i++) + rules[i] = mRulesList.get(i).onComplete(null); + + mRenderTheme = createTheme(rules); + + mRulesList.clear(); + mStyles.clear(); + mRuleStack.clear(); + mElementStack.clear(); + + mTextureAtlas = null; + } + + RenderTheme createTheme(Rule[] rules) { + return new RenderTheme(mMapBackground, mTextScale, rules, mLevels); + } + + @Override + public void endElement(String uri, String localName, String qName) { + mElementStack.pop(); + + if (ELEMENT_NAME_MATCH.equals(localName)) { + mRuleStack.pop(); + if (mRuleStack.empty()) { + if (isVisible(mCurrentRule)) { + mRulesList.add(mCurrentRule); + } + } else { + mCurrentRule = mRuleStack.peek(); + } + } else if (ELEMENT_NAME_STYLE_MENU.equals(localName)) { + // when we are finished parsing the menu part of the file, we can get the + // categories to render from the initiator. This allows the creating action + // to select which of the menu options to choose + if (null != mTheme.getMenuCallback()) { + // if there is no callback, there is no menu, so the categories will be null + mCategories = mTheme.getMenuCallback().getCategories(mRenderThemeStyleMenu); + } + } + } + + @Override + public void error(SAXParseException exception) { + log.debug(exception.getMessage()); + } + + @Override + public void warning(SAXParseException exception) { + log.debug(exception.getMessage()); + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws ThemeException { + try { + if (ELEMENT_NAME_RENDER_THEME.equals(localName)) { + checkState(localName, Element.RENDER_THEME); + createRenderTheme(localName, attributes); + + } else if (ELEMENT_NAME_MATCH.equals(localName)) { + checkState(localName, Element.RULE); + RuleBuilder rule = createRule(localName, attributes); + if (!mRuleStack.empty() && isVisible(rule)) { + mCurrentRule.addSubRule(rule); + } + mCurrentRule = rule; + mRuleStack.push(mCurrentRule); + + } else if ("style-text".equals(localName)) { + checkState(localName, Element.STYLE); + handleTextElement(localName, attributes, true, false); + + } else if ("style-area".equals(localName)) { + checkState(localName, Element.STYLE); + handleAreaElement(localName, attributes, true); + + } else if ("style-line".equals(localName)) { + checkState(localName, Element.STYLE); + handleLineElement(localName, attributes, true, false); + + } else if ("outline-layer".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + LineStyle line = createLine(null, localName, attributes, mLevels++, true, false); + mStyles.put(OUTLINE_STYLE + line.style, line); + + } else if ("area".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + handleAreaElement(localName, attributes, false); + + } else if ("caption".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + handleTextElement(localName, attributes, false, true); + + } else if ("circle".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + CircleStyle circle = createCircle(localName, attributes, mLevels++); + if (isVisible(circle)) + mCurrentRule.addStyle(circle); + + } else if ("line".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + handleLineElement(localName, attributes, false, false); + + } else if ("pathText".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + handleTextElement(localName, attributes, false, false); + + } else if ("symbol".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + SymbolStyle symbol = createSymbol(localName, attributes); + if (symbol != null && isVisible(symbol)) + mCurrentRule.addStyle(symbol); + + } else if ("outline".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + LineStyle outline = createOutline(attributes.getValue("use"), attributes); + if (outline != null && isVisible(outline)) + mCurrentRule.addStyle(outline); + + } else if ("extrusion".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + ExtrusionStyle extrusion = createExtrusion(localName, attributes, mLevels++); + if (isVisible(extrusion)) + mCurrentRule.addStyle(extrusion); + + } else if ("lineSymbol".equals(localName)) { + checkState(localName, Element.RENDERING_INSTRUCTION); + handleLineElement(localName, attributes, false, true); + + } else if ("atlas".equals(localName)) { + checkState(localName, Element.ATLAS); + createAtlas(localName, attributes); + + } else if ("rect".equals(localName)) { + checkState(localName, Element.ATLAS); + createTextureRegion(localName, attributes); + + } else if ("cat".equals(localName)) { + checkState(qName, Element.RENDERING_STYLE); + mCurrentLayer.addCategory(getStringAttribute(attributes, "id")); + + } else if ("layer".equals(localName)) { + // render theme menu layer + checkState(qName, Element.RENDERING_STYLE); + boolean enabled = false; + if (getStringAttribute(attributes, "enabled") != null) { + enabled = Boolean.valueOf(getStringAttribute(attributes, "enabled")); + } + boolean visible = Boolean.valueOf(getStringAttribute(attributes, "visible")); + mCurrentLayer = mRenderThemeStyleMenu.createLayer(getStringAttribute(attributes, "id"), visible, enabled); + String parent = getStringAttribute(attributes, "parent"); + if (null != parent) { + XmlRenderThemeStyleLayer parentEntry = mRenderThemeStyleMenu.getLayer(parent); + if (null != parentEntry) { + for (String cat : parentEntry.getCategories()) { + mCurrentLayer.addCategory(cat); + } + for (XmlRenderThemeStyleLayer overlay : parentEntry.getOverlays()) { + mCurrentLayer.addOverlay(overlay); + } + } + } + + } else if ("name".equals(localName)) { + // render theme menu name + checkState(qName, Element.RENDERING_STYLE); + mCurrentLayer.addTranslation(getStringAttribute(attributes, "lang"), getStringAttribute(attributes, "value")); + + } else if ("overlay".equals(localName)) { + // render theme menu overlay + checkState(qName, Element.RENDERING_STYLE); + XmlRenderThemeStyleLayer overlay = mRenderThemeStyleMenu.getLayer(getStringAttribute(attributes, "id")); + if (overlay != null) { + mCurrentLayer.addOverlay(overlay); + } + + } else if ("stylemenu".equals(localName)) { + checkState(qName, Element.RENDERING_STYLE); + mRenderThemeStyleMenu = new XmlRenderThemeStyleMenu(getStringAttribute(attributes, "id"), + getStringAttribute(attributes, "defaultlang"), getStringAttribute(attributes, "defaultvalue")); + + } else { + log.error("unknown element: {}", localName); + throw new SAXException("unknown element: " + localName); + } + } catch (SAXException e) { + throw new ThemeException(e.getMessage()); + } catch (IOException e) { + throw new ThemeException(e.getMessage()); + } + } + + private RuleBuilder createRule(String localName, Attributes attributes) { + String cat = null; + int element = Rule.Element.ANY; + int closed = Closed.ANY; + String keys = null; + String values = null; + byte zoomMin = 0; + byte zoomMax = Byte.MAX_VALUE; + int selector = 0; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("e".equals(name)) { + String val = value.toUpperCase(Locale.ENGLISH); + if ("WAY".equals(val)) + element = Rule.Element.WAY; + else if ("NODE".equals(val)) + element = Rule.Element.NODE; + } else if ("k".equals(name)) { + if (!"*".equals(value)) + keys = value; + } else if ("v".equals(name)) { + if (!"*".equals(value)) + values = value; + } else if ("cat".equals(name)) { + cat = value; + } else if ("closed".equals(name)) { + String val = value.toUpperCase(Locale.ENGLISH); + if ("YES".equals(val)) + closed = Closed.YES; + else if ("NO".equals(val)) + closed = Closed.NO; + } else if ("zoom-min".equals(name)) { + zoomMin = Byte.parseByte(value); + } else if ("zoom-max".equals(name)) { + zoomMax = Byte.parseByte(value); + } else if ("select".equals(name)) { + if ("first".equals(value)) + selector |= Selector.FIRST; + if ("when-matched".equals(value)) + selector |= Selector.WHEN_MATCHED; + } else { + XmlMapsforgeThemeBuilder.logUnknownAttribute(localName, name, value, i); + } + } + + if (closed == Closed.YES) + element = Rule.Element.POLY; + else if (closed == Closed.NO) + element = Rule.Element.LINE; + + XmlMapsforgeThemeBuilder.validateNonNegative("zoom-min", zoomMin); + XmlMapsforgeThemeBuilder.validateNonNegative("zoom-max", zoomMax); + if (zoomMin > zoomMax) + throw new ThemeException("zoom-min must be less or equal zoom-max: " + zoomMin); + + RuleBuilder b = RuleBuilder.create(keys, values); + b.cat(cat); + b.zoom(zoomMin, zoomMax); + b.element(element); + b.select(selector); + return b; + } + + private TextureRegion getAtlasRegion(String src) { + if (mTextureAtlas == null) + return null; + + TextureRegion texture = mTextureAtlas.getTextureRegion(src); + + if (texture == null) + log.debug("missing texture atlas item '" + src + "'"); + + return texture; + } + + private void handleLineElement(String localName, Attributes attributes, boolean isStyle, boolean symbolLine) + throws SAXException { + + String use = attributes.getValue("use"); + LineStyle style = null; + + if (use != null) { + style = (LineStyle) mStyles.get(LINE_STYLE + use); + if (style == null) { + log.debug("missing line style 'use': " + use); + return; + } + } + + LineStyle line = createLine(style, localName, attributes, mLevels++, false, symbolLine); + + if (isStyle) { + mStyles.put(LINE_STYLE + line.style, line); + } else { + if (isVisible(line)) { + mCurrentRule.addStyle(line); + /* Note 'outline' will not be inherited, it's just a + * shortcut to add the outline RenderInstruction. */ + String outlineValue = attributes.getValue("outline"); + if (outlineValue != null) { + LineStyle outline = createOutline(outlineValue, attributes); + if (outline != null) + mCurrentRule.addStyle(outline); + } + } + } + } + + /** + * @param line optional: line style defaults + * @param level the drawing level of this instruction. + * @param isOutline is outline layer + * @return a new Line with the given rendering attributes. + */ + private LineStyle createLine(LineStyle line, String elementName, Attributes attributes, + int level, boolean isOutline, boolean symbolLine) { + LineBuilder b = mLineBuilder.set(line); + b.isOutline(isOutline); + b.level(level); + b.themeCallback(mThemeCallback); + String src = null; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("id".equals(name)) + b.style = value; + + else if ("cat".equals(name)) + b.cat(value); + + else if ("src".equals(name)) + src = value; + + else if ("use".equals(name)) + ;// ignore + + else if ("outline".equals(name)) + ;// ignore + + else if ("stroke".equals(name)) + b.color(value); + + else if ("width".equals(name) || "stroke-width".equals(name)) { + b.strokeWidth = parseFloat(value) * mScale2; + if (line == null) { + if (!isOutline) + validateNonNegative("width", b.strokeWidth); + } else { + /* use stroke width relative to 'line' */ + b.strokeWidth += line.width; + if (b.strokeWidth <= 0) + b.strokeWidth = 1; + } + } else if ("cap".equals(name) || "stroke-linecap".equals(name)) + b.cap = Cap.valueOf(value.toUpperCase(Locale.ENGLISH)); + + else if ("fix".equals(name)) + b.fixed = parseBoolean(value); + + else if ("stipple".equals(name)) + b.stipple = Math.round(parseInt(value) * mScale2); + + else if ("stipple-stroke".equals(name)) + b.stippleColor(value); + + else if ("stipple-width".equals(name)) + b.stippleWidth = parseFloat(value); + + else if ("fade".equals(name)) + b.fadeScale = Integer.parseInt(value); + + else if ("min".equals(name)) + ; //min = Float.parseFloat(value); + + else if ("blur".equals(name)) + b.blur = parseFloat(value); + + else if ("style".equals(name)) + ; // ignore + + else if ("dasharray".equals(name)) + ; // TBD + + else if ("symbol-width".equals(name)) + b.symbolWidth = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-height".equals(name)) + b.symbolHeight = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-percent".equals(name)) + b.symbolPercent = Integer.parseInt(value); + + else if ("stroke-dasharray".equals(name)) { + b.strokeDasharray = parseFloatArray(value); + for (int j = 0; j < b.strokeDasharray.length; ++j) { + b.strokeDasharray[j] = b.strokeDasharray[j] * mScale; + } + } else if ("dy".equals(name)) { + // NB: minus.. + //TODO b.dy = -Float.parseFloat(value) * mScale; + } else if ("align-center".equals(name)) { + //TODO handle align-center + } else if ("repeat".equals(name)) { + //TODO handle repeat + } else if ("display".equals(name)) { + //TODO handle display + } else + logUnknownAttribute(elementName, name, value, i); + } + + + if (b.strokeDasharray != null) {//create a dashed texture + int bmpWidth = 0; + int bmpHeight = (int) (b.strokeWidth); + if (bmpHeight < 1) bmpHeight = 2; + for (float f : b.strokeDasharray) { + if (f < 1) f = 1; + bmpWidth += f; + } + + int factor = 10; + Bitmap bmp = CanvasAdapter.newBitmap(bmpWidth * factor, bmpHeight * factor, 0); + Canvas canvas = CanvasAdapter.newCanvas(); + canvas.setBitmap(bmp); + + boolean bw = false; + int x = 0; + for (float f : b.strokeDasharray) { + if (f < 1) f = 1; + canvas.fillRectangle(x * factor, 0, (int) f * factor, bmpHeight * factor, (bw ? Color.TRANSPARENT : Color.WHITE)); + x += f; + bw = !bw; + } + b.texture = new TextureItem(bmp); + b.texture.mipmap = false; + b.stipple = (int) (bmpWidth * 1.2f); + b.stippleWidth = bmpWidth; + b.fixed = false; + b.randomOffset = false; + + b.stippleColor = b.fillColor; + b.fillColor = Color.TRANSPARENT; + b.strokeColor = Color.TRANSPARENT; + + } else { + b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, b.symbolPercent); + if (symbolLine) { + + // we have no way to set a repeatGap for the renderer, + // so we create a texture that already contains this repeatGap. + float repeatGap = REPEAT_GAP_DEFAULT * mScale; + float repeatStart = REPEAT_START_DEFAULT * mScale; + int width = (int) (b.texture.width + repeatGap); + int height = b.texture.height; + Bitmap bmp = CanvasAdapter.newBitmap(width, height, 0); + Canvas canvas = CanvasAdapter.newCanvas(); + canvas.setBitmap(bmp); + canvas.drawBitmap(b.texture.bitmap, repeatStart, 0); + b.texture = new TextureItem(bmp); + + // we must set stipple values + // The multipliers are determined empirically to + // correspond to the representation at Mapsforge! + b.stipple = b.texture.width * 3; + b.strokeWidth *= 2 * mScale; + + // use texture color + b.stippleColor = Color.WHITE; + b.fillColor = Color.TRANSPARENT; + b.strokeColor = Color.TRANSPARENT; + + b.fixed = false; + } + } + + return b.build(); + } + + private void handleAreaElement(String localName, Attributes attributes, boolean isStyle) + throws SAXException { + + String use = attributes.getValue("use"); + AreaStyle style = null; + + if (use != null) { + style = (AreaStyle) mStyles.get(AREA_STYLE + use); + if (style == null) { + log.debug("missing area style 'use': " + use); + return; + } + } + + AreaStyle area = createArea(style, localName, attributes, mLevels++); + + if (isStyle) { + mStyles.put(AREA_STYLE + area.style, area); + } else { + if (isVisible(area)) + mCurrentRule.addStyle(area); + } + } + + /** + * @return a new Area with the given rendering attributes. + */ + private AreaStyle createArea(AreaStyle area, String elementName, Attributes attributes, + int level) { + AreaBuilder b = mAreaBuilder.set(area); + b.level(level); + b.themeCallback(mThemeCallback); + String src = null; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("id".equals(name)) + b.style = value; + + else if ("cat".equals(name)) + b.cat(value); + + else if ("use".equals(name)) + ;// ignore + + else if ("src".equals(name)) + src = value; + + else if ("fill".equals(name)) + b.color(value); + + else if ("stroke".equals(name)) + b.strokeColor(value); + + else if ("stroke-width".equals(name)) { + float strokeWidth = Float.parseFloat(value); + validateNonNegative("stroke-width", strokeWidth); + b.strokeWidth = strokeWidth * mScale2; + + } else if ("fade".equals(name)) + b.fadeScale = Integer.parseInt(value); + + else if ("blend".equals(name)) + b.blendScale = Integer.parseInt(value); + + else if ("blend-fill".equals(name)) + b.blendColor(value); + + else if ("mesh".equals(name)) + b.mesh(Boolean.parseBoolean(value)); + + else if ("symbol-width".equals(name)) + b.symbolWidth = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-height".equals(name)) + b.symbolHeight = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-percent".equals(name)) + b.symbolPercent = Integer.parseInt(value); + + else if ("symbol-scaling".equals(name)) { + // no-op + } else + logUnknownAttribute(elementName, name, value, i); + } + + b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, b.symbolPercent); + + return b.build(); + } + + private LineStyle createOutline(String style, Attributes attributes) { + if (style != null) { + LineStyle line = (LineStyle) mStyles.get(OUTLINE_STYLE + style); + if (line != null && line.outline) { + String cat = null; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("cat".equals(name)) { + cat = value; + break; + } + } + + return line + .setCat(cat); + } + } + log.debug("BUG not an outline style: " + style); + return null; + } + + private void createAtlas(String elementName, Attributes attributes) throws IOException { + String img = null; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("img".equals(name)) { + img = value; + } else { + XmlMapsforgeThemeBuilder.logUnknownAttribute(elementName, name, value, i); + } + } + validateExists("img", img, elementName); + + Bitmap bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), img); + if (bitmap != null) + mTextureAtlas = new TextureAtlas(bitmap); + } + + private void createTextureRegion(String elementName, Attributes attributes) { + if (mTextureAtlas == null) + return; + + String regionName = null; + Rect r = null; + + for (int i = 0, n = attributes.getLength(); i < n; i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("id".equals(name)) { + regionName = value; + } else if ("pos".equals(name)) { + String[] pos = value.split(" "); + if (pos.length == 4) { + r = new Rect(Integer.parseInt(pos[0]), + Integer.parseInt(pos[1]), + Integer.parseInt(pos[2]), + Integer.parseInt(pos[3])); + } + } else { + XmlMapsforgeThemeBuilder.logUnknownAttribute(elementName, name, value, i); + } + } + validateExists("id", regionName, elementName); + validateExists("pos", r, elementName); + + mTextureAtlas.addTextureRegion(regionName.intern(), r); + } + + private void checkElement(String elementName, Element element) throws SAXException { + Element parentElement; + switch (element) { + case RENDER_THEME: + if (!mElementStack.empty()) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case RULE: + parentElement = mElementStack.peek(); + if (parentElement != Element.RENDER_THEME + && parentElement != Element.RULE) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case STYLE: + parentElement = mElementStack.peek(); + if (parentElement != Element.RENDER_THEME) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case RENDERING_INSTRUCTION: + if (mElementStack.peek() != Element.RULE) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case ATLAS: + parentElement = mElementStack.peek(); + // FIXME + if (parentElement != Element.RENDER_THEME + && parentElement != Element.ATLAS) { + throw new SAXException(UNEXPECTED_ELEMENT + elementName); + } + return; + + case RENDERING_STYLE: + return; + } + + throw new SAXException("unknown enum value: " + element); + } + + private void checkState(String elementName, Element element) throws SAXException { + checkElement(elementName, element); + mElementStack.push(element); + } + + private void createRenderTheme(String elementName, Attributes attributes) { + Integer version = null; + int mapBackground = Color.WHITE; + float baseStrokeWidth = 1; + float baseTextScale = 1; + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("schemaLocation".equals(name)) + continue; + + if ("version".equals(name)) + version = Integer.parseInt(value); + + else if ("map-background".equals(name)) { + mapBackground = Color.parseColor(value); + if (mThemeCallback != null) + mapBackground = mThemeCallback.getColor(mapBackground); + } else if ("base-stroke-width".equals(name)) + baseStrokeWidth = Float.parseFloat(value); + + else if ("base-text-scale".equals(name)) + baseTextScale = Float.parseFloat(value); + + else if ("map-background-outside".equals(name)) { + //TODO handle map-background-outside + } else + XmlMapsforgeThemeBuilder.logUnknownAttribute(elementName, name, value, i); + + } + + validateExists("version", version, elementName); + + if (version != RENDER_THEME_VERSION) + throw new ThemeException("invalid render theme version:" + + version); + + validateNonNegative("base-stroke-width", baseStrokeWidth); + validateNonNegative("base-text-scale", baseTextScale); + + mMapBackground = mapBackground; + mTextScale = baseTextScale; + } + + private void handleTextElement(String localName, Attributes attributes, boolean isStyle, + boolean isCaption) throws SAXException { + + String style = attributes.getValue("use"); + TextBuilder pt = null; + + if (style != null) { + pt = mTextStyles.get(style); + if (pt == null) { + log.debug("missing text style: " + style); + return; + } + } + + TextBuilder b = createText(localName, attributes, isCaption, pt); + if (isStyle) { + log.debug("put style {}", b.style); + mTextStyles.put(b.style, TextStyle.builder().from(b)); + } else { + TextStyle text = b.buildInternal(); + if (isVisible(text)) + mCurrentRule.addStyle(text); + } + } + + /** + * @param caption ... + * @return a new Text with the given rendering attributes. + */ + private TextBuilder createText(String elementName, Attributes attributes, + boolean caption, TextBuilder style) { + TextBuilder b; + if (style == null) { + b = mTextBuilder.reset(); + b.caption = caption; + } else + b = mTextBuilder.from(style); + b.themeCallback(mThemeCallback); + String symbol = null; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("id".equals(name)) + b.style = value; + + else if ("cat".equals(name)) + b.cat(value); + + else if ("k".equals(name)) + b.textKey = value.intern(); + + else if ("font-family".equals(name)) + b.fontFamily = FontFamily.valueOf(value.toUpperCase(Locale.ENGLISH)); + + else if ("font-style".equals(name)) + b.fontStyle = FontStyle.valueOf(value.toUpperCase(Locale.ENGLISH)); + + else if ("font-size".equals(name)) + b.fontSize = Float.parseFloat(value); + + else if ("fill".equals(name)) + b.fillColor = Color.parseColor(value); + + else if ("stroke".equals(name)) + b.strokeColor = Color.parseColor(value); + + else if ("stroke-width".equals(name)) + b.strokeWidth = Float.parseFloat(value) * mScale2; + + else if ("caption".equals(name)) + b.caption = Boolean.parseBoolean(value); + + else if ("priority".equals(name)) + b.priority = Integer.parseInt(value); + + else if ("area-size".equals(name)) + b.areaSize = Float.parseFloat(value); + + else if ("dy".equals(name)) + // NB: minus.. + b.dy = -Float.parseFloat(value) * mScale; + + else if ("symbol".equals(name)) + symbol = value; + + else if ("use".equals(name)) + ;/* ignore */ + + else if ("symbol-width".equals(name)) + b.symbolWidth = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-height".equals(name)) + b.symbolHeight = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-percent".equals(name)) + b.symbolPercent = Integer.parseInt(value); + + else if ("display".equals(name)) { + //TODO Handle display attribute NEVER, ALWAYS, IFSPACE; + } else if ("symbol-id".equals(name)) { + //TODO Handle symbol-id; + } else if ("position".equals(name)) { + //TODO Handle position: AUTO, CENTER, BELOW, BELOW_LEFT, BELOW_RIGHT, ABOVE, ABOVE_LEFT, ABOVE_RIGHT, LEFT, RIGHT + } else + logUnknownAttribute(elementName, name, value, i); + } + + validateExists("k", b.textKey, elementName); + validateNonNegative("size", b.fontSize); + validateNonNegative("stroke-width", b.strokeWidth); + + if (symbol != null && symbol.length() > 0) { + String lowValue = symbol.toLowerCase(Locale.ENGLISH); + if (lowValue.endsWith(".png") || lowValue.endsWith(".svg")) { + try { + b.bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), symbol, b.symbolWidth, b.symbolHeight, b.symbolPercent); + } catch (Exception e) { + log.debug(e.getMessage()); + } + } else + b.texture = getAtlasRegion(symbol); + } + + return b; + } + + /** + * @param level the drawing level of this instruction. + * @return a new Circle with the given rendering attributes. + */ + private CircleStyle createCircle(String elementName, Attributes attributes, int level) { + CircleBuilder b = mCircleBuilder.reset(); + b.level(level); + b.themeCallback(mThemeCallback); + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("r".equals(name) || "radius".equals(name)) + b.radius(Float.parseFloat(value) * mScale2); + + else if ("cat".equals(name)) + b.cat(value); + + else if ("scale-radius".equals(name)) + b.scaleRadius(Boolean.parseBoolean(value)); + + else if ("fill".equals(name)) + b.color(Color.parseColor(value)); + + else if ("stroke".equals(name)) + b.strokeColor(Color.parseColor(value)); + + else if ("stroke-width".equals(name)) + b.strokeWidth(Float.parseFloat(value) * mScale2); + + else + logUnknownAttribute(elementName, name, value, i); + } + + validateExists("radius", b.radius, elementName); + validateNonNegative("radius", b.radius); + validateNonNegative("stroke-width", b.strokeWidth); + + return b.build(); + } + + /** + * @return a new Symbol with the given rendering attributes. + */ + private SymbolStyle createSymbol(String elementName, Attributes attributes) { + SymbolBuilder b = mSymbolBuilder.reset(); + String src = null; + + for (int i = 0; i < attributes.getLength(); i++) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("src".equals(name)) + src = value; + + else if ("cat".equals(name)) + b.cat(value); + + else if ("symbol-width".equals(name)) + b.symbolWidth = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-height".equals(name)) + b.symbolHeight = (int) (Integer.parseInt(value) * mScale); + + else if ("symbol-percent".equals(name)) + b.symbolPercent = Integer.parseInt(value); + + else if ("symbol-scaling".equals(name)) { + // no-op + } else if ("display".equals(name)) { + //TODO Handle display attribute NEVER, ALWAYS, IFSPACE; + } else if ("id".equals(name)) { + //TODO Handle 'id' + } else if ("priority".equals(name)) { + //TODO Handle 'priority' + } else + logUnknownAttribute(elementName, name, value, i); + } + + validateExists("src", src, elementName); + + String lowSrc = src.toLowerCase(Locale.ENGLISH); + if (lowSrc.endsWith(".png") || lowSrc.endsWith(".svg")) { + try { + Bitmap bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, b.symbolPercent); + if (bitmap != null) + return buildSymbol(b, src, bitmap); + } catch (Exception e) { + log.debug(e.getMessage()); + } + return null; + } + return b.texture(getAtlasRegion(src)).build(); + } + + SymbolStyle buildSymbol(SymbolBuilder b, String src, Bitmap bitmap) { + return b.bitmap(bitmap).build(); + } + + private ExtrusionStyle createExtrusion(String elementName, Attributes attributes, int level) { + ExtrusionBuilder b = mExtrusionBuilder.reset(); + b.level(level); + b.themeCallback(mThemeCallback); + + for (int i = 0; i < attributes.getLength(); ++i) { + String name = attributes.getLocalName(i); + String value = attributes.getValue(i); + + if ("cat".equals(name)) + b.cat(value); + + else if ("side-color".equals(name)) + b.colorSide(Color.parseColor(value)); + + else if ("top-color".equals(name)) + b.colorTop(Color.parseColor(value)); + + else if ("line-color".equals(name)) + b.colorLine(Color.parseColor(value)); + + else if ("default-height".equals(name)) + b.defaultHeight(Integer.parseInt(value)); + + else + logUnknownAttribute(elementName, name, value, i); + } + + return b.build(); + } + + private String getStringAttribute(Attributes attributes, String name) { + for (int i = 0; i < attributes.getLength(); ++i) { + if (attributes.getLocalName(i).equals(name)) { + return attributes.getValue(i); + } + } + return null; + } + + /** + * A style is visible if categories is not set or the style has no category + * or the categories contain the style's category. + */ + private boolean isVisible(RenderStyle renderStyle) { + return mCategories == null || renderStyle.cat == null || mCategories.contains(renderStyle.cat); + } + + /** + * A rule is visible if categories is not set or the rule has no category + * or the categories contain the rule's category. + */ + private boolean isVisible(RuleBuilder rule) { + return mCategories == null || rule.cat == null || mCategories.contains(rule.cat); + } + + private static void validateNonNegative(String name, float value) { + if (value < 0) + throw new ThemeException(name + " must not be negative: " + + value); + } + + private static void validateExists(String name, Object obj, String elementName) { + if (obj == null) + throw new ThemeException("missing attribute " + name + + " for element: " + elementName); + } + + private static float[] parseFloatArray(String dashString) { + String[] dashEntries = SPLIT_PATTERN.split(dashString); + float[] dashIntervals = new float[dashEntries.length]; + for (int i = 0; i < dashEntries.length; ++i) { + dashIntervals[i] = Float.parseFloat(dashEntries[i]); + } + return dashIntervals; + } +} diff --git a/vtm/src/org/oscim/theme/styles/LineStyle.java b/vtm/src/org/oscim/theme/styles/LineStyle.java index da65cfc5..0021c560 100644 --- a/vtm/src/org/oscim/theme/styles/LineStyle.java +++ b/vtm/src/org/oscim/theme/styles/LineStyle.java @@ -2,6 +2,7 @@ * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2013 Hannes Janetzek * Copyright 2016-2017 devemux86 + * Copyright 2017 Longri * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -47,6 +48,7 @@ public final class LineStyle extends RenderStyle { public final int symbolWidth; public final int symbolHeight; public final int symbolPercent; + public boolean dashTexture; public LineStyle(int stroke, float width) { this(0, "", stroke, width, Cap.BUTT, true, 0, 0, 0, -1, 0, false, null, true); @@ -112,6 +114,7 @@ public final class LineStyle extends RenderStyle { this.symbolWidth = b.symbolWidth; this.symbolHeight = b.symbolHeight; this.symbolPercent = b.symbolPercent; + this.dashTexture = b.strokeDasharray != null; } @Override @@ -143,6 +146,7 @@ public final class LineStyle extends RenderStyle { public int symbolWidth; public int symbolHeight; public int symbolPercent; + public float[] strokeDasharray; public LineBuilder() { } @@ -273,7 +277,7 @@ public final class LineStyle extends RenderStyle { symbolWidth = 0; symbolHeight = 0; symbolPercent = 100; - + strokeDasharray = null; return self(); }