package de.blau.android.filter; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import java.util.SortedMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.design.widget.FloatingActionButton; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.RelativeLayout; import de.blau.android.Main; import de.blau.android.R; import de.blau.android.osm.Node; import de.blau.android.osm.OsmElement; import de.blau.android.osm.Relation; import de.blau.android.osm.RelationMember; import de.blau.android.osm.Way; import de.blau.android.prefs.Preferences; import de.blau.android.util.Snack; import de.blau.android.util.Util; /** * Filter plus UI for filtering on tags * NOTE: the relevant ways should be processed before nodes * @author simon * */ public class TagFilter extends Filter { public static final String DEFAULT_FILTER = "Default"; private class FilterEntry implements Serializable { private static final long serialVersionUID = 2L; /** * Entry is active */ boolean active = false; /** * Include matching elements */ boolean include = false; /** * Include all element types */ boolean allElements = false; /** * Include way nodes */ boolean withWayNodes = false; /** * OSM element type */ String type; /** * Regular expression for keys of tags */ Pattern key; /** * Regular expression for values of tags */ Pattern value; FilterEntry(boolean include, String type, String key, String value, boolean active) { this.include = include; allElements = "*".equals(type); // just check this once withWayNodes = type.endsWith("+"); this.type = withWayNodes ? type.substring(0, type.length()-1) : type; this.key = key != null && !"".equals(key) ? Pattern.compile(key) : null; this.value = value != null && !"".equals(value) ? Pattern.compile(value) : null; this.active = active; } /** * Test it filter entry matches * @param type OSM object type * @param key key of tag * @param value value of tag * @return true if a match */ boolean match(String type, String key, String value) { if (allElements || this.type.equals(type)) { Matcher keyMatcher = null; if (this.key != null) { if (key == null) { return false; } keyMatcher = this.key.matcher(key); } Matcher valueMatcher = null; if (this.value != null) { if (value == null) { return false; } valueMatcher = this.value.matcher(value); } return (keyMatcher == null || keyMatcher.matches()) && (valueMatcher == null || valueMatcher.matches()); } return false; } @Override public String toString() { return "Active " + active + " include " + include + " type " + type + " key " + key + " value " + value; } } private ArrayList<FilterEntry> filter = new ArrayList<FilterEntry>(); /** * */ private static final long serialVersionUID = 1L; private final static String DEBUG_TAG = "TagFilter"; private boolean enabled = true; transient private SQLiteDatabase mDatabase; public TagFilter(Context context) { super(); init(context); // filter.clear(); // filter, include INTEGER DEFAULT 0, type TEXT DEFAULT '*', key TEXT DEFAULT '*', value DEFAULT '*', active INTEGER ; Cursor dbresult = mDatabase.query( "filterentries", new String[] {"include", "type", "key", "value", "active"}, "filter = ?", new String[] {DEFAULT_FILTER}, null, null, null); dbresult.moveToFirst(); for (int i = 0; i < dbresult.getCount(); i++) { try { filter.add(new FilterEntry( dbresult.getInt(0) == 1, dbresult.getString(1), dbresult.getString(2), dbresult.getString(3), dbresult.getInt(4) == 1)); } catch (PatternSyntaxException psex) { Log.e(DEBUG_TAG,psex.getMessage()); if (context instanceof Activity) { Snack.barError((Activity)context, context.getString(R.string.toast_invalid_filter_regexp,dbresult.getString(2),dbresult.getString(3))); } } dbresult.moveToNext(); } dbresult.close(); } @Override public void init(Context context) { mDatabase = new TagFilterDatabaseHelper(context).getReadableDatabase(); } private Include filter(OsmElement e) { Include include = Include.DONT; String type = e.getName(); for (FilterEntry f:filter) { if (f.active) { Include match = Include.DONT; SortedMap<String,String>tags = e.getTags(); if (tags != null && tags.size() > 0) { for (Entry<String,String>t:tags.entrySet()) { if (f.match(type,t.getKey(),t.getValue())) { match = f.withWayNodes ? Include.INCLUDE_WITH_WAYNODES : Include.INCLUDE; break; } } } else { match = f.match(type,null,null) ? (f.withWayNodes ? Include.INCLUDE_WITH_WAYNODES : Include.INCLUDE) : Include.DONT; } if (match != Include.DONT) { // we have a match // Log.d(DEBUG_TAG,e.getDescription(true) + " matched " + f.toString()); include = f.include ? match : Include.DONT; //FIXME should relation membership be able to override this? break; } } } if (include == Include.DONT) { // check if it is a relation member List<Relation> parents = e.getParentRelations(); if (parents != null) { for (Relation r:parents) { Include relationInclude = testRelation(r, false); if (relationInclude != null && relationInclude != Include.DONT) { return relationInclude; // inherit include status from relation } } } } // Log.d(DEBUG_TAG,e.getDescription() + " include: " + include); return include; } @Override public boolean include(Node node, boolean selected) { if (!enabled || selected) { return true; } Include include = cachedNodes.get(node); if (include != null) { return include != Include.DONT; } include = filter(node); cachedNodes.put(node,include); return include != Include.DONT; } @Override public boolean include(Way way, boolean selected) { if (!enabled) { return false; } Include include = cachedWays.get(way); if (include != null) { return include != Include.DONT; } include = filter(way); if (include == Include.INCLUDE_WITH_WAYNODES) { for (Node n:way.getNodes()) { Include includeNode = cachedNodes.get(n); if (includeNode == null || (include != Include.DONT && includeNode == Include.DONT)) { // if not originally included overwrite now if (include == Include.DONT && (n.hasTags() || n.hasParentRelations())) { // no entry yet so we have to check tags and relations include(n,false); continue; } cachedNodes.put(n,include); } } } cachedWays.put(way,include); return include != Include.DONT || selected; } @Override public boolean include(Relation relation, boolean selected) { return testRelation(relation, selected) != Include.DONT; } Include testRelation(Relation relation, boolean selected) { if (!enabled || selected) { return Include.INCLUDE_WITH_WAYNODES; } Include include = cachedRelations.get(relation); if (include != null) { return include; } include = filter(relation); cachedRelations.put(relation, include); List<RelationMember> members = relation.getMembers(); if (members != null) { for (RelationMember rm:members) { OsmElement element = rm.getElement(); if (element != null) { if (element instanceof Way) { Way w = (Way)element; Include includeWay = cachedWays.get(w); if (includeWay == null || (include != Include.DONT && includeWay == Include.DONT)) { // if not originally included overwrite now if (include == Include.INCLUDE_WITH_WAYNODES) { for (Node n:w.getNodes()) { cachedNodes.put(n,include); } } cachedWays.put(w,include); } } else if (element instanceof Node) { Node n = (Node)element; Include includeNode = cachedNodes.get(n); if (includeNode == null || (include != Include.DONT && includeNode == Include.DONT)) { // if not originally included overwrite now cachedNodes.put(n,include); } } else if (element instanceof Relation) { // FIXME } } } } return include; } /** * Tag filter controls */ private transient FloatingActionButton tagFilterButton; private transient ViewGroup parent; private transient RelativeLayout controls; private transient Update update; @Override public void addControls(ViewGroup layout, final Update update) { Log.d(DEBUG_TAG, "adding filter controls"); this.parent = layout; this.update = update; tagFilterButton = (FloatingActionButton)parent.findViewById(R.id.tagFilterButton); final Context context = layout.getContext(); // we weren't already added ... if (tagFilterButton == null) { Preferences prefs = new Preferences(context); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); String buttonPos = layout.getContext().getString(R.string.follow_GPS_left); controls = (RelativeLayout)inflater.inflate(prefs.followGPSbuttonPosition().equals(buttonPos)?R.layout.tagfilter_controls_right:R.layout.tagfilter_controls_left, layout); tagFilterButton = (FloatingActionButton)controls.findViewById(R.id.tagFilterButton); } tagFilterButton.setClickable(true); tagFilterButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View b) { Log.d(DEBUG_TAG,"Button clicked"); TagFilterActivity.start(context, DEFAULT_FILTER); } }); Util.setAlpha(tagFilterButton, Main.FABALPHA); setupControls(false); } private void setupControls(boolean toggle) { enabled = toggle ? !enabled : enabled; update.execute(); } @Override public void removeControls() { if (parent != null && controls != null) { parent.removeView(controls); } } @Override public void hideControls() { if(tagFilterButton != null) { tagFilterButton.setVisibility(View.GONE); } } @Override public void showControls() { if(tagFilterButton != null) { tagFilterButton.setVisibility(View.VISIBLE); } } }