package de.blau.android.presets; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.PrintStream; import java.io.Serializable; import java.security.DigestInputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Stack; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.AttributeList; import org.xml.sax.HandlerBase; import org.xml.sax.SAXException; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.BaseAdapter; import android.widget.ScrollView; import android.widget.TextView; import ch.poole.poparser.Po; import de.blau.android.App; import de.blau.android.R; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement.ElementType; import de.blau.android.osm.Relation; import de.blau.android.osm.Tags; import de.blau.android.osm.Way; import de.blau.android.prefs.AdvancedPrefDatabase; import de.blau.android.prefs.PresetEditorActivity; import de.blau.android.util.FileUtil; import de.blau.android.util.Hash; import de.blau.android.util.SavingHelper; import de.blau.android.util.SearchIndexUtils; import de.blau.android.util.StringWithDescription; import de.blau.android.util.Util; import de.blau.android.util.collections.MultiHashMap; import de.blau.android.views.WrappingLayout; /** * This class loads and represents JOSM preset files. * * Presets can come from one of three sources: * a) the default preset, which is loaded from the default asset locations (see below) * b) an APK-based preset, which is loaded from an APK * c) a downloaded preset, which is downloaded to local storage by {@link PresetEditorActivity} * * For APK-based presets, the APK must have a "preset.xml" file in the asset directory, * and may have images in the "images" subdirectory in the asset directory. * A preset is considered APK-based if the constructor receives a package name. * In the preset editor, use the package name prefixed by the {@link APKPRESET_URLPREFIX} * to specify an APK preset. * * The preset.xml is loaded from the following sources: * a) for the default preset, "preset.xml" in the default asset locations * b) for APK-based presets, "preset.xml" in the APK asset directory * c) for downloaded presets, "preset.xml" in the preset data directory * * Icons referenced in the XML preset definition by relative URL are loaded from the following locations: * 1. If a package name is given and the APK contains a matching asset, from the asset ("images/" is prepended to the path) * 2. Otherwise, from the default asset location (see below, "images/" is prepended to the path) * * Icons referenced in the XML preset by a http or https URL are loaded from the presets data directory, * where they should be placed under a name derived from the URL hash by {@link PresetEditorActivity}. * Default and APK presets cannot have http/https icons. * * If an asset needs to be loaded from the default asset locations, the loader checks for the existence * of an APK with the package name specified in {@link PresetIconManager#EXTERNAL_DEFAULT_ASSETS_PACKAGE}. * If this package exists and contains a matching asset, it is loaded from there. * Otherwise, it is loaded from the Vespucci asset directory. * The external default assets package just needs an asset directory that can contain a preset.xml and/or image directory. * * @author Jan Schejbal */ public class Preset implements Serializable { /** * */ private static final long serialVersionUID = 7L; /** name of the preset XML file in a preset directory */ public static final String PRESETXML = "preset.xml"; /** name of the MRU serialization file in a preset directory */ private static final String MRUFILE = "mru.dat"; public static final String APKPRESET_URLPREFIX = "apk:"; // hardwired layout stuff public static final int SPACING = 5; // private static final int MAX_MRU_SIZE = 50; private static final String DEBUG_TAG = Preset.class.getName(); /** The directory containing all data (xml, MRU data, images) about this preset */ private File directory; /** Lists items having a tag. The map key is tagkey+"\t"+tagvalue. * tagItems.get(tagkey+"\t"+tagvalue) will give you all items that have the tag tagkey=tagvalue */ private final MultiHashMap<String, PresetItem> tagItems = new MultiHashMap<String, PresetItem>(); /** The root group of the preset, containing all top-level groups and items */ private PresetGroup rootGroup; /** {@link PresetIconManager} used for icon loading */ private transient PresetIconManager iconManager; /** all known preset items in order of loading */ private ArrayList<PresetItem> allItems = new ArrayList<PresetItem>(); /** all known preset groups in order of loading */ private ArrayList<PresetGroup> allGroups = new ArrayList<PresetGroup>(); public enum PresetKeyType { /** * arbitrary single value */ TEXT, /** * multiple values, single select */ COMBO, /** * multiple values, multiple select */ MULTISELECT, /** * single value, set or unset */ CHECK } public enum MatchType { NONE, KEY, KEY_NEG, KEY_VALUE, KEY_VALUE_NEG, } private final static String COMBO_DELIMITER = ","; private final static String MULTISELECT_DELIMITER = ";"; /** Maps all possible keys to the respective values for autosuggest (only key/values applying to nodes) */ private final MultiHashMap<String, StringWithDescription> autosuggestNodes = new MultiHashMap<String, StringWithDescription>(true); /** Maps all possible keys to the respective values for autosuggest (only key/values applying to ways) */ private final MultiHashMap<String, StringWithDescription> autosuggestWays = new MultiHashMap<String, StringWithDescription>(true); /** Maps all possible keys to the respective values for autosuggest (only key/values applying to closed ways) */ private final MultiHashMap<String, StringWithDescription> autosuggestClosedways = new MultiHashMap<String, StringWithDescription>(true); /** Maps all possible keys to the respective values for autosuggest (only key/values applying to areas (MPs)) */ private final MultiHashMap<String, StringWithDescription> autosuggestAreas = new MultiHashMap<String, StringWithDescription>(true); /** Maps all possible keys to the respective values for autosuggest (only key/values applying to closed ways) */ private final MultiHashMap<String, StringWithDescription> autosuggestRelations = new MultiHashMap<String, StringWithDescription>(true); /** for search support */ private final MultiHashMap<String, PresetItem> searchIndex = new MultiHashMap<String, PresetItem>(); private final MultiHashMap<String, PresetItem> translatedSearchIndex = new MultiHashMap<String, PresetItem>(); private Po po = null; /** * Serializable class for storing Most Recently Used information. * Hash is used to check compatibility. */ protected static class PresetMRUInfo implements Serializable { private static final long serialVersionUID = 7708132207266548489L; PresetMRUInfo(String presetHash) { this.presetHash = presetHash; } /** hash of current preset (used to check validity of recentPresets indexes) */ final String presetHash; /** indexes of recently used presets (for use with allItems) */ LinkedList<Integer> recentPresets = new LinkedList<Integer>(); public volatile boolean changed = false; } private final PresetMRUInfo mru; private String externalPackage; private static class PresetFileFilter implements FilenameFilter { @Override public boolean accept(File dir, String name) { return name.endsWith(".xml"); } } /** * create a dummy preset */ public Preset() { mru = null; } /** * Creates a preset object. * @param ctx context (used for preset loading) * @param directory directory to load/store preset data (XML, icons, MRUs) * @param name of external package containing preset assets for APK presets, null for other presets * @throws Exception */ public Preset(Context ctx, File directory, String externalPackage) throws Exception { this.directory = directory; this.externalPackage = externalPackage; rootGroup = new PresetGroup(null, "", null); //noinspection ResultOfMethodCallIgnored directory.mkdir(); InputStream fileStream = null; try { if (directory.getName().equals(AdvancedPrefDatabase.ID_DEFAULT)) { Log.i("Preset", "Loading default preset"); iconManager = new PresetIconManager(ctx, null, null); fileStream = iconManager.openAsset(PRESETXML, true); // get translations InputStream poFileStream = null; try { poFileStream = iconManager.openAsset("preset_"+Locale.getDefault()+".po", true); if (poFileStream == null) { poFileStream = iconManager.openAsset("preset_"+Locale.getDefault().getLanguage()+".po", true); } if (poFileStream != null) { try { po = new Po(poFileStream); } catch (Exception ignored) { Log.e("Preset","Parsing translation file for " + Locale.getDefault() + " or " + Locale.getDefault().getLanguage() + " failed"); } catch (Error ignored) { Log.e("Preset","Parsing translation file for " + Locale.getDefault() + " or " + Locale.getDefault().getLanguage() + " failed"); } } } finally { SavingHelper.close(poFileStream); } } else if (externalPackage != null) { Log.i("Preset", "Loading APK preset, package=" + externalPackage + ", directory="+directory.toString()); iconManager = new PresetIconManager(ctx, directory.toString(), externalPackage); fileStream = iconManager.openAsset(PRESETXML, false); // po = new Po(iconManager.openAsset("preset_"+Locale.getDefault()+".po", false)); } else { Log.i("Preset", "Loading downloaded preset, directory="+directory.toString()); iconManager = new PresetIconManager(ctx, directory.toString(), null); File indir = new File(directory.toString()); fileStream = null; // force crash and burn if (indir != null) { File[] list = indir.listFiles(new PresetFileFilter()); if (list != null && list.length > 0) { // simply use the first XML file found String presetFilename = list[0].getName(); Log.i("Preset", "Preset file name " + presetFilename); fileStream = new FileInputStream(new File(directory, presetFilename)); // get translations presetFilename = presetFilename.substring(0, presetFilename.length()-4); InputStream poFileStream = null; try { // try to open .po files either with the same name as the preset file or the standard name try { poFileStream = new FileInputStream(new File(directory,presetFilename+"_"+Locale.getDefault()+".po")); } catch (FileNotFoundException fnfe) { try { poFileStream = new FileInputStream(new File(directory,presetFilename+"_"+Locale.getDefault().getLanguage()+".po")); } catch (FileNotFoundException fnfe2) { try { presetFilename = PRESETXML.substring(0, PRESETXML.length()-4); poFileStream = new FileInputStream(new File(directory,presetFilename+"_"+Locale.getDefault()+".po")); } catch (FileNotFoundException fnfe3) { try { poFileStream = new FileInputStream(new File(directory,presetFilename+"_"+Locale.getDefault().getLanguage()+".po")); } catch (FileNotFoundException fnfe4) { // no translations } } } } if (poFileStream != null) { try { po = new Po(poFileStream); } catch (Exception ignored) { Log.e("Preset","Parsing translation file for " + Locale.getDefault() + " or " + Locale.getDefault().getLanguage() + " failed"); } catch (Error ignored) { Log.e("Preset","Parsing translation file for " + Locale.getDefault() + " or " + Locale.getDefault().getLanguage() + " failed"); } } } finally { SavingHelper.close(poFileStream); } } else { Log.e("Preset","Can't find preset file" ); } } else { Log.e("Preset","Can't open preset directory " + directory.toString()); } } DigestInputStream hashStream = new DigestInputStream( fileStream, MessageDigest.getInstance("SHA-256")); parseXML(hashStream); // Finish hash String hashValue = Hash.toHex(hashStream.getMessageDigest().digest()); // in theory, it could be possible that the stream parser does not read the entire file // and maybe even randomly stops at a different place each time. // in practice, it does read the full file, which means this gives the actual sha256 of the file, // - even if you add a 1 MB comment after the document-closing tag. // remove chunks - this messes up the index disabled for now // for (PresetItem c:new ArrayList<PresetItem>(allItems)) { // if (c.isChunk()) { // allItems.remove(c); // } // } mru = initMRU(directory, hashValue); // for (String k:searchIndex.getKeys()) { // String l = k; // for (PresetItem pi:searchIndex.get(k)) { // l = l + " " + pi.getName(); // } // Log.d("SearchIndex",l); // } Log.d("SearchIndex","length: " + searchIndex.getKeys().size()); } finally { SavingHelper.close(fileStream); } } /** * Construct a new preset from existing elements * * @param elements list of PresetElements */ public Preset(@NonNull List<PresetElement> elements) { mru = null; String name ="Empty Preset"; if (elements != null && !elements.isEmpty()) { name = elements.get(0).getName(); } rootGroup = new PresetGroup(null, name, null); addElementsToIndex(rootGroup, elements); } /** * Recursively add tags from the preset to the index of the new preset * * @param group current group * @param elements list of PresetElements */ private void addElementsToIndex(PresetGroup group, List<PresetElement> elements) { for (PresetElement e:elements) { if (e instanceof PresetGroup) { addElementsToIndex(group, ((PresetGroup)e).getElements()); } else if (e instanceof PresetItem) { // build tagItems from existing preset for (Entry<String,StringWithDescription>entry:((PresetItem)e).getFixedTags().entrySet()) { tagItems.add(entry.getKey()+"\t"+entry.getValue().getValue(), (PresetItem) e); } for (Entry<String,StringWithDescription[]>entry:((PresetItem)e).getRecommendedTags().entrySet()) { String key = entry.getKey(); if (entry.getValue()==null || entry.getValue().length == 0) { tagItems.add(key+"\t", (PresetItem) e); } else { for (StringWithDescription v:entry.getValue()) { tagItems.add(key+"\t"+v.getValue(), (PresetItem) e); } } } for (Entry<String,StringWithDescription[]>entry:((PresetItem)e).getOptionalTags().entrySet()) { String key = entry.getKey(); if (entry.getValue()==null || entry.getValue().length == 0) { tagItems.add(key+"\t", (PresetItem) e); } else { for (StringWithDescription v:entry.getValue()) { tagItems.add(key+"\t"+v.getValue(), (PresetItem) e); } } } } } } private PresetIconManager getIconManager(Context ctx) { if (directory.getName().equals(AdvancedPrefDatabase.ID_DEFAULT)) { return new PresetIconManager(ctx, null, null); } else if (externalPackage != null) { return new PresetIconManager(ctx, directory.toString(), externalPackage); } else { return new PresetIconManager(ctx, directory.toString(), null); } } /** * Parses the XML during import * @param input the input stream from which to read XML data * @throws ParserConfigurationException * @throws SAXException * @throws IOException */ @SuppressWarnings("deprecation") private void parseXML(InputStream input) throws ParserConfigurationException, SAXException, IOException { SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); saxParser.parse(input, new HandlerBase() { /** stack of group-subgroup-subsubgroup... where we currently are*/ private Stack<PresetGroup> groupstack = new Stack<PresetGroup>(); /** item currently being processed */ private PresetItem currentItem = null; /** true if we are currently processing the optional section of an item */ private boolean inOptionalSection = false; /** hold reference to chunks */ private HashMap<String,PresetItem> chunks = new HashMap<String,PresetItem>(); /** store current combo or multiselect key */ private String listKey = null; private ArrayList<StringWithDescription> listValues = null; private String valuesContext = null; private String delimiter = null; { groupstack.push(rootGroup); } /** * ${@inheritDoc}. */ @Override public void startElement(String name, AttributeList attr) throws SAXException { if ("presets".equals(name)) { // do nothing for now } else if ("group".equals(name)) { PresetGroup parent = groupstack.peek(); PresetGroup g = new PresetGroup(parent, attr.getValue("name"), attr.getValue("icon")); String context = attr.getValue("name_context"); if (context != null) { g.setNameContext(context); } groupstack.push(g); } else if ("item".equals(name)) { if (currentItem != null) { throw new SAXException("Nested items are not allowed"); } PresetGroup parent = groupstack.peek(); String type = attr.getValue("type"); if (type == null) { type = attr.getValue("gtype"); // note gtype seems to be undocumented } currentItem = new PresetItem(parent, attr.getValue("name"), attr.getValue("icon"), type); String context = attr.getValue("name_context"); if (context != null) { currentItem.setNameContext(context); } currentItem.setDeprecated("true".equals(attr.getValue("deprecated"))); } else if ("chunk".equals(name)) { if (currentItem != null) { throw new SAXException("Nested items are not allowed"); } String type = attr.getValue("type"); if (type == null) { type = attr.getValue("gtype"); // note gtype seems to be undocumented } currentItem = new PresetItem(null, attr.getValue("id"), attr.getValue("icon"), type); currentItem.setChunk(); } else if ("separator".equals(name)) { new PresetSeparator(groupstack.peek()); } else if (currentItem != null) { // the following only make sense if we actually found an item if ("optional".equals(name)) { inOptionalSection = true; } else if ("key".equals(name)) { String key = attr.getValue("key"); String match = attr.getValue("match"); if (!inOptionalSection) { if ("none".equals(match)) {// don't include in fixed tags if not used for matching currentItem.addTag(false, key, PresetKeyType.TEXT, attr.getValue("value")); } else { currentItem.addTag(key, PresetKeyType.TEXT, attr.getValue("value"), attr.getValue("text")); } } else { // Optional fixed tags should not happen, their values will NOT be automatically inserted. currentItem.addTag(true, key, PresetKeyType.TEXT, attr.getValue("value")); } if (match != null) { currentItem.setMatchType(key,match); } String textContext = attr.getValue("text_context"); if (textContext != null) { currentItem.setTextContext(key,textContext); } } else if ("text".equals(name)) { String key = attr.getValue("key"); currentItem.addTag(inOptionalSection, key, PresetKeyType.TEXT, (String)null); String text = attr.getValue("text"); if (text != null) { currentItem.addHint(attr.getValue("key"),text); } String defaultValue = attr.getValue("default"); if (defaultValue != null) { currentItem.addDefault(key,defaultValue); } String textContext = attr.getValue("text_context"); if (textContext != null) { currentItem.setTextContext(key,textContext); } String match = attr.getValue("match"); if (match != null) { currentItem.setMatchType(key,match); } String javaScript = attr.getValue("javascript"); if (javaScript != null) { currentItem.setJavaScript(key,javaScript); } } else if ("link".equals(name)) { String language = Locale.getDefault().getLanguage(); String href = attr.getValue(language.toLowerCase(Locale.US)+".href"); if (href==null) { href = attr.getValue("href"); } if (href!=null) { currentItem.setMapFeatures(href); } } else if ("check".equals(name)) { String key = attr.getValue("key"); String value_on = attr.getValue("value_on") == null ? "yes" : attr.getValue("value_on"); String value_off = attr.getValue("value_off") == null ? "no" : attr.getValue("value_off"); String disable_off = attr.getValue("disable_off"); String values = value_on; // zap value_off if disabled if (disable_off != null && disable_off.equals("true")) { value_off = ""; } else { values = value_on + COMBO_DELIMITER + value_off; } String displayValues = ""; //FIXME this is a bit of a hack as there is no display_values attribute for checks boolean first = true; for (String v:values.split(COMBO_DELIMITER)) { if (!first) { displayValues = displayValues + COMBO_DELIMITER; } else { first = false; } displayValues = displayValues + Util.capitalize(v); } currentItem.setSort(key,false); // don't sort currentItem.addTag(inOptionalSection, key, PresetKeyType.CHECK, values, displayValues, null, COMBO_DELIMITER, null); if (!"yes".equals(value_on)) { currentItem.addOnValue(key,value_on); } String defaultValue = attr.getValue("default") == null ? value_off : ("on".equals(attr.getValue("default")) ? value_on : value_off); if (defaultValue != null) { currentItem.addDefault(key,defaultValue); } String text = attr.getValue("text"); if (text != null) { currentItem.addHint(key,text); } String textContext = attr.getValue("text_context"); if (textContext != null) { currentItem.setTextContext(key,textContext); } String match = attr.getValue("match"); if (match != null) { currentItem.setMatchType(key,match); } } else if ("combo".equals(name) || "multiselect".equals(name)) { boolean multiselect = "multiselect".equals(name); String key = attr.getValue("key"); delimiter = attr.getValue("delimiter"); if (delimiter == null) { delimiter = multiselect ? MULTISELECT_DELIMITER : COMBO_DELIMITER; } String values = attr.getValue("values"); String displayValues = attr.getValue("display_values"); String shortDescriptions = attr.getValue("short_descriptions"); valuesContext = attr.getValue("values_context"); if (values != null) { currentItem.addTag(inOptionalSection, key, multiselect ? PresetKeyType.MULTISELECT : PresetKeyType.COMBO, values, displayValues, shortDescriptions, delimiter, valuesContext); } else { listKey = key; listValues = new ArrayList<StringWithDescription>(); } String defaultValue = attr.getValue("default"); if (defaultValue != null) { currentItem.addDefault(key,defaultValue); } String text = attr.getValue("text"); if (text != null) { currentItem.addHint(key, text); } String textContext = attr.getValue("text_context"); if (textContext != null) { currentItem.setTextContext(key,textContext); } String match = attr.getValue("match"); if (match != null) { currentItem.setMatchType(key,match); } String sort = attr.getValue("values_sort"); if (sort != null) { currentItem.setSort(key,"yes".equals(sort) || "true".equals(sort)); // normally this will not be set because true is the default } String editable = attr.getValue("editable"); if (editable != null) { currentItem.setEditable(key,"yes".equals(editable) || "true".equals(editable)); } } else if ("role".equals(name)) { String key = attr.getValue("key"); String text = attr.getValue("text"); String textContext = attr.getValue("text_context"); if (textContext != null) { currentItem.setTextContext(key,textContext); } currentItem.addRole(new StringWithDescription(key, po != null && text != null ? (textContext!=null?po.t(textContext,text):po.t(text)) : text)); } else if ("reference".equals(name)) { PresetItem chunk = chunks.get(attr.getValue("ref")); // note this assumes that there are no forward references if (chunk != null) { currentItem.fixedTags.putAll(chunk.getFixedTags()); if (!currentItem.isChunk()) { for (Entry<String,StringWithDescription> e:chunk.getFixedTags().entrySet()) { StringWithDescription v = e.getValue(); String value = ""; if (v != null && v.getValue() != null) { value = v.getValue(); } tagItems.add(e.getKey()+"\t"+value, currentItem); } } currentItem.optionalTags.putAll(chunk.getOptionalTags()); addToTagItems(currentItem, chunk.getOptionalTags()); currentItem.recommendedTags.putAll(chunk.getRecommendedTags()); addToTagItems(currentItem, chunk.getRecommendedTags()); currentItem.hints.putAll(chunk.hints); currentItem.addAllDefaults(chunk.defaults); currentItem.keyType.putAll(chunk.keyType); currentItem.setAllMatchTypes(chunk.matchType); currentItem.addAllRoles(chunk.roles); // FIXME this and the following could lead to duplicate entries currentItem.addAllLinkedPresetNames(chunk.linkedPresetNames); currentItem.setAllSort(chunk.sort); currentItem.setAllJavaScript(chunk.javascript); currentItem.setAllEditable(chunk.editable); currentItem.addAllDelimiters(chunk.delimiters); } } else if ("list_entry".equals(name)) { if (listValues != null) { String v = attr.getValue("value"); if (v != null) { String d = attr.getValue("display_value"); if (d == null) { d = attr.getValue("short_description"); } listValues.add(new StringWithDescription(v,po != null ? (valuesContext != null?po.t(valuesContext,d):po.t(d)):d)); } } } else if ("preset_link".equals(name)) { String presetName = attr.getValue("preset_name"); if (presetName != null) { currentItem.addLinkedPresetName(presetName); } } } else { Log.d(DEBUG_TAG, name + " must be in a preset item"); throw new SAXException(name + " must be in a preset item"); } } void addToTagItems(PresetItem currentItem, Map<String,StringWithDescription[]>tags) { if (currentItem.isChunk()) { // only do this on the final expansion return; } for (Entry<String,StringWithDescription[]> e:tags.entrySet()) { StringWithDescription values[] = e.getValue(); if (values != null) { for (StringWithDescription v:values) { String value = ""; if (v != null && v.getValue() != null) { value = v.getValue(); } tagItems.add(e.getKey()+"\t"+value, currentItem); } } } } @Override public void endElement(String name) throws SAXException { if ("group".equals(name)) { groupstack.pop(); } else if ("optional".equals(name)) { inOptionalSection = false; } else if ("item".equals(name)) { // Log.d("Preset","PresetItem: " + currentItem.toString()); if (!currentItem.isDeprecated()) { currentItem.buildSearchIndex(); } currentItem = null; listKey = null; listValues = null; } else if ("chunk".equals(name)) { chunks.put(currentItem.getName(),currentItem); currentItem = null; listKey = null; listValues = null; } else if ("combo".equals(name) || "multiselect".equals(name)) { if (listKey != null && listValues != null) { StringWithDescription[] v = new StringWithDescription[listValues.size()]; currentItem.addTag(inOptionalSection, listKey, "combo".equals(name)?PresetKeyType.COMBO:PresetKeyType.MULTISELECT, listValues.toArray(v), delimiter); } listKey = null; listValues = null; } } }); } /** * Initializes Most-recently-used data by either loading them or creating an empty list * @param directory data directory of the preset * @param hashValue XML hash value to check if stored data fits the XML * @returns a MRU object valid for this Preset, never null */ private PresetMRUInfo initMRU(File directory, String hashValue) { PresetMRUInfo tmpMRU; ObjectInputStream mruReader = null; FileInputStream fout = null; try { fout = new FileInputStream(new File(directory, MRUFILE)); mruReader = new ObjectInputStream(fout); tmpMRU = (PresetMRUInfo) mruReader.readObject(); if (!tmpMRU.presetHash.equals(hashValue)) { throw new InvalidObjectException("hash mismatch"); } } catch (Exception e) { tmpMRU = new PresetMRUInfo(hashValue); // Deserialization failed for whatever reason (missing file, wrong version, ...) - use empty list Log.i("Preset", "No usable old MRU list, creating new one ("+e.toString()+")"); } finally { SavingHelper.close(mruReader); SavingHelper.close(fout); } return tmpMRU; } /** * Returns a list of icon URLs referenced by a preset * @param presetDir a File object pointing to the directory containing this preset * @return an ArrayList of http and https URLs as string, or null if there is an error during parsing */ @SuppressWarnings("deprecation") public static ArrayList<String> parseForURLs(File presetDir) { final ArrayList<String> urls = new ArrayList<String>(); File[] list = presetDir.listFiles(new PresetFileFilter()); String presetFilename = null; if (list != null) { if (list.length > 0) { // simply use the first XML file found presetFilename = list[0].getName(); } } else { return null; } try { SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser(); saxParser.parse(new File(presetDir, presetFilename), new HandlerBase() { /** * ${@inheritDoc}. */ @Override public void startElement(String name, AttributeList attr) throws SAXException { if ("group".equals(name) || "item".equals(name)) { String url = attr.getValue("icon"); if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) { urls.add(url); } } } }); } catch (Exception e) { Log.e("PresetURLParser", "Error parsing preset", e); return null; } return urls; } /** @return the root group of the preset, containing all top-level groups and items */ public PresetGroup getRootGroup() { return rootGroup; } /* * return true if the item is from this Preset */ public boolean contains(PresetItem pi) { return allItems.contains(pi); } /** * Return the index of the preset by sequential search FIXME * @param name * @return */ private Integer getItemIndexByName(@NonNull String name) { Log.d("Preset","getItemIndexByName " + name); for (PresetItem pi:allItems) { if (pi != null) { String n = pi.getName(); if (n != null && n.equals(name)) { return Integer.valueOf(pi.getItemIndex()); } } } Log.d("Preset","getItemIndexByName " + name + " not found"); return null; } /** * Return a preset by name * Note: the names are not guaranteed to be unique, this will simple return the first found * @param name the name to search for * @return the preset item or null if not found */ @Nullable public PresetItem getItemByName(@NonNull String name) { Integer index = getItemIndexByName(name); if (index != null) { return allItems.get(index.intValue()); } return null; } /** * Return a preset by index * * @param the index value * @return the preset item or null if not found */ @Nullable public PresetItem getItemByIndex(int index) { return allItems.get(index); } /** * Find a preset group by name * * Has to traverse the whole preset tree. * * @param name the preset should have * @return the group or null if not found */ @Nullable public PresetGroup getGroupByName(@NonNull String name) { return getGroupByName(getRootGroup(), name); } @Nullable private PresetGroup getGroupByName(@NonNull PresetGroup group, @NonNull String name) { for (PresetElement e:group.getElements()) { if (e instanceof PresetGroup) { if (name.equals(e.getName())) { return (PresetGroup) e; } else { PresetGroup g = getGroupByName((PresetGroup) e, name); if (g != null) { return g; } } } } return null; } /** * Return a PresetElement by identifiying it with its place in the hierarchy * * @param group PresetGroup to start the search at * @param path the path * @return the PresetElement or null if not found */ @Nullable public PresetElement getElementByPath(@NonNull PresetGroup group, @NonNull PresetElementPath path) { int size = path.path.size(); if (size > 0) { String segment = path.path.get(0); for (PresetElement e:group.getElements()) { if (segment.equals(e.getName())) { if (size == 1) { return e; } else { if (e instanceof PresetGroup) { path.path.remove(0); return getElementByPath((PresetGroup) e, path); } } } } } return null; } /** * Return a preset group by index * * @param the index value * @return the preset item or null if not found */ @Nullable public PresetGroup getGroupByIndex(int index) { return allGroups.get(index); } /** * Returns a view showing the most recently used presets * @param handler the handler which will handle clicks on the presets * @param type filter to show only presets applying to this type * @return the view */ public View getRecentPresetView(Context ctx, Preset[] presets, PresetClickHandler handler, ElementType type) { PresetGroup recent = new PresetGroup(null, "recent", null); for (Preset p: presets) { if (p != null && p.hasMRU()) { for (Integer index : p.mru.recentPresets) { recent.addElement(p.allItems.get(index.intValue())); } } } return recent.getGroupView(ctx, handler, type, null); } public boolean hasMRU() { return mru.recentPresets.size() > 0; } /** * Add a preset to the front of the MRU list (removing old duplicates and limiting the list to 50 entries if needed) * @param item the item to add */ public void putRecentlyUsed(PresetItem item) { Integer id = item.getItemIndex(); // prevent duplicates if (!mru.recentPresets.remove(id)) { // calling remove(Object), i.e. removing the number if it is in the list, not the i-th item // preset is not in the list, add linked presets first PresetItem pi = allItems.get(id.intValue()); if (pi.getLinkedPresetNames() != null) { LinkedList<String>linkedPresetNames = new LinkedList<String>(pi.getLinkedPresetNames()); Collections.reverse(linkedPresetNames); for (String n:linkedPresetNames) { if (!mru.recentPresets.contains(id)) { Integer presetIndex = getItemIndexByName(n); if (presetIndex != null) { // null if the link wasn't found if (!mru.recentPresets.contains(presetIndex)) { // only add if not already present mru.recentPresets.addFirst(presetIndex); if (mru.recentPresets.size() > MAX_MRU_SIZE) { mru.recentPresets.removeLast(); } } } else { Log.e("Preset","linked preset not found for " + n + " in preset " + pi.getName()); } } } } } mru.recentPresets.addFirst(id); if (mru.recentPresets.size() > MAX_MRU_SIZE) { mru.recentPresets.removeLast(); } mru.changed = true; } /** * Remove a preset * @param item the item to remove */ public void removeRecentlyUsed(PresetItem item) { Integer id = item.getItemIndex(); // prevent duplicates mru.recentPresets.remove(id); // calling remove(Object), i.e. removing the number if it is in the list, not the i-th item mru.changed = true; } /** * Remove a preset * @param item the item to remove */ public void resetRecentlyUsed() { mru.recentPresets = new LinkedList<Integer>(); mru.changed = true; saveMRU(); } /** Saves the current MRU data to a file */ public void saveMRU() { if (mru.changed) { ObjectOutputStream out = null; FileOutputStream fout = null; try { fout = new FileOutputStream(new File(directory, MRUFILE)); out = new ObjectOutputStream(fout); out.writeObject(mru); out.close(); } catch (Exception e) { Log.e("Preset", "MRU saving failed", e); } finally { SavingHelper.close(out); SavingHelper.close(fout); } } } private String toJSON() { String result = ""; for (PresetItem pi:allItems) { if (!pi.isChunk()) { result = result + pi.toJSON(); } } return result; } /** * Finds the preset item best matching a certain tag set, or null if no preset item matches. * To match, all (mandatory) tags of the preset item need to be in the tag set. * The preset item does NOT need to have all tags in the tag set, but the tag set needs * to have all (mandatory) tags of the preset item. * * If multiple items match, the most specific one (i.e. having most tags) wins. * If there is a draw, no guarantees are made. * * @param presets presets to match against * @param tags tags to check against (i.e. tags of a map element) * @return null, or the "best" matching item for the given tag set */ public static PresetItem findBestMatch(Preset presets[], Map<String,String> tags) { return findBestMatch(presets, tags, false); } /** * Finds the preset item best matching a certain tag set, or null if no preset item matches. * To match, all (mandatory) tags of the preset item need to be in the tag set. * The preset item does NOT need to have all tags in the tag set, but the tag set needs * to have all (mandatory) tags of the preset item. * * If multiple items match, the most specific one (i.e. having most tags) wins. * If there is a draw, no guarantees are made. * * @param presets presets presets to match against * @param tags tags to check against (i.e. tags of a map element) * @param useAddressKeys use addr: keys if true * @return a preset or null if none found */ public static PresetItem findBestMatch(Preset presets[], Map<String,String> tags, boolean useAddressKeys) { int bestMatchStrength = 0; PresetItem bestMatch = null; if (tags==null || presets==null) { Log.e(DEBUG_TAG, "findBestMatch " + (tags==null?"tags null":"presets null")); return null; } // Build candidate list Set<PresetItem> possibleMatches = buildPossibleMatches(presets, tags, false); // if we only have address keys retry if (useAddressKeys && possibleMatches.size() == 0) { possibleMatches = buildPossibleMatches(presets, tags, true); } // Find best final int FIXED_WEIGHT = 100; // always prioritize presets with fixed keys for (PresetItem possibleMatch : possibleMatches) { int fixedTagCount = possibleMatch.getFixedTagCount()*FIXED_WEIGHT; if (fixedTagCount + possibleMatch.getRecommendedTags().size() < bestMatchStrength) { continue; // isn't going to help } int matches = 0; if (fixedTagCount > 0) { // has required tags if (possibleMatch.matches(tags)) { matches = fixedTagCount; } } if (possibleMatch.getRecommendedTags().size() > 0) { matches = matches + possibleMatch.matchesRecommended(tags); } if (matches > bestMatchStrength) { bestMatch = possibleMatch; bestMatchStrength = matches; } } // Log.d(DEBUG_TAG,"findBestMatch " + bestMatch); return bestMatch; } /** * Attempt to find a (any) match of the tags with the supplied presets * * @param presets presets to search in * @param tags tags to match * @return a preset or null if none found */ @Nullable public static PresetItem findMatch(@NonNull Preset presets[], @NonNull Map<String,String> tags) { if (tags==null || presets==null) { Log.e(DEBUG_TAG, "findBestMatch " + (tags==null?"tags null":"presets null")); return null; } // Build candidate list Set<PresetItem> possibleMatches = buildPossibleMatches(presets, tags, false); // Find match for (PresetItem possibleMatch : possibleMatches) { if (possibleMatch.getFixedTagCount() > 0) { // has required tags if (possibleMatch.matches(tags)) { return possibleMatch; } } else if (possibleMatch.getRecommendedTags().size() > 0 && possibleMatch.matchesRecommended(tags) > 0) { return possibleMatch; } } // Log.d(DEBUG_TAG,"findBestMatch " + bestMatch); return null; } /** * Return a set of presets that could match the tags * * @param presets current presets * @param tags the tags * @param useAddressKeys use address keys * @return set of presets */ private static Set<PresetItem> buildPossibleMatches(Preset[] presets, Map<String, String> tags, boolean useAddressKeys) { HashSet<PresetItem> possibleMatches = new HashSet<PresetItem>(); for (Preset p:presets) { if (p != null) { for (Entry<String, String> tag : tags.entrySet()) { String key = tag.getKey(); if (Tags.IMPORTANT_TAGS.contains(key) || (key.startsWith(Tags.KEY_ADDR_BASE) && useAddressKeys)) { String tagString = tag.getKey()+"\t"; possibleMatches.addAll(p.tagItems.get(tagString)); // for stuff that doesn't have fixed values possibleMatches.addAll(p.tagItems.get(tagString+tag.getValue())); } } } } return possibleMatches; } /** * Filter a list of elements by type * @param originalElements the list to filter * @param type the type to allow * @return a filtered list containing only elements of the specified type */ private static ArrayList<PresetElement> filterElements( ArrayList<PresetElement> originalElements, ElementType type) { ArrayList<PresetElement> filteredElements = new ArrayList<PresetElement>(); for (PresetElement e : originalElements) { if (!e.isDeprecated()) { if (e.appliesTo(type)) { filteredElements.add(e); } else if ((e instanceof PresetSeparator) && !filteredElements.isEmpty() && !(filteredElements.get(filteredElements.size()-1) instanceof PresetSeparator)) { // add separators if there is a non-separator element above them filteredElements.add(e); } } } return filteredElements; } /** * Represents an element (group or item) in a preset data structure */ public abstract class PresetElement implements Serializable { /** * */ private static final long serialVersionUID = 5L; String name; String nameContext = null; private String iconpath; private String mapiconpath; private transient Drawable icon; private transient BitmapDrawable mapIcon; PresetGroup parent; boolean appliesToWay; boolean appliesToNode; boolean appliesToClosedway; boolean appliesToRelation; boolean appliesToArea; private boolean deprecated = false; private String region = null; private String mapFeatures; /** * Creates the element, setting parent, name and icon, and registers with the parent * @param parent parent group (or null if this is the root group) * @param name name of the element * @param iconpath The icon path (either "http://" URL or "presets/" local image reference) */ public PresetElement(PresetGroup parent, String name, String iconpath) { this.parent = parent; this.name = name; this.iconpath = iconpath; mapiconpath = iconpath; icon = null; mapIcon = null; if (parent != null) { parent.addElement(this); } } public String getName() { return name; } /** * Return the name of this preset element, potentially translated * @return */ public String getTranslatedName() { if (nameContext!=null) { return po!=null?po.t(nameContext,getName()):getName(); } return po!=null?po.t(getName()):getName(); } /** * Return the icon for the preset or a place holder * @return */ public Drawable getIcon() { if (icon == null) { if (iconManager == null) { iconManager = getIconManager(App.getCurrentInstance().getApplicationContext()); } if (iconpath != null) { icon = iconManager.getDrawableOrPlaceholder(iconpath, 36); iconpath = null; } else { return iconManager.getPlaceHolder(36); } } return icon; } public BitmapDrawable getMapIcon() { if (mapIcon == null && mapiconpath != null) { if (iconManager == null) { iconManager = getIconManager(App.getCurrentInstance().getApplicationContext()); } mapIcon = iconManager.getDrawable(mapiconpath, de.blau.android.Map.ICON_SIZE_DP); mapiconpath = null; } return mapIcon; } public PresetGroup getParent() { return parent; } public void setParent(PresetGroup pg) { parent = pg; } /** * Returns a basic view representing the current element (i.e. a button with icon and name). * Can (and should) be used when implementing {@link #getView(PresetClickHandler)}. * @param selected if true highlight the background * @return the view */ private TextView getBaseView(Context ctx, boolean selected) { Resources res = ctx.getResources(); // GradientDrawable shape = new GradientDrawable(); // shape.setCornerRadius(8); TextView v = new TextView(ctx); float density = res.getDisplayMetrics().density; v.setText(getTranslatedName()); v.setTextColor(ContextCompat.getColor(ctx,R.color.preset_text)); v.setTextSize(TypedValue.COMPLEX_UNIT_SP,10); v.setEllipsize(TextUtils.TruncateAt.END); v.setMaxLines(2); v.setPadding((int)(4*density), (int)(4*density), (int)(4*density), (int)(4*density)); // v.setBackgroundDrawable(shape); if (this instanceof PresetGroup) { v.setBackgroundColor(ContextCompat.getColor(ctx, selected ? R.color.material_deep_teal_200 : R.color.dark_grey)); } else { v.setBackgroundColor(ContextCompat.getColor(ctx, selected ? R.color.material_deep_teal_500 : R.color.preset_bg)); } Drawable viewIcon = getIcon(); if (viewIcon != null) { v.setCompoundDrawables(null, viewIcon, null, null); v.setCompoundDrawablePadding((int)(4*density)); } else { // no icon, shouldn't happen anymore leave in logging for now Log.d(DEBUG_TAG,"No icon for " + getName()); } v.setWidth((int)(72*density)); v.setHeight((int)(72*density)); v.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); return v; } /** * Returns a view representing this element (i.e. a button with icon and name) * Implement this in subtypes * @param handler handler to handle clicks on the element (may be null) * @param selected highlight this element * @return a view ready to display to represent this element */ public abstract View getView(Context ctx, final PresetClickHandler handler, boolean selected); public boolean appliesTo(ElementType type) { switch (type) { case NODE: return appliesToNode; case WAY: return appliesToWay; case CLOSEDWAY: return appliesToClosedway; case RELATION: return appliesToRelation; case AREA: return appliesToArea; } return true; // should never happen } /** * Recursively sets the flag indicating that this element is relevant for nodes */ void setAppliesToNode() { if (!appliesToNode) { appliesToNode = true; if (parent != null) { parent.setAppliesToNode(); } } } /** * Recursively sets the flag indicating that this element is relevant for nodes */ void setAppliesToWay() { if (!appliesToWay) { appliesToWay = true; if (parent != null) { parent.setAppliesToWay(); } } } /** * Recursively sets the flag indicating that this element is relevant for nodes */ void setAppliesToClosedway() { if (!appliesToClosedway) { appliesToClosedway = true; if (parent != null) { parent.setAppliesToClosedway(); } } } /** * Recursively sets the flag indicating that this element is relevant for relations */ void setAppliesToRelation() { if (!appliesToRelation) { appliesToRelation = true; if (parent != null) { parent.setAppliesToRelation(); } } } /** * Recursively sets the flag indicating that this element is relevant for an area */ void setAppliesToArea() { if (!appliesToArea) { appliesToArea = true; if (parent != null) { parent.setAppliesToArea(); } } } void setMapFeatures(String url) { if (url != null) { mapFeatures = url; } } public Uri getMapFeatures() { return Uri.parse(mapFeatures); } void setNameContext(String context) { nameContext = context; } public boolean isDeprecated() { return deprecated; } public void setDeprecated(boolean deprecated) { this.deprecated = deprecated; } public String getRegion() { return region; } public void setRegion(String region) { this.region = region; } /** * Get an object documenting where in the hierarchy this element is. * * This is essentially the only unique way of identifying a specific preset * * @param root PresetGroup that this is relative to * @return and object containing the path elements */ public PresetElementPath getPath(PresetGroup root) { for (PresetElement e:root.getElements()) { if (e.equals(this)) { PresetElementPath result = new PresetElementPath(); result.path.add(e.getName()); return result; } else { if (e instanceof PresetGroup) { PresetElementPath result = getPath((PresetGroup) e); if (result != null) { result.path.add(0, e.getName()); return result; } } } } return null; } @Override public String toString() { return name + " " + iconpath + " " + mapiconpath + " " + appliesToWay + " " + appliesToNode + " " + appliesToClosedway + " " + appliesToRelation + " " + appliesToArea; } } /** * Represents a separator in a preset group */ public class PresetSeparator extends PresetElement { /** * */ private static final long serialVersionUID = 1L; public PresetSeparator(PresetGroup parent) { super(parent, "", null); } @Override public View getView(Context ctx, PresetClickHandler handler, boolean selected) { View v = new View(ctx); v.setMinimumHeight(1); v.setMinimumWidth(99999); // for WrappingLayout return v; } } /** * Represents a preset group, which may contain items, groups and separators */ public class PresetGroup extends PresetElement { /** * */ private static final long serialVersionUID = 2L; private final int groupIndex; /** Elements in this group */ private ArrayList<PresetElement> elements = new ArrayList<PresetElement>(); public PresetGroup(PresetGroup parent, String name, String iconpath) { super(parent,name,iconpath); groupIndex = allGroups.size(); allGroups.add(this); } public void addElement(PresetElement element) { addElement(element, true); } public void addElement(PresetElement element, boolean setParent) { elements.add(element); if (setParent) { element.setParent(this); } } public ArrayList<PresetElement> getElements() { return elements; } /** * Returns a view showing this group's icon * @param handler the handler handling clicks on the icon * @param selected highlight the background if true * @return a view/button representing this PresetElement */ @Override public View getView(Context ctx, final PresetClickHandler handler, boolean selected) { TextView v = super.getBaseView(ctx, selected); v.setTypeface(null,Typeface.BOLD); if (handler != null) { v.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { handler.onGroupClick(PresetGroup.this); } }); v.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { return handler.onGroupLongClick(PresetGroup.this); } }); } v.setTag("G"+this.getGroupIndex()); return v; } public int getGroupIndex() { return groupIndex; } /** * @param selectedElement highlight the background if true * @return a view showing the content (nodes, subgroups) of this group */ public View getGroupView(Context ctx, PresetClickHandler handler, ElementType type, PresetElement selectedElement) { ScrollView scrollView = new ScrollView(ctx); scrollView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); return getGroupView(ctx, scrollView, handler, type, selectedElement); } /** * @param selectedElement highlight the background if true * @return a view showing the content (nodes, subgroups) of this group */ public View getGroupView(Context ctx, ScrollView scrollView, PresetClickHandler handler, ElementType type, PresetElement selectedElement) { scrollView.removeAllViews(); WrappingLayout wrappingLayout = new WrappingLayout(ctx); float density = ctx.getResources().getDisplayMetrics().density; // wrappingLayout.setBackgroundColor(ctx.getResources().getColor(android.R.color.white)); wrappingLayout.setBackgroundColor(ContextCompat.getColor(ctx,android.R.color.transparent)); // make transparent wrappingLayout.setHorizontalSpacing((int)(SPACING*density)); wrappingLayout.setVerticalSpacing((int)(SPACING*density)); ArrayList<PresetElement> filteredElements = type == null ? elements : filterElements(elements, type); ArrayList<View> childViews = new ArrayList<View>(); for (PresetElement element : filteredElements) { childViews.add(element.getView(ctx, handler, element.equals(selectedElement))); } wrappingLayout.setWrappedChildren(childViews); scrollView.addView(wrappingLayout); return scrollView; } } /** Represents a preset item (e.g. "footpath", "grocery store") */ public class PresetItem extends PresetElement { /** * */ private static final long serialVersionUID = 10L; /** "fixed" tags, i.e. the ones that have a fixed key-value pair */ private LinkedHashMap<String, StringWithDescription> fixedTags = new LinkedHashMap<String, StringWithDescription>(); /** Tags that are not in the optional section, but do not have a fixed key-value-pair. * The map key provides the key, while the map value (String[]) provides the possible values. */ private LinkedHashMap<String, StringWithDescription[]> recommendedTags = new LinkedHashMap<String, StringWithDescription[]>(); /** Tags that are in the optional section. * The map key provides the key, while the map value (String[]) provides the possible values. */ private LinkedHashMap<String, StringWithDescription[]> optionalTags = new LinkedHashMap<String, StringWithDescription[]>(); /** * Hints to be displayed in a suitable form */ private HashMap<String, String> hints = new HashMap<String, String>(); /** * Default values */ private HashMap<String, String> defaults = null; /** * Non standard on values */ private HashMap<String, String> onValue = null; /** * Roles */ private LinkedList<StringWithDescription> roles = null; /** * Linked names of presets */ private LinkedList<String> linkedPresetNames = null; /** * Sort values or not */ private HashMap<String,Boolean> sort = null; /** * Key to key type */ private HashMap<String,PresetKeyType> keyType = new HashMap<String,PresetKeyType>(); /** * Key to match properties */ private HashMap<String,MatchType> matchType = null; /** * Key to combo and multiselect delimiters */ private HashMap<String,String> delimiters = null; /** * Key to combo and multiselect editable property */ private HashMap<String,Boolean> editable = null; /** * Translation contexts */ private HashMap<String,String> textContext = null; private HashMap<String,String> valueContext = null; /** * Scripts for pre-filling text fields */ private HashMap<String,String> javascript = null; /** * true if a chunk */ private boolean chunk = false; private final int itemIndex; public PresetItem(PresetGroup parent, String name, String iconpath, String types) { super(parent, name, iconpath); if (types == null) { // Type not specified, assume all types setAppliesToNode(); setAppliesToWay(); setAppliesToClosedway(); setAppliesToRelation(); setAppliesToArea(); } else { String[] typesArray = types.split(","); for (String type : typesArray) { if (Node.NAME.equals(type)) { setAppliesToNode(); } else if (Way.NAME.equals(type)) { setAppliesToWay(); } else if ("closedway".equals(type)) { setAppliesToClosedway(); // FIXME don't add if it really an area } else if ("multipolygon".equals(type)) { setAppliesToArea(); } else if ("area".equals(type)) { setAppliesToArea(); // } else if (Relation.NAME.equals(type)) { setAppliesToRelation(); } } } itemIndex = allItems.size(); allItems.add(this); } /** * build the search index */ void buildSearchIndex() { addToSearchIndex(name); if (parent != null) { String parentName = parent.getName(); if (parentName != null && parentName.length() > 0) { addToSearchIndex(parentName); } } for (String k:fixedTags.keySet()) { addToSearchIndex(k); addToSearchIndex(fixedTags.get(k).getValue()); addToSearchIndex(fixedTags.get(k).getDescription()); } } /** * Add a name, any translation and the individual words to the index. * Currently we assume that all words are significant * @param term */ void addToSearchIndex(String term) { // search support if (term != null) { String normalizedName = SearchIndexUtils.normalize(term); searchIndex.add(normalizedName,this); String words[] = normalizedName.split(" "); if (words.length > 1) { for (String w:words) { searchIndex.add(w,this); } } if (po != null) { // and any translation String normalizedTranslatedName = SearchIndexUtils.normalize(po.t(term)); translatedSearchIndex.add(normalizedTranslatedName,this); String translastedWords[] = normalizedName.split(" "); if (translastedWords.length > 1) { for (String w:translastedWords) { translatedSearchIndex.add(w,this); } } } } } /** * Adds a fixed tag to the item, registers the item in the tagItems map and populates autosuggest. * @param key key name of the tag * @param value value of the tag */ public void addTag(final String key, final PresetKeyType type, String value, String text) { if (key == null) { throw new NullPointerException("null key not supported"); } if (value == null) { value = ""; } if (text != null && po != null) { text = po.t(text); } fixedTags.put(key, new StringWithDescription(value, text)); if (!chunk) { tagItems.add(key+"\t"+value, this); } // Log.d(DEBUG_TAG,name + " key " + key + " type " + type); keyType.put(key,type); if (appliesTo(ElementType.NODE)) { autosuggestNodes.add(key, value.length() > 0 ? new StringWithDescription(value, text) : null); } if (appliesTo(ElementType.WAY)) { autosuggestWays.add(key, value.length() > 0 ? new StringWithDescription(value, text) : null); } if (appliesTo(ElementType.CLOSEDWAY)) { autosuggestClosedways.add(key, value.length() > 0 ? new StringWithDescription(value, text) : null); } if (appliesTo(ElementType.RELATION)) { autosuggestRelations.add(key, value.length() > 0 ? new StringWithDescription(value, text) : null); } if (appliesTo(ElementType.AREA)) { autosuggestAreas.add(key, value.length() > 0 ? new StringWithDescription(value, text) : null); } } /** * Adds a recommended or optional tag to the item and populates autosuggest. * @param optional true if optional, false if recommended * @param key key name of the tag * @param values values string from the XML (comma-separated list of possible values) */ public void addTag(boolean optional, String key, PresetKeyType type, String values) { addTag(optional, key, type, values, null, null, COMBO_DELIMITER, null); } public void addTag(boolean optional, String key, PresetKeyType type, String values, String displayValues, String shortDescriptions, final String delimiter, String valuesContext) { String[] valueArray = (values == null) ? new String[0] : values.split(Pattern.quote(delimiter)); String[] displayValueArray = (displayValues == null) ? new String[0] : displayValues.split(Pattern.quote(delimiter)); String[] shortDescriptionArray = (shortDescriptions == null) ? new String[0] : shortDescriptions.split(Pattern.quote(delimiter)); StringWithDescription[] valuesWithDesc = new StringWithDescription[valueArray.length]; boolean useDisplayValues = valueArray.length == displayValueArray.length; boolean useShortDescriptions = !useDisplayValues && valueArray.length == shortDescriptionArray.length; for (int i=0;i<valueArray.length;i++){ String description = null; if (useDisplayValues) { description = (po != null && displayValueArray[i] != null) ? (valuesContext != null ? po.t(valuesContext,displayValueArray[i]) : po.t(displayValueArray[i])) : displayValueArray[i]; } else if (useShortDescriptions) { description = (po != null && shortDescriptionArray[i] != null) ? (valuesContext != null ? po.t(valuesContext,shortDescriptionArray[i]) : po.t(shortDescriptionArray[i])):shortDescriptionArray[i]; } valuesWithDesc[i] = new StringWithDescription(valueArray[i], description); } addTag(optional, key, type, valuesWithDesc, delimiter); } public void addTag(boolean optional, String key, PresetKeyType type, StringWithDescription[] valueArray, final String delimiter) { if (!chunk){ if (valueArray==null || valueArray.length == 0) { tagItems.add(key+"\t", this); } else { for (StringWithDescription v:valueArray) { tagItems.add(key+"\t"+v.getValue(), this); } } } // Log.d(DEBUG_TAG,name + " key " + key + " type " + type); keyType.put(key,type); if (appliesTo(ElementType.NODE)) { autosuggestNodes.add(key, valueArray); } if (appliesTo(ElementType.WAY)) { autosuggestWays.add(key, valueArray); } if (appliesTo(ElementType.CLOSEDWAY)) { autosuggestClosedways.add(key, valueArray); } if (appliesTo(ElementType.RELATION)) { autosuggestRelations.add(key, valueArray); } if (appliesTo(ElementType.AREA)) { autosuggestAreas.add(key, valueArray); } (optional ? optionalTags : recommendedTags).put(key, valueArray); // only save delimiter if not default if ((type == PresetKeyType.MULTISELECT && !MULTISELECT_DELIMITER.equals(delimiter)) || (type == PresetKeyType.COMBO && !COMBO_DELIMITER.equals(delimiter))) { addDelimiter(key,delimiter); } } public void addRole(final StringWithDescription value) { if (roles == null) { roles = new LinkedList<StringWithDescription>(); } roles.add(value); } public void addAllRoles(LinkedList<StringWithDescription> newRoles) { if (roles == null) { roles = newRoles; // doesn't matter if newRoles is null } else if (newRoles != null){ roles.addAll(newRoles); } } public List<StringWithDescription> getRoles() { return roles != null ? Collections.unmodifiableList(roles) : null; } /** * Save hint for the tag * @param key * @param hint */ public void addHint(String key, String hint) { hints.put(key, hint); } /** * Return, potentially translated, "text" field from preset * @param key * @return */ public String getHint(String key) { if (po != null) { return po.t(hints.get(key)); } return hints.get(key); } /** * Save default for the tag * @param key * @param defaultValue */ public void addDefault(String key, String defaultValue) { if (defaults == null) { defaults = new HashMap<String, String>(); } defaults.put(key, defaultValue); } public void addAllDefaults(HashMap<String, String> newDefaults) { if (defaults == null) { defaults = newDefaults; // doesn't matter if newDefaults is null } else if (newDefaults != null){ defaults.putAll(newDefaults); } } public String getDefault(String key) { return defaults != null ? defaults.get(key) : null; } /** * Save non-standard values for the tag * @param key * @param on */ public void addOnValue(String key, String on) { if (onValue == null) { onValue = new HashMap<String, String>(); } onValue.put(key, on); } public void addAllOnValues(HashMap<String, String> newOnValues) { if (onValue == null) { onValue = newOnValues; // doesn't matter if newOnValues is null } else if (newOnValues != null){ onValue.putAll(newOnValues); } } /** * Get the value that should be used for a checked check box * @param key the key for the checkbox * @return either default value or what has been set in the preset */ public String getOnValue(String key) { if (onValue != null) { return onValue.get(key) != null ? onValue.get(key) : "yes"; } return "yes"; } /** * Save non-standard values for the tag * @param key * @param on */ public void addDelimiter(String key, String delimiter) { if (delimiters == null) { delimiters = new HashMap<String,String>(); } delimiters.put(key, delimiter); } public void addAllDelimiters(HashMap<String, String> newDelimiters) { if (delimiters == null) { delimiters = newDelimiters; // doesn't matter if newOnValues is null } else if (newDelimiters != null){ delimiters.putAll(newDelimiters); } } public char getDelimiter(String key) { return (delimiters != null && delimiters.get(key) != null? delimiters.get(key) : (getKeyType(key) == PresetKeyType.MULTISELECT ? MULTISELECT_DELIMITER : COMBO_DELIMITER)).charAt(0); } public void setMatchType(String key, String match) { if (matchType == null) { matchType = new HashMap<String,MatchType>(); } MatchType type = null; if (match.equals("none")) { type = MatchType.NONE; } else if (match.equals("key")) { type = MatchType.KEY; } else if (match.equals("key!")) { type = MatchType.KEY_NEG; } else if (match.equals("keyvalue")) { type = MatchType.KEY_VALUE; } else if (match.equals("keyvalue!")) { type = MatchType.KEY_VALUE_NEG; } matchType.put(key, type); } public void setAllMatchTypes(HashMap<String,MatchType> newMatchTypes) { if (matchType == null) { matchType = newMatchTypes; // doesn't matter if newMatchTypes is null } else if (newMatchTypes != null){ matchType.putAll(newMatchTypes); } } public MatchType getMatchType(String key) { return matchType != null ? matchType.get(key) : null; } public void addLinkedPresetName(String presetName) { if (linkedPresetNames == null) { linkedPresetNames = new LinkedList<String>(); } linkedPresetNames.add(presetName); } public void addAllLinkedPresetNames(LinkedList<String> newLinkedPresetNames) { if (linkedPresetNames == null) { linkedPresetNames = newLinkedPresetNames; // doesn't matter if newLinkedPresetNames is null } else if (newLinkedPresetNames != null){ linkedPresetNames.addAll(newLinkedPresetNames); } } public LinkedList<String> getLinkedPresetNames() { return linkedPresetNames; } /** * Returns a list of linked preset items * * @param noPrimary if true only items will be returned that doen't correspond to primary OSM objects * @return list of PresetItems */ public List<PresetItem> getLinkedPresets(boolean noPrimary) { ArrayList<PresetItem> result = new ArrayList<PresetItem>(); Log.e(DEBUG_TAG,"Linked presets for " + getName()); if (linkedPresetNames != null) { linkedLoop: for (String n:linkedPresetNames) { Integer index = getItemIndexByName(n); // FIXME this involves a sequential search if (index != null) { PresetItem candidateItem = allItems.get(index.intValue()); if (noPrimary) { // remove primary objects Set<String>linkedPresetTags = candidateItem.getFixedTags().keySet(); if (linkedPresetTags.isEmpty()) { linkedPresetTags = candidateItem.getRecommendedTags().keySet(); } for (String k:linkedPresetTags) { if (Tags.IMPORTANT_TAGS.contains(k)) { continue linkedLoop; } } } result.add(candidateItem); } else { Log.e(DEBUG_TAG,"Couldn't find linked preset " + n); } } } return result; } public void setSort(String key, boolean sortIt) { if (sort == null) { sort = new HashMap<String,Boolean>(); } sort.put(key,sortIt); } public void setAllSort(HashMap<String,Boolean> newSort) { if (sort == null) { sort = newSort; // doesn't matter if newSort is null } else if (newSort != null){ sort.putAll(newSort); } } public boolean sortIt(String key) { return (sort == null || sort.get(key) == null) ? true : sort.get(key); } public void setJavaScript(String key, String script) { if (javascript == null) { javascript = new HashMap<String,String>(); } javascript.put(key,script); } public void setAllJavaScript(HashMap<String,String> newJavaScript) { if (javascript == null) { javascript = newJavaScript; // doesn't matter if newSort is null } else if (newJavaScript != null){ javascript.putAll(newJavaScript); } } public String getJavaScript(String key) { return javascript == null ? null : javascript.get(key); } public void setEditable(String key, boolean isEditable) { if (editable == null) { editable = new HashMap<String,Boolean>(); } editable.put(key,isEditable); } public void setAllEditable(HashMap<String,Boolean> newEditable) { if (editable == null) { editable = newEditable; // doesn't matter if newSort is null } else if (newEditable != null){ editable.putAll(newEditable); } } /** * Check is the combo or multiselect should be editable * NOTE: contrary to the definition in JOSM the default is false/no * @param key * @return */ public boolean isEditable(String key) { return (editable == null || editable.get(key) == null) ? false : editable.get(key); } public void setTextContext(String key, String textContext) { if (this.textContext == null) { this.textContext = new HashMap<String, String>(); } this.textContext.put(key, textContext); } public String getTextContext(String key) { return textContext.get(key); } public void setValueContext(String key, String valueContext) { if (this.valueContext == null) { this.valueContext = new HashMap<String, String>(); } this.valueContext.put(key, valueContext); } public String getValueContext(String key) { return valueContext.get(key); } /** * @return the fixed tags belonging to this item (unmodifiable) */ public Map<String,StringWithDescription> getFixedTags() { return Collections.unmodifiableMap(fixedTags); } /** * Return the number of keys with fixed values * @return */ public int getFixedTagCount() { return fixedTags.size(); } public boolean isFixedTag(String key) { return fixedTags.containsKey(key); } public boolean isRecommendedTag(String key) { return recommendedTags.containsKey(key); } public boolean isOptionalTag(String key) { return optionalTags.containsKey(key); } public Map<String,StringWithDescription[]> getRecommendedTags() { return Collections.unmodifiableMap(recommendedTags); } public Map<String,StringWithDescription[]> getOptionalTags() { return Collections.unmodifiableMap(optionalTags); } /** * Return a ist of the values suitable for autocomplete, note vales for fixed tags are not returned * @param key * @return */ public Collection<StringWithDescription> getAutocompleteValues(String key) { Collection<StringWithDescription> result = new LinkedHashSet<StringWithDescription>(); if (recommendedTags.containsKey(key)) { result.addAll(Arrays.asList(recommendedTags.get(key))); } else if (optionalTags.containsKey(key)) { result.addAll(Arrays.asList(optionalTags.get(key))); } return result; } /** * Return what kind of selection applies to the values of this key * @param key * @return */ public PresetKeyType getKeyType(String key) { PresetKeyType result = keyType.get(key); if (result==null) { return PresetKeyType.TEXT; } return result; } /** * Checks if all tags belonging to this item exist in the given tagSet, * i.e. the node to which the tagSet belongs could be what this preset specifies. * @param tagSet the tagSet to compare against this item * @return */ public boolean matches(Map<String,String> tagSet) { if (name.equals("Addresses")) { Log.d(DEBUG_TAG,"matching addresses fixed"); } for (Entry<String, StringWithDescription> tag : fixedTags.entrySet()) { // for each own tag String key = tag.getKey(); if (!tagSet.containsKey(key)) { return false; } MatchType type = getMatchType(key); if (type==MatchType.NONE) { continue; } String otherTagValue = tagSet.get(key); if (!tag.getValue().equals(otherTagValue) && type!=MatchType.KEY) { return false; } } return true; } /** * Returns the number of matches between the list of recommended tags (really a misnomer) and the provided tags * @param tagSet * @return number of matches */ public int matchesRecommended(Map<String,String> tagSet) { if (name.equals("Addresses")) { Log.d(DEBUG_TAG,"matching addresses recommended"); } int matches = 0; for (Entry<String, StringWithDescription[]> tag : recommendedTags.entrySet()) { // for each own tag String key = tag.getKey(); if (tagSet.containsKey(key)) { // key could have null value in the set // value not empty if (getMatchType(key)==MatchType.NONE) { // don't count this break; } if (getMatchType(key)==MatchType.KEY) { matches++; break; } String otherTagValue = tagSet.get(key); for (StringWithDescription v:tag.getValue()) { if (v.equals(otherTagValue)) { matches++; break; } } } } return matches; } @Override public View getView(Context ctx, final PresetClickHandler handler, boolean selected) { View v = super.getBaseView(ctx, selected); if (handler != null) { v.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { handler.onItemClick(PresetItem.this); } }); v.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { return handler.onItemLongClick(PresetItem.this); } }); } v.setTag(""+this.getItemIndex()); return v; } /** * Return true if the key is contained in this preset * @param key * @return */ public boolean hasKey(String key) { return fixedTags.containsKey(key) || recommendedTags.containsKey(key) || optionalTags.containsKey(key); } /** * Return true if the key and value is contained in this preset taking match attribute in to account * Note mathe="none" is handled the same as "key" in this method * @param key * @return */ public boolean hasKeyValue(String key, String value) { StringWithDescription swd = fixedTags.get(key); if (swd!=null) { if ("".equals(value) || swd.getValue()==null || swd.equals(value) || "".equals(swd.getValue())) { return true; } } MatchType type = getMatchType(key); PresetKeyType keyType = getKeyType(key); if (recommendedTags.containsKey(key)) { if (type==MatchType.KEY || type==MatchType.NONE || keyType==PresetKeyType.MULTISELECT) { // MULTISELECT always editable return true; } StringWithDescription[] swdArray = recommendedTags.get(key); if (swdArray != null && swdArray.length > 0) { for (StringWithDescription v:swdArray) { if ("".equals(value) || v.getValue()==null || v.equals(value) || "".equals(v.getValue())) { return true; } } } else { return true; } } if (optionalTags.containsKey(key)) { if (type==MatchType.KEY || type==MatchType.NONE || keyType==PresetKeyType.MULTISELECT) { // MULTISELECT always editable return true; } StringWithDescription[] swdArray = optionalTags.get(key); if (swdArray != null && swdArray.length > 0) { for (StringWithDescription v:swdArray) { if ("".equals(value) || v.getValue()==null || v.equals(value) || "".equals(v.getValue())) { return true; } } } else { return true; } } return false; } /** * Get the index of this item * @return the index */ public int getItemIndex() { return itemIndex; } @Override public String toString() { String tagStrings = ""; tagStrings = " required: "; for (String k:fixedTags.keySet()) { tagStrings = tagStrings + " " + k + "=" + fixedTags.get(k); } tagStrings = tagStrings + " recommended: "; for (String k:recommendedTags.keySet()) { tagStrings = tagStrings + " " + k + "="; for (StringWithDescription v:recommendedTags.get(k)) { tagStrings = tagStrings + " " + v.getValue(); } } tagStrings = tagStrings + " optional: "; for (String k:optionalTags.keySet()) { tagStrings = tagStrings + " " + k + "="; for (StringWithDescription v:optionalTags.get(k)) { tagStrings = tagStrings + " " + v.getValue(); } } return super.toString() + tagStrings; } void setChunk() { chunk = true; } boolean isChunk() { return chunk; } public String toJSON() { String jsonString = ""; for (String k:fixedTags.keySet()) { jsonString = jsonString + tagToJSON(k, fixedTags.get(k).getValue()); } for (String k:recommendedTags.keySet()) { // check match attribute MatchType match = getMatchType(k); PresetKeyType type = getKeyType(k); if (isEditable(k) || type==PresetKeyType.TEXT) { jsonString = jsonString + tagToJSON(k, null); } if (!isEditable(k) && type != PresetKeyType.TEXT && (match==null || match == MatchType.KEY_VALUE || match == MatchType.KEY)) { for (StringWithDescription v:recommendedTags.get(k)) { jsonString = jsonString + tagToJSON(k, v.getValue()); } } } for (String k:optionalTags.keySet()) { // check match attribute MatchType match = getMatchType(k); PresetKeyType type = getKeyType(k); if (isEditable(k) || type==PresetKeyType.TEXT || (match != null && match != MatchType.KEY_VALUE)) { jsonString = jsonString + tagToJSON(k, null); } if (!isEditable(k) && type != PresetKeyType.TEXT && (match==null || match == MatchType.KEY_VALUE || match == MatchType.KEY)) { for (StringWithDescription v:optionalTags.get(k)) { jsonString = jsonString + tagToJSON(k, v.getValue()); } } } return jsonString; } /** * For taginfo.openstreetmap.org * @param key * @param value * @return */ private String tagToJSON(String key, String value) { String presetName = name; PresetElement p = getParent(); while (p != null && p != rootGroup && !"".equals(p.getName())){ presetName = p.getName() + "/" + presetName; p = p.getParent(); } String result = "{\"description\":\"" + presetName + "\",\"key\": \"" + key + "\"" + (value == null ? "" : ",\"value\": \"" + value + "\""); result = result + ",\"object_types\": ["; boolean first = true; if (appliesToNode) { result = result + "\"node\""; first = false; } if (appliesToWay) { if (!first) { result = result + ","; } result = result + "\"way\""; first = false; } if (appliesToRelation) { if (!first) { result = result + ","; } result = result + "\"relation\""; first = false; } if (appliesToClosedway || appliesToArea) { if (!first) { result = result + ","; } result = result + "\"area\""; first = false; } return result + "]},\n"; } public void groupI18nKeys() { Util.groupI18nKeys(recommendedTags); Util.groupI18nKeys(optionalTags); } } /** * Adapter providing the preset elements in this group * currently unused, left here in case it is later needed */ @SuppressWarnings("unused") private class PresetGroupAdapter extends BaseAdapter { private final ArrayList<PresetElement> elements; private PresetClickHandler handler; private final Context context; private PresetGroupAdapter(Context ctx, ArrayList<PresetElement> content, ElementType type, PresetClickHandler handler) { this.handler = handler; context = ctx; elements = filterElements(content, type); } @Override public View getView(int position, View convertView, ViewGroup parent) { return getItem(position).getView(context, handler, false); } @Override public int getCount() { return elements.size(); } @Override public PresetElement getItem(int position) { return elements.get(position); } @Override public long getItemId(int position) { return position; } @Override public boolean isEnabled(int position) { return !(getItem(position) instanceof PresetSeparator); } } /** Interface for handlers handling clicks on item or group icons */ public interface PresetClickHandler { void onItemClick(PresetItem item); boolean onItemLongClick(PresetItem item); void onGroupClick(PresetGroup group); boolean onGroupLongClick(PresetGroup group); } public static Collection<String> getAutocompleteKeys(Preset[] presets, ElementType type) { Collection<String> result = new LinkedHashSet<String>(); for (Preset p:presets) { if (p!=null) { switch (type) { case NODE: result.addAll(p.autosuggestNodes.getKeys()); break; case WAY: result.addAll(p.autosuggestWays.getKeys()); break; case CLOSEDWAY: result.addAll(p.autosuggestClosedways.getKeys()); break; case RELATION: result.addAll(p.autosuggestRelations.getKeys()); break; case AREA: result.addAll(p.autosuggestAreas.getKeys()); break; default: return null; // should never happen, all cases are covered } } } List<String> r = new ArrayList<String>(result); Collections.sort(r); return r; } public static Collection<StringWithDescription> getAutocompleteValues(Preset[] presets, ElementType type, String key) { Collection<StringWithDescription> result = new LinkedHashSet<StringWithDescription>(); for (Preset p:presets) { if (p!=null) { switch (type) { case NODE: result.addAll(p.autosuggestNodes.get(key)); break; case WAY: result.addAll(p.autosuggestWays.get(key)); break; case CLOSEDWAY: result.addAll(p.autosuggestClosedways.get(key)); break; case RELATION: result.addAll(p.autosuggestRelations.get(key)); break; case AREA: result.addAll(p.autosuggestAreas.get(key)); break; default: return Collections.emptyList(); } } } List<StringWithDescription> r = new ArrayList<StringWithDescription>(result); Collections.sort(r); return r; } public static MultiHashMap<String, PresetItem> getSearchIndex(Preset[] presets) { MultiHashMap<String, PresetItem> result = new MultiHashMap<String, PresetItem>(); for (Preset p:presets) { if (p != null) { result.addAll(p.searchIndex); } } return result; } public static MultiHashMap<String, PresetItem> getTranslatedSearchIndex(Preset[] presets) { MultiHashMap<String, PresetItem> result = new MultiHashMap<String, PresetItem>(); for (Preset p:presets) { if (p!=null) { result.addAll(p.translatedSearchIndex); } } return result; } /** * Build an intent to startup up the correct mapfeatures wiki page * @param ctx * @param p * @return */ public static Intent getMapFeaturesIntent(Context ctx, PresetItem p) { Uri uri = null; if (p != null) { try { uri = p.getMapFeatures(); } catch (NullPointerException npe) { // Log.d(DEBUG_TAG,"Preset " + p.getName() + " has no/invalid map feature uri"); } } if (uri == null) { uri = Uri.parse(ctx.getString(R.string.link_mapfeatures)); } return new Intent(Intent.ACTION_VIEW, uri); } /** * Split multi select values with the preset defined delimiter character * @param values list of values that can potentially be split * @param preset the preset that sould be used * @param key the key used to determine the delimter value * @return list of split values */ @Nullable public static ArrayList<String> splitValues(ArrayList<String>values, @NonNull PresetItem preset, @NonNull String key) { ArrayList<String> result = new ArrayList<String>(); String delimiter = String.valueOf(preset.getDelimiter(key)); if (values==null) { return null; } for (String v:values) { if (v==null) { continue; } for (String s:v.split(Pattern.quote(delimiter))) { result.add(s.trim()); } } return result; } /** * This is for the taginfo project repo and not really for testing */ public static boolean generateTaginfoJson(Context ctx, String filename) { Preset[] presets = App.getCurrentPresets(ctx); PrintStream outputStream = null; FileOutputStream fout = null; try { // String filename = new SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(new Date())+".json"; File outfile = new File(FileUtil.getPublicDirectory(), filename); fout = new FileOutputStream(outfile); outputStream = new PrintStream(new BufferedOutputStream(fout)); outputStream.println("{"); outputStream.println("\"data_format\":1,"); outputStream.println("\"data_url\":\"https://raw.githubusercontent.com/MarcusWolschon/osmeditor4android/master/taginfo.json\","); outputStream.println("\"project\":{"); outputStream.println("\"name\":\"Vespucci\","); outputStream.println("\"description\":\"Offline editor for OSM data on Android.\","); outputStream.println("\"project_url\":\"https://github.com/MarcusWolschon/osmeditor4android\","); outputStream.println("\"doc_url\":\"http://vespucci.io/\","); outputStream.println("\"icon_url\":\"https://raw.githubusercontent.com/MarcusWolschon/osmeditor4android/master/res/drawable/osm_logo.png\","); outputStream.println("\"keywords\":["); outputStream.println("\"editor\""); outputStream.println("]},"); outputStream.println("\"tags\":["); for (int i=0;i<presets.length;i++) { String json = presets[i].toJSON(); if (i==presets.length-1) { int comma = json.lastIndexOf(','); json = json.substring(0, comma); } outputStream.print(json); } outputStream.println("]}"); } catch (Exception e) { Log.e(DEBUG_TAG, "Export failed - " + filename); e.printStackTrace(); return false; } finally { SavingHelper.close(outputStream); SavingHelper.close(fout); } return true; } }