Unify the filter. Still some bugs but it looks better. Location filter looks good.

master
Sander Vocke 6 years ago
parent a098274fa8
commit 28dbc5714c
  1. 17
      src/main.js
  2. 213
      src/queries.js
  3. 140
      src/userquerywidget.js

@ -8,7 +8,7 @@ import { SearchBar } from './searchbar.js';
import { InternalErrorPage } from './error.js';
import { LoadingPage } from './loading.js';
import { ProvideDB } from './database.js';
import { UserQuery, user_query_from_search_string, maybe_image_query, do_image_query, maybe_album_query, do_album_query, maybe_tag_query, do_tag_query, ConstFilter, ResultTypeEnum } from './queries.js';
import { UserQuery, user_query_from_search_string, maybe_image_query, do_image_query, maybe_album_query, do_album_query, maybe_tag_query, do_tag_query, ConstFilter } from './queries.js';
import { Browser } from './browser.js';
import { UserQueryWidget } from './userquerywidget.js';
import { ResultsView } from './resultsview.js';
@ -53,8 +53,7 @@ export function LoadedMainPage(props) {
useEffect(() => {
// Single-fire effect to start retrieving the albums and tags lists.
var blank_user_query = new UserQuery();
blank_user_query.album_filter = new ConstFilter(ResultTypeEnum.ALBUM, true);
blank_user_query.tag_filter = new ConstFilter(ResultTypeEnum.TAG, true);
blank_user_query.filter = new ConstFilter(true);
var sql_album_query = maybe_album_query(blank_user_query);
var sql_tag_query = maybe_tag_query(blank_user_query);
@ -109,13 +108,13 @@ export function LoadedMainPage(props) {
}
function onNewQuery(q) {
var do_update_photos = !_.isEqual(q.image_filter, gallery_user_query.image_filter);
var do_update_albums = !_.isEqual(q.album_filter, gallery_user_query.album_filter);
var do_update_tags = !_.isEqual(q.tag_filter, gallery_user_query.tag_filter);
var do_update = !_.isEqual(q.filter, gallery_user_query.filter);
setGalleryUserQuery(q);
if (do_update_photos) { updatePhotos(q); }
if (do_update_albums) { updateAlbums(q); }
if (do_update_tags) { updateTags(q); }
if (do_update) {
updatePhotos(q);
updateAlbums(q);
updateTags(q);
}
}
function onSearch(q) {

@ -89,16 +89,15 @@ export const MatchTypeEnum = {
MATCH_TAG_NAME_EQUALS: 10, // Match on the local tag name (excluding parents)
}
/*
export const ResultTypeEnum = {
IMAGE: 1,
ALBUM: 2,
TAG: 3,
}
*/
export class ResultFilter {
constructor(type) { this.result_type = type; }
result_type = ResultTypeEnum.IMAGE;
to_sql_where() { return "(1=1)"; }
is_true() { return false; }
@ -108,7 +107,7 @@ export class ResultFilter {
}
export class ConstFilter extends ResultFilter {
constructor(type, val) { super(type); this.constval = val; }
constructor(val) { super(); this.constval = val; }
constval = true; // True lets everything through, false rejects everything
to_sql_where() {
@ -121,12 +120,12 @@ export class ConstFilter extends ResultFilter {
}
export class NegationFilter extends ResultFilter {
constructor(rtype, body) {
super(rtype);
constructor(body) {
super();
this.body = body;
}
body = new ConstFilter(this.return_type, false);
body = new ConstFilter(false);
to_sql_where() {
return "NOT (" + this.body.to_sql_where() + ")";
@ -137,10 +136,10 @@ export class NegationFilter extends ResultFilter {
simplify() {
var f = this.body.simplify();
if (f.is_true()) {
return new ConstFilter(this.result_type, false);
return new ConstFilter(false);
}
if (f.is_false()) {
return new ConstFilter(this.result_type, true);
return new ConstFilter(true);
}
if (f.is_negation()) {
return f.body;
@ -155,8 +154,8 @@ export const TimeFilterTypeEnum = {
}
export class TimeFilter extends ResultFilter {
constructor(rtype, time, type) {
super(rtype);
constructor(time, type) {
super();
this.time = time;
this.type = type;
}
@ -186,8 +185,8 @@ export const ImageTypeEnum = {
}
export class ImageTypeFilter extends ResultFilter {
constructor(rtype, type) {
super(rtype);
constructor(type) {
super();
this.type = type;
}
@ -202,8 +201,8 @@ export class ImageTypeFilter extends ResultFilter {
}
export class MatchingFilter extends ResultFilter {
constructor(rtype, from, mtype) {
super(rtype);
constructor(from, mtype) {
super();
this.match_from = from;
this.match_type = mtype;
}
@ -258,14 +257,16 @@ export const LogicalOperatorEnum = {
}
export class LogicalOperatorFilter extends ResultFilter {
constructor(type, a, b, op) { super(type); this.sub_filter_a = a; this.sub_filter_b = b; this.operator = op; }
sub_filter_a = false;
sub_filter_b = false;
constructor(operands, op) { super(); this.operands = operands; this.operator = op; }
operands = false;
operator = LogicalOperatorEnum.AND;
to_sql_where() {
var where1 = this.sub_filter_a.to_sql_where();
var where2 = this.sub_filter_b.to_sql_where();
var operand_wheres = [];
for (let i = 0; i < this.operands.length; i++) {
operand_wheres.push(this.operands[i].to_sql_where());
}
var operator_str = "";
if (this.operator === LogicalOperatorEnum.AND) {
operator_str = " AND ";
@ -274,30 +275,51 @@ export class LogicalOperatorFilter extends ResultFilter {
} else {
throw new Error('Unsupported logical operator: ' + this.operator);
}
return "(" + where1 + operator_str + where2 + ")";
var retstr = "(" + operand_wheres[0];
for (let i = 1; i < this.operands.length; i++) {
retstr += operator_str + operand_wheres[i];
}
retstr += ")";
return retstr;
}
simplify() {
var a = this.sub_filter_a.simplify();
var b = this.sub_filter_b.simplify();
var simple = [];
for (let i = 0; i < this.operands.length; i++) {
simple.push(this.operands[i].simplify());
}
if (this.operator === LogicalOperatorEnum.OR) {
if (a.is_true() || b.is_true()) {
return new ConstFilter(this.return_type, true);
for (let i = simple.length - 1; i >= 0; i--) {
if (simple[i].is_true()) {
return new ConstFilter(true);
}
if (simple[i].is_false()) {
// Remove this element, because OR FALSE means nothing
simple.splice(i, 1);
}
}
if (a.is_false()) { return b; }
if (b.is_false()) { return a; }
}
if (this.operator === LogicalOperatorEnum.AND) {
if (a.is_false() || b.is_false()) {
return new ConstFilter(this.return_type, false);
for (let i = simple.length - 1; i >= 0; i--) {
if (simple[i].is_false()) {
return new ConstFilter(false);
}
if (simple[i].is_true()) {
// Remove this element, because AND TRUE means nothing
simple.splice(i, 1);
}
}
if (a.is_true()) { return b; }
if (b.is_true()) { return a; }
}
return this;
if(simple.length == 0) {
return null;
} else if(simple.length == 1) {
return simple[0];
}
return new LogicalOperatorFilter(simple, this.operator);
}
}
@ -306,8 +328,8 @@ export class LocationFilter extends ResultFilter {
// to a location and filters objects outside said location.
// Locations are directly stored from Nominatim API responses,
// which have GeoJSON and other metadata.
constructor(rtype, geo_area) {
super(rtype);
constructor(geo_area) {
super();
this.geo_area = geo_area;
}
@ -327,9 +349,7 @@ export class LocationFilter extends ResultFilter {
}
export class UserQuery {
image_filter = new ConstFilter(ResultTypeEnum.IMAGE, false);
album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false);
tag_filter = new ConstFilter(ResultTypeEnum.TAG, false);
filter = new ConstFilter(false);
}
// This query will return database entries with the fields "id", "uniqueHash", "relativePath" (of the album) and "name" for each matching image.
@ -378,24 +398,24 @@ export function tag_query_with_where(maybe_where) {
export function maybe_image_query(user_query, database) {
var where = false;
if (user_query.image_filter) {
where = "WHERE " + user_query.image_filter.to_sql_where();
if (user_query.filter) {
where = "WHERE " + user_query.filter.to_sql_where();
}
return image_query_with_where(where, database);
}
export function maybe_album_query(user_query, database) {
var where = false;
if (user_query.album_filter) {
where = "WHERE " + user_query.album_filter.to_sql_where();
if (user_query.filter) {
where = "WHERE " + user_query.filter.to_sql_where();
}
return album_query_with_where(where, database);
}
export function maybe_tag_query(user_query) {
var where = false;
if (user_query.tag_filter) {
where = "WHERE " + user_query.tag_filter.to_sql_where();
if (user_query.filter) {
where = "WHERE " + user_query.filter.to_sql_where();
}
return tag_query_with_where(where);
}
@ -408,51 +428,31 @@ export function filter_is_const_false(filter) {
return false;
}
function filter_from_text_segment(result_type, segment) {
function filter_from_text_segment(segment) {
var filter = false;
var name_filter;
if (result_type === ResultTypeEnum.IMAGE) {
// Option 1: match on image name
name_filter = new MatchingFilter(
result_type,
segment['text'],
MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL
);
// Option 2: match on album path (TODO: need natural matching)
var album_filter = new MatchingFilter(
result_type,
segment['text'],
MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS_OR_CHILD
);
filter = new LogicalOperatorFilter(result_type, name_filter, album_filter, LogicalOperatorEnum.OR);
if (segment['negated']) {
filter = new NegationFilter(result_type, filter);
}
} else if (result_type === ResultTypeEnum.ALBUM) {
// TODO: We need a natural matcher for album names
name_filter = new MatchingFilter(
result_type,
segment['text'],
MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS_OR_CHILD
);
filter = name_filter;
if (segment['negated']) {
filter = new NegationFilter(result_type, filter);
}
} else if (result_type === ResultTypeEnum.TAG) {
// TODO: We need a natural matcher for tag names
name_filter = new MatchingFilter(
result_type,
segment['text'],
MatchTypeEnum.MATCH_TAG_NAME_EQUALS_OR_CHILD
);
filter = name_filter;
if (segment['negated']) {
filter = new NegationFilter(result_type, filter);
}
// Option 1: match on image name
var image_filter = new MatchingFilter(
segment['text'],
MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL
);
// Option 2: match on album path (TODO: need natural matching)
var album_filter = new MatchingFilter(
segment['text'],
MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD
);
// Option 3: match on tag name
// TODO: We need a natural matcher for tag names
var tag_filter = new MatchingFilter(
segment['text'],
MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD
);
filter = new LogicalOperatorFilter([image_filter, album_filter, tag_filter], LogicalOperatorEnum.OR);
if (segment['negated']) {
filter = new NegationFilter(filter);
}
return filter.simplify();
@ -468,12 +468,8 @@ export function user_query_from_search_string(search_string) {
var r = new UserQuery();
texts.forEach(text => {
r.image_filter = new LogicalOperatorFilter(ResultTypeEnum.IMAGE, r.image_filter,
filter_from_text_segment(ResultTypeEnum.IMAGE, text), LogicalOperatorEnum.AND).simplify();
r.album_filter = new LogicalOperatorFilter(ResultTypeEnum.ALBUM, r.album_filter,
filter_from_text_segment(ResultTypeEnum.ALBUM, text), LogicalOperatorEnum.AND).simplify();
r.tag_filter = new LogicalOperatorFilter(ResultTypeEnum.TAG, r.tag_filter,
filter_from_text_segment(ResultTypeEnum.TAG, text), LogicalOperatorEnum.AND).simplify();
r.filter = new LogicalOperatorFilter([r.filter, filter_from_text_segment(text)],
LogicalOperatorEnum.OR).simplify();
});
return r;
@ -481,21 +477,8 @@ export function user_query_from_search_string(search_string) {
export function user_query_from_browsed_album(album_path) {
var r = new UserQuery();
r.image_filter =
r.filter =
new MatchingFilter(
ResultTypeEnum.IMAGE,
album_path,
MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD,
false).simplify();
r.album_filter =
new MatchingFilter(
ResultTypeEnum.ALBUM,
album_path,
MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD,
false).simplify();
r.tag_filter =
new MatchingFilter(
ResultTypeEnum.TAG,
album_path,
MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD,
false).simplify();
@ -504,24 +487,10 @@ export function user_query_from_browsed_album(album_path) {
export function user_query_from_browsed_tag(tag_path) {
var r = new UserQuery();
r.image_filter =
new MatchingFilter(
ResultTypeEnum.IMAGE,
tag_path,
MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD,
false).simplify();
r.album_filter =
r.filter =
new MatchingFilter(
ResultTypeEnum.ALBUM,
tag_path,
MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD,
false).simplify();
r.tag_filter =
new MatchingFilter(
ResultTypeEnum.TAG,
tag_path,
MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD,
false).simplify();
return r;
}

@ -17,6 +17,7 @@ import Select from '@material-ui/core/Select';
import TextField from '@material-ui/core/TextField';
import ScheduleIcon from '@material-ui/icons/Schedule';
import VideocamIcon from '@material-ui/icons/Videocam';
import LocationOnIcon from '@material-ui/icons/LocationOn';
import { MuiPickersUtilsProvider, DateTimePicker } from "@material-ui/pickers";
import DateFnsUtils from '@date-io/date-fns';
@ -30,7 +31,7 @@ import { SearchBar } from './searchbar.js';
import {
filter_is_const_false, ConstFilter, LogicalOperatorFilter, MatchingFilter,
ResultTypeEnum, LogicalOperatorEnum, MatchTypeEnum, NegationFilter, TimeFilterTypeEnum,
LogicalOperatorEnum, MatchTypeEnum, NegationFilter, TimeFilterTypeEnum,
TimeFilter, ImageTypeFilter, ImageTypeEnum, LocationFilter
} from './queries.js'
import { Typography } from '@material-ui/core';
@ -82,7 +83,6 @@ export function EditLocationFilterExpression(props) {
setGeoLayers([layer]);
// Update the proposed polygon
console.log("Updated proposal:", result);
setProposal(result);
}
@ -94,7 +94,6 @@ export function EditLocationFilterExpression(props) {
// Do a geometry query.
query_geometry(query)
.then(result => {
console.log("Query result:", result);
if (result) {
updateProposal(result);
}
@ -327,15 +326,15 @@ export function EditFilterExpressionDialog(props) {
const handleTypeChange = e => {
var val = e.target.value;
if (val == FilterTypeEnum.CONST) {
setFilter(new ConstFilter(filter.result_type, true));
setFilter(new ConstFilter(true));
} else if (val == FilterTypeEnum.MATCHING) {
setFilter(new MatchingFilter(filter.result_type, "", MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL));
setFilter(new MatchingFilter("", MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL));
} else if (val == FilterTypeEnum.TIME) {
setFilter(new TimeFilter(filter.result_type, Date.now(), TimeFilterTypeEnum.AFTER));
setFilter(new TimeFilter(Date.now(), TimeFilterTypeEnum.AFTER));
} else if (val == FilterTypeEnum.IMAGETYPE) {
setFilter(new ImageTypeFilter(filter.result_type, ImageTypeEnum.PHOTO));
setFilter(new ImageTypeFilter(ImageTypeEnum.PHOTO));
} else if (val == FilterTypeEnum.LOCATION) {
setFilter(new LocationFilter(filter.result_type, [[0.0, 0.0]]));
setFilter(new LocationFilter([[0.0, 0.0]]));
} else {
throw new Error('Unsupported filter type: ' + val);
}
@ -521,24 +520,14 @@ export function LogicalOperatorFilterExpressionControl(props) {
}
var _ = require('lodash');
const handleAChanged = (new_a) => {
if (new_a == null) {
onChange(expr.sub_filter_b);
return;
}
const handleOpChanged = (idx, new_op) => {
var new_me = _.cloneDeep(expr);
new_me.sub_filter_a = new_a;
onChange(new_me);
}
const handleBChanged = (new_b) => {
if (new_b == null) {
onChange(expr.sub_filter_a);
return;
}
var new_me = _.cloneDeep(expr);
new_me.sub_filter_b = new_b;
if (new_op == null) {
new_me.operands.splice(idx, 1);
} else {
new_me.operands[idx] = new_op;
}
onChange(new_me);
}
@ -546,16 +535,19 @@ export function LogicalOperatorFilterExpressionControl(props) {
<>
<Box className={classes.logic_op_outer}>
<Box className={classes.logic_op_sbs}>
<Box>
<Box className={classes.logic_op_subexpr}>
<FilterExpressionControl expr={expr.sub_filter_a} onChange={handleAChanged} mayBeRemoved={true} />
</Box>
</Box>
<Box>
<Box className={classes.logic_op_subexpr}>
<FilterExpressionControl expr={expr.sub_filter_b} onChange={handleBChanged} mayBeRemoved={true} />
</Box>
</Box>
{expr.operands.map((operand, idx) => {
const handleChange = new_op => {
handleOpChanged(idx, new_op);
};
return (<>
<Box>
<Box className={classes.logic_op_subexpr}>
<FilterExpressionControl expr={operand} onChange={handleChange} mayBeRemoved={true} />
</Box>
</Box>
</>);
})}
</Box>
<Button
variant="outlined"
@ -567,7 +559,7 @@ export function LogicalOperatorFilterExpressionControl(props) {
</Button>
</Box>
</>
)
);
}
export function ConstFilterExpressionControl(props) {
@ -677,7 +669,7 @@ export function ImageTypeFilterExpressionControl(props) {
export function LocationFilterExpressionControl(props) {
const classes = useStyles();
const { expr, onClick, onChange } = props;
const { expr, onClick } = props;
return (
<Button
@ -685,8 +677,9 @@ export function LocationFilterExpressionControl(props) {
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
startIcon={<LocationOnIcon />}
>
LOCATION
{expr.geo_area.display_name}
</Button>
);
}
@ -763,7 +756,7 @@ export function FilterExpressionControl(props) {
const handleNegation = () => {
handleCloseMenu();
var new_filter = new NegationFilter(expr.result_type, expr);
var new_filter = new NegationFilter(expr);
onChange(new_filter.simplify());
}
@ -818,18 +811,7 @@ export function FilterExpressionControl(props) {
export function FilterControl(props) {
const classes = useStyles();
const { filter, onChange, resultType, resultTypeString } = props;
const enabled = !filter_is_const_false(filter);
function handleResultToggled() {
if (enabled) {
onChange(new ConstFilter(resultType, false));
}
else {
onChange(new ConstFilter(resultType, true));
}
}
const { filter, onChange } = props;
// The root node may in principle never be removed,
// except if it is a transformation node (then the body becomes the new root).
@ -838,16 +820,6 @@ export function FilterControl(props) {
return (
<>
<Box className={classes.filtercontrol}>
<FormControlLabel
control={
<Switch
checked={enabled}
onChange={handleResultToggled}
color="primary"
/>
}
label={resultTypeString + ':'}
/>
<FilterExpressionControl expr={filter} onChange={onChange} mayBeRemoved={may_be_removed} />
</Box>
</>
@ -856,55 +828,17 @@ export function FilterControl(props) {
export function UserQueryWidget(props) {
const { userQuery, onChange } = props;
const classes = useStyles();
var _ = require('lodash');
function handleImageFilterChange(filter) {
var q = _.cloneDeep(userQuery);
q.image_filter = filter;
onChange(q);
}
function handleAlbumFilterChange(filter) {
var q = _.cloneDeep(userQuery);
q.album_filter = filter;
onChange(q);
}
function handleTagFilterChange(filter) {
function handleFilterChange(filter) {
var q = _.cloneDeep(userQuery);
q.tag_filter = filter;
q.filter = filter;
onChange(q);
}
return (
<>
<Box className={classes.bordered + " " + classes.margined}>
<Box className={classes.margined}>
<FilterControl
filter={userQuery.image_filter}
onChange={handleImageFilterChange}
resultType={ResultTypeEnum.IMAGE}
resultTypeString="Images" />
</Box>
</Box>
<Box className={classes.bordered + " " + classes.margined}>
<Box className={classes.margined}>
<FilterControl
filter={userQuery.album_filter}
onChange={handleAlbumFilterChange}
resultType={ResultTypeEnum.ALBUM}
resultTypeString="Albums" />
</Box>
</Box >
<Box className={classes.bordered + " " + classes.margined}>
<Box className={classes.margined}>
<FilterControl
filter={userQuery.tag_filter}
onChange={handleTagFilterChange}
resultType={ResultTypeEnum.TAG}
resultTypeString="Tags" />
</Box>
</Box >
</>
<FilterControl
filter={userQuery.filter}
onChange={handleFilterChange} />
);
}
Loading…
Cancel
Save