Some groundwork for location matching.

master
Sander Vocke 6 years ago
parent e6d5f8f754
commit 4c20f8f36e
  1. 5
      src/database.js
  2. 25
      src/main.js
  3. 21
      src/polygons.js
  4. 64
      src/queries.js
  5. 48
      src/userquerywidget.js

@ -47,8 +47,13 @@ export function regexp_match(string, regex) {
return string.match(regex) != null;
}
export function is_in_geo_polygon(lat, long, polyid) {
return true;
}
export function add_custom_functions(db) {
db.create_function("REGEXP", regexp_match);
db.create_function("IS_IN_GEO_POLYGON", is_in_geo_polygon);
}
// Digikam stores its tree of tags as individual tags,

@ -12,6 +12,7 @@ import { UserQuery, user_query_from_search_string, maybe_image_query, do_image_q
import { Browser } from './browser.js';
import { UserQueryWidget } from './userquerywidget.js';
import { ResultsView } from './resultsview.js';
import { PolygonStoreProvider } from './polygons.js';
const useStyles = makeStyles(theme => ({
root: {
@ -125,16 +126,18 @@ export function LoadedMainPage(props) {
return (
<>
<Box className={classes.root}>
<Box className={classes.navigator}>
<Box className={classes.margined}>{albums && <Browser albums={allAlbums} tags={allTags} onNewQuery={onNewQuery} />}</Box>
<PolygonStoreProvider>
<Box className={classes.root}>
<Box className={classes.navigator}>
<Box className={classes.margined}>{albums && <Browser albums={allAlbums} tags={allTags} onNewQuery={onNewQuery} />}</Box>
</Box>
<Box className={classes.searchandview}>
<Box className={classes.margined}><SearchBar onSubmit={onSearch} /></Box>
<Box className={classes.margined}><UserQueryWidget userQuery={gallery_user_query} onChange={onNewQuery} /></Box>
<Box className={classes.margined}>{photos && <ResultsView photos={photos ? photos : []} albums={albums ? albums : []} tags={tags ? tags : []} />}</Box>
</Box>
</Box>
<Box className={classes.searchandview}>
<Box className={classes.margined}><SearchBar onSubmit={onSearch} /></Box>
<Box className={classes.margined}><UserQueryWidget userQuery={gallery_user_query} onChange={onNewQuery} /></Box>
<Box className={classes.margined}>{photos && <ResultsView photos={photos ? photos : []} albums={albums ? albums : []} tags={tags ? tags : []} />}</Box>
</Box>
</Box>
</PolygonStoreProvider>
</>
);
}
@ -150,14 +153,14 @@ export function MainPage() {
return (
<ThemeProvider theme={theme}>
<ProvideDB
db_url="https://192.168.1.101/digikam-fatclient-data/digikam4.db"
db_url="https://sandervocke-nas.duckdns.org/digikam-fatclient-data/digikam4.db"
>
{({ db_error, db }) => (
<>
{(db == null && !db_error) && <LoadingPage file={"digikam4.db"} />}
{db_error && <InternalErrorPage message={db_error.message} />}
{db != null && <LoadedMainPage database={db} photos_dir="https://192.168.1.101/digikam-fatclient-data/photos" thumbs_dir="https://192.168.1.101/digikam-fatclient-data/thumbs" />}
{db != null && <LoadedMainPage database={db} photos_dir="https://sandervocke-nas.duckdns.org/digikam-fatclient-data/photos" thumbs_dir="https://sandervocke-nas.duckdns.org/digikam-fatclient-data/thumbs" />}
</>
)}
</ProvideDB>

@ -0,0 +1,21 @@
import React, { createContext, useReducer } from 'react';
const polygonStore = createContext({});
const { Provider } = polygonStore;
const PolygonStoreProvider = ({ children }) => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'add polygon':
const newstate = state;
newstate[action.payload.id] = action.payload.data;
return newstate;
default:
throw new Error();
};
}, {});
return <Provider value={{ state, dispatch }}>{children}</Provider>;
};
export { polygonStore, PolygonStoreProvider }

@ -33,7 +33,7 @@ export function do_image_query(query, database, collection_path, collection_thum
}
resolve(photos);
})
.catch(err => { throw err; });
.catch(err => { throw err; });
});
}
@ -167,9 +167,9 @@ export class TimeFilter extends ResultFilter {
to_sql_where() {
var operator = '';
if(this.type == TimeFilterTypeEnum.AFTER) {
if (this.type == TimeFilterTypeEnum.AFTER) {
operator = '>=';
} else if(this.type == TimeFilterTypeEnum.BEFORE) {
} else if (this.type == TimeFilterTypeEnum.BEFORE) {
operator = '<=';
} else {
throw new Error("Unsupported time filter type.");
@ -303,6 +303,34 @@ export class LogicalOperatorFilter extends ResultFilter {
}
}
export class LocationFilter extends ResultFilter {
// The location filter always compares object locations (points)
// to a location polygon and filters objects outside said polygon.
constructor(rtype, polygon, polygon_store_dispatch) {
super(rtype);
this.polygon = polygon;
this.polygon_store_dispatch = polygon_store_dispatch;
}
to_sql_where() {
// When we turn this into a query, we will use a custom SQL function which compares to a
// polygon. However, we don't want to encode an entire polygon in an SQL query.
// Therefore we make use of polygons stored in Javascript state, which can be read
// by the custom comparison function invoked by SQL.js.
// We need to store our polygon in said state and then return the query to guarantee that
// it will have access to said polygon.
const _ = require('lodash');
const poly = {
id: _.uniqueId('polygon_'),
data: this.polygon
}
this.polygon_store_dispatch({ type: 'add polygon', payload: poly });
return 'IS_IN_GEO_POLYGON(ImagePositions.latitude, ImagePositions.longitude, "' + poly.id + '")';
}
simplify() { return this; }
}
export class UserQuery {
image_filter = new ConstFilter(ResultTypeEnum.IMAGE, false);
album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false);
@ -316,21 +344,20 @@ export function image_query_with_where(maybe_where) {
+ "FROM Images INNER JOIN Albums ON Images.album=Albums.id "
+ "LEFT JOIN ImageTags ON Images.id=ImageTags.imageid "
+ "LEFT JOIN ImageInformation ON Images.id=ImageInformation.imageid "
+ "LEFT JOIN Tags ON ImageTags.tagid=Tags.id " + (maybe_where ? maybe_where : "")
+ "LEFT JOIN ImagePositions ON ImagePositions.imageid=Images.id "
+ "LEFT JOIN Tags ON ImageTags.tagid=Tags.id "
+ (maybe_where ? maybe_where : "")
+ " GROUP BY Images.id;";
}
// This query will return database entries with the fields "id" and "relativePath" for each matching album.
export function album_query_with_where(maybe_where) {
var query = "SELECT Albums.id, Albums.relativePath FROM Albums";
var query = "SELECT Albums.id, Albums.relativePath FROM Albums "
+ "LEFT JOIN Images ON Images.album=Albums.id "
+ "LEFT JOIN ImageTags ON ImageTags.imageid=Images.id "
+ "LEFT JOIN ImagePositions ON ImagePositions.imageid=Images.id "
+ "LEFT JOIN Tags ON Tags.id=ImageTags.tagid";
// If there is tags/images stuff in the where clause, we need to do a join on those tables.
if(maybe_where && (maybe_where.includes("Tags.") || maybe_where.includes("Images."))) {
query = query + " LEFT JOIN Images ON Images.album=Albums.id "
+ " LEFT JOIN ImageTags ON ImageTags.imageid=Images.id "
+ " LEFT JOIN Tags ON Tags.id=ImageTags.tagid";
}
query = query + " " + (maybe_where ? maybe_where : "") + " GROUP BY Albums.id;";
return query;
@ -338,20 +365,17 @@ export function album_query_with_where(maybe_where) {
// This query will return database entries with the fields "id" and "name" for each matching tag.
export function tag_query_with_where(maybe_where) {
var query = "SELECT Tags.id, Tags.name, Tags.fullname FROM Tags LEFT JOIN TagProperties ON Tags.id=TagProperties.tagid";
var query = "SELECT Tags.id, Tags.name, Tags.fullname FROM Tags LEFT JOIN TagProperties ON Tags.id=TagProperties.tagid "
+ "LEFT JOIN ImageTags ON ImageTags.tagid=Tags.id "
+ "LEFT JOIN Images ON Images.id=ImageTags.imageid "
+ "LEFT JOIN ImagePositions ON ImagePositions.imageid=Images.id "
+ "LEFT JOIN Albums ON Albums.id=Images.album";
// Add a clause to the WHERE to hide internal tags.
var exclude_internal = ' (Tags.name="People" OR TagProperties.property IS NULL OR TagProperties.property<>"internalTag")';
var where = maybe_where ?
maybe_where + ' AND' + exclude_internal :
"WHERE " + exclude_internal;
// If there is albums/images stuff in the where clause, we need to do a join on those tables.
if(where.includes("Albums.") || where.includes("Images.")) {
query = query + " LEFT JOIN ImageTags ON ImageTags.tagid=Tags.id "
+ " LEFT JOIN Images ON Images.id=ImageTags.imageid "
+ " LEFT JOIN Albums ON Albums.id=Images.album";
}
query = query + " " + where + " GROUP BY Tags.id;";
return query;

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useContext } from 'react';
import Switch from '@material-ui/core/Switch';
import Box from '@material-ui/core/Box';
@ -24,10 +24,12 @@ import { format } from 'date-fns';
import { makeStyles } from '@material-ui/core/styles';
import { polygonStore } from './polygons.js';
import {
filter_is_const_false, ConstFilter, LogicalOperatorFilter, MatchingFilter,
ResultTypeEnum, LogicalOperatorEnum, MatchTypeEnum, NegationFilter, TimeFilterTypeEnum,
TimeFilter, ImageTypeFilter, ImageTypeEnum
TimeFilter, ImageTypeFilter, ImageTypeEnum, LocationFilter
} from './queries.js'
import { Typography } from '@material-ui/core';
@ -55,6 +57,14 @@ const useStyles = makeStyles(theme => ({
},
}));
export function EditLocationFilterExpression(props) {
const { onChange, filter } = props;
return (
<Typography>TODO</Typography>
);
}
export function EditTimeFilterExpression(props) {
const { onChange, filter } = props;
@ -229,6 +239,8 @@ export function EditFilterExpressionDialog(props) {
const { onClose, startingFilter, open } = props;
const [filter, setFilter] = React.useState(startingFilter);
const polygons_dispatch = useContext(polygonStore).dispatch;
const FilterTypeEnum = {
CONST: 0,
NEGATION: 1,
@ -236,6 +248,7 @@ export function EditFilterExpressionDialog(props) {
LOGICAL: 3,
TIME: 4,
IMAGETYPE: 5,
LOCATION: 6,
};
useEffect(() => {
@ -256,6 +269,8 @@ export function EditFilterExpressionDialog(props) {
setFilter(new TimeFilter(filter.result_type, Date.now(), TimeFilterTypeEnum.AFTER));
} else if (val == FilterTypeEnum.IMAGETYPE) {
setFilter(new ImageTypeFilter(filter.result_type, ImageTypeEnum.PHOTO));
} else if (val == FilterTypeEnum.LOCATION) {
setFilter(new LocationFilter(filter.result_type, [[0.0, 0.0]], polygons_dispatch));
} else {
throw new Error('Unsupported filter type: ' + val);
}
@ -268,6 +283,7 @@ export function EditFilterExpressionDialog(props) {
else if (filter instanceof LogicalOperatorFilter) { return FilterTypeEnum.LOGICAL; }
else if (filter instanceof TimeFilter) { return FilterTypeEnum.TIME; }
else if (filter instanceof ImageTypeFilter) { return FilterTypeEnum.IMAGETYPE; }
else if (filter instanceof LocationFilter) { return FilterTypeEnum.LOCATION; }
else {
throw new Error('Unsupported filter type: ' + filter);
}
@ -293,6 +309,8 @@ export function EditFilterExpressionDialog(props) {
control = <EditTimeFilterExpression {...subprops} />
} else if (filter instanceof ImageTypeFilter) {
control = <EditImageTypeFilterExpression {...subprops} />
} else if (filter instanceof LocationFilter) {
control = <EditLocationFilterExpression {...subprops} />
}
// If this is a "leaf" filter, we will allow changing the filter type in the dialog.
@ -302,7 +320,8 @@ export function EditFilterExpressionDialog(props) {
(filter instanceof ConstFilter) ||
(filter instanceof MatchingFilter) ||
(filter instanceof TimeFilter) ||
(filter instanceof ImageTypeFilter);
(filter instanceof ImageTypeFilter) ||
(filter instanceof LocationFilter);
return (
<Dialog aria-labelledby={id} open={open}>
@ -319,6 +338,7 @@ export function EditFilterExpressionDialog(props) {
<MenuItem value={FilterTypeEnum.MATCHING}>Matching</MenuItem>
<MenuItem value={FilterTypeEnum.TIME}>Date/Time</MenuItem>
<MenuItem value={FilterTypeEnum.IMAGETYPE}>Media type</MenuItem>
<MenuItem value={FilterTypeEnum.LOCATION}>Location</MenuItem>
</Select>
</FormControl>
}
@ -567,9 +587,9 @@ export function ImageTypeFilterExpressionControl(props) {
const { expr, onClick, onChange } = props;
var icon = false;
if(expr.type == ImageTypeEnum.PHOTO) {
if (expr.type == ImageTypeEnum.PHOTO) {
icon = <PhotoIcon />
} else if(expr.type == ImageTypeEnum.VIDEO) {
} else if (expr.type == ImageTypeEnum.VIDEO) {
icon = <VideocamIcon />
} else {
throw new Error("Unsupported image type");
@ -587,6 +607,22 @@ export function ImageTypeFilterExpressionControl(props) {
);
}
export function LocationFilterExpressionControl(props) {
const classes = useStyles();
const { expr, onClick, onChange } = props;
return (
<Button
variant="outlined"
className={classes.filterexpcontrol}
aria-controls="simple-menu" aria-haspopup="true"
onClick={onClick}
>
LOCATION
</Button>
);
}
export function FilterExpressionControl(props) {
const { expr, onChange, isRoot } = props;
const [anchorEl, setAnchorEl] = React.useState(null);
@ -676,6 +712,8 @@ export function FilterExpressionControl(props) {
filter_elem = <TimeFilterExpressionControl {...props} onClick={handleClick} onChange={onChange} />
} else if (expr instanceof ImageTypeFilter) {
filter_elem = <ImageTypeFilterExpressionControl {...props} onClick={handleClick} onChange={onChange} />
} else if (expr instanceof LocationFilter) {
filter_elem = <LocationFilterExpressionControl {...props} onClick={handleClick} onChange={onChange} />
} else {
throw new Error('Unsupported filter expression');
}

Loading…
Cancel
Save