Made a quick 'n dirty tool to simplify continent polygons. Used it to provide quicker continent lookups.

master
Sander Vocke 6 years ago
parent 6ed986bfc3
commit 453069b33b
  1. 1757
      offline/simplify_polygons/concaveman-bundle.js
  2. 1
      offline/simplify_polygons/continent_polygons.geojson_var
  3. 113
      offline/simplify_polygons/index.html
  4. 1
      public/continent_polygons_simplified.geojson
  5. 50
      src/database.js
  6. 84
      src/geolocation.js
  7. 2
      src/queries.js
  8. 2
      src/userquerywidget.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,113 @@
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<h1>Concaveman</h1>
<p>
An interactive demo of <a href="https://github.com/mapbox/concaveman">concaveman</a> - a Javascript module for
fast 2D concave hulls. Use the sliders to visualize the effect of the two parameters, concavity and
lengthThreshold.
</p>
<div>
<label for="concavity">Concavity </label>
<input type="range" oninput="updateConcavity(this.value)" id="concavity" min="0" value="2.0" max="3.2"
step="0.05">
<input type="text" id="concavityText" value="2.0">
<label for="lengthThreshold"> Length Threshold</label>
<input type="range" oninput="updateLengthThreshold(this.value)" id="lengthThreshold" min="0" value="0" max="100"
step="1">
<input type="text" id="lengthThresholdText" value="0.0">
</div>
<br>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="./concaveman-bundle.js"></script>
<script type="text/javascript" language="javascript" src="continent_polygons.geojson_var"></script>
<script>
var width = 2000,
height = 2000,
minx = -200,
miny = -100,
scale = 3;
var concavity = 2.0;
var lengthThreshold = 0.0;
function updateConcavity(val) {
concavity = val;
document.getElementById('concavityText').value = val;
updateConcaveHull();
};
function updateLengthThreshold(val) {
lengthThreshold = val;
document.getElementById('lengthThresholdText').value = val;
updateConcaveHull();
};
function get_all_points(geojson) {
var points = [];
if (geojson.type == "Polygon") {
geojson.coordinates[0].forEach(e => points.push(e));
} else if (geojson.type == "MultiPolygon") {
for (let i = 0; i < geojson.coordinates.length; i++) {
geojson.coordinates[i][0].forEach(e => points.push(e));
}
}
return points;
}
console.log("GEOJson: ", geojson);
var points = geojson.features.map(feature => {
return get_all_points(feature.geometry);
});
console.log(points);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// svg.selectAll('circle')
// .data(points)
// .enter()
// .append('circle')
// .attr("cx", function (d) { return (d[0] - minx) * scale; })
// .attr("cy", function (d) { return (d[1] - miny) * scale; })
// .attr("r", 2);
function updateConcaveHull() {
svg.selectAll('polygon').remove();
svg.selectAll('polygon')
//.data([concaveman(points, concavity, lengthThreshold)])
.data(points.map(partpoints => {
return concaveman(partpoints, concavity, lengthThreshold)
}))
.enter()
.append('polygon')
.attr('points', function (points) {
data = ""
for (var i = points.length; i--;) {
data += [(points[i][0] - minx) * scale, (points[i][1] - miny) * scale].join(',');
data += " ";
}
data += [(points[0][0] - minx) * scale, (points[0][1] - miny) * scale].join(',');
return data
})
.attr('stroke', 'black')
.attr('fill', 'yellow')
.attr('opacity', 0.2)
.attr('stroke-width', 1);
var new_geojson = geojson;
for(let i=0; i<new_geojson.features.length; i++) {
new_geojson.features[i].geometry.type = "MultiPolygon";
new_geojson.features[i].geometry.coordinates = [[
concaveman(points[i], concavity, lengthThreshold)
]];
};
console.log("New GeoJSON: ", new_geojson);
console.log(JSON.stringify(new_geojson));
};
updateConcaveHull();
</script>
</body>

File diff suppressed because one or more lines are too long

@ -1,9 +1,11 @@
import React, { useEffect, useState, useContext } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
initialize_image_index, image_in_area, load_static_geo_polygons initialize_image_index, image_in_area, load_static_geo_polygons
} from './geo_store.js'; } from './geolocation.js';
// Perform a series of sequential queries to an SQL.js database,
// asynchronously, and return the last query result.
export async function sqljs_async_queries(sqljs_object, queries) { export async function sqljs_async_queries(sqljs_object, queries) {
//var t0 = performance.now(); //var t0 = performance.now();
for (let i = 0; i < (queries.length - 1); i++) { for (let i = 0; i < (queries.length - 1); i++) {
@ -57,7 +59,7 @@ export function is_in_geo_polygon_from_store(image_id, lat, long, polygon_hash)
// Digikam stores its tree of tags as individual tags, // Digikam stores its tree of tags as individual tags,
// linked only by their parent ID. This makes searching // linked only by their parent ID. This makes searching
// difficult. Therefore we add a column in the tag table // difficult. Therefore we add a column in the tag table
// which holds the "full tag" (e.g. People/John for the tag John). // which holds the "full tag" (e.g. People/John for the tag John with parent People).
export async function add_full_tag_info(db) { export async function add_full_tag_info(db) {
var res = db.exec("SELECT id, pid, name FROM Tags LEFT JOIN TagProperties ON Tags.id=TagProperties.tagid"); var res = db.exec("SELECT id, pid, name FROM Tags LEFT JOIN TagProperties ON Tags.id=TagProperties.tagid");
if (!Array.isArray(res) || res.length == 0) { if (!Array.isArray(res) || res.length == 0) {
@ -103,23 +105,57 @@ export async function add_full_tag_info(db) {
return db; return db;
} }
function initialize_image_index_from_db(sqljs_db) {
// Get image positions and group them on identical geolocation.
var img_query = "SELECT GROUP_CONCAT(Images.id), ImagePositions.latitudeNumber, ImagePositions.longitudeNumber "
+ "FROM Images "
+ "LEFT JOIN ImagePositions ON ImagePositions.imageid=Images.id "
+ "WHERE ImagePositions.latitudeNumber NOT NULL GROUP BY ImagePositions.latitudeNumber, ImagePositions.longitudeNumber;";
return new Promise((resolve, reject) => {
sqljs_async_queries(sqljs_db, [img_query])
.then(res => {
var points = []; // will contain a list of unique [long, lat] points
var ids_per_point = []; // will contain a list of image ids per point in "points", same indexing
if (res && Array.isArray(res) && res.length > 0) {
var cols = res[0].columns;
var data = res[0].values;
data.forEach(row => {
points.push([parseFloat(row[cols.indexOf("longitudeNumber")]), parseFloat(row[cols.indexOf("latitudeNumber")])]);
ids_per_point.push(row[cols.indexOf("GROUP_CONCAT(Images.id)")].split(',').map(e => parseInt(e)));
});
}
initialize_image_index(points, ids_per_point);
resolve();
});
});
}
// TODO: this looks like a nice re-usable provider, but it also initializes
// global geolocation state. That means it can only be used once in an application.
// Using React's useContext is hard because we also need to access the geo state
// via SQlite. Can we fix this somehow?
export function ProvideDB(props) { export function ProvideDB(props) {
const { children, db_url } = props; const { children, db_url } = props;
const [db, setDb] = useState(null); const [db, setDb] = useState(null);
const [error, setError] = useState(false); const [error, setError] = useState(false);
useEffect(() => { useEffect(() => {
fetch_sqljs_db_from_sqlite(db_url) fetch_sqljs_db_from_sqlite(db_url) // Fetch the database and load it...
.then(db => { .then(db => {
add_full_tag_info(db) add_full_tag_info(db) // ...add additional tags...
.then((newdb) => { .then((newdb) => {
initialize_image_index(newdb) initialize_image_index_from_db(newdb) // ...Build an index for geo searches...
.then(() => { .then(() => {
load_static_geo_polygons() load_static_geo_polygons() // ...Load queriable areas...
.then(() => { .then(() => {
// ... Add custom functions ...
db.create_function("REGEXP", regexp_match); db.create_function("REGEXP", regexp_match);
db.create_function("IS_IN_GEO", is_in_geo_polygon_from_store); db.create_function("IS_IN_GEO", is_in_geo_polygon_from_store);
setError(false); setError(false);
// ...And the database and geo tools are ready to use.
setDb(newdb); setDb(newdb);
}) })
}) })

@ -1,16 +1,17 @@
import KDBush from 'kdbush'; import KDBush from 'kdbush';
import { sqljs_async_queries } from './database.js';
import * as turf from '@turf/turf'; import * as turf from '@turf/turf';
// The geolocation store keeps global state related to
// geolocation. This allows data to be accessed from
// e.g. within SQLite queries.
var g_GeoStore = { var g_GeoStore = {
areas: {}, areas: {}, // A store of GeoJSON (multi-)polygons to query against. Stored as areahash -> area
area_query_results: {}, area_query_results: {}, // A cache of "image within area" query results. Stored as areahash -> results
image_index: {}, image_index: {}, // To store an index data structure on all images.
static_areas: {}, static_areas: {}, // To store GeoJSON areas which can be searched for by the user. Stored as name -> geojson
}; };
export function hash_geo_area(geo_area) { function hash_geo_area(geo_area) {
var hash = require('object-hash'); var hash = require('object-hash');
return hash(geo_area); return hash(geo_area);
} }
@ -31,34 +32,19 @@ export function get_geo_area_from_store(hash) {
throw new Error("Requested non-existent geo area from store."); throw new Error("Requested non-existent geo area from store.");
} }
export function initialize_image_index(database) { // Initialize the index to speed up image-in-area searches.
// points should be a list of unique [ long, lat ] points.
// ids_per_point should be the a list of image IDs for each [ long, lat ] point (same length).
export function initialize_image_index(points, ids_per_point) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var img_query = "SELECT GROUP_CONCAT(Images.id), ImagePositions.latitudeNumber, ImagePositions.longitudeNumber FROM Images " // Store all info we will need later into the image index.
+ "LEFT JOIN ImagePositions ON ImagePositions.imageid=Images.id WHERE ImagePositions.latitudeNumber NOT NULL GROUP BY ImagePositions.latitudeNumber, ImagePositions.longitudeNumber;"; g_GeoStore["image_index"] = {
points: points,
sqljs_async_queries(database, [img_query]) ids_per_point: ids_per_point,
.then(res => { kdbush: new KDBush(points, p => p[0], p => p[1], 16, Float64Array)
var points = []; // will contain a list of unique [long, lat] points };
var ids_per_point = []; // will contain a list of image ids per point in "points", same indexing
if (res && Array.isArray(res) && res.length > 0) { resolve();
var cols = res[0].columns;
var data = res[0].values;
data.forEach(row => {
points.push([parseFloat(row[cols.indexOf("longitudeNumber")]), parseFloat(row[cols.indexOf("latitudeNumber")])]);
ids_per_point.push(row[cols.indexOf("GROUP_CONCAT(Images.id)")].split(',').map(e => parseInt(e)));
});
}
// Store all info we will need later into the image index.
g_GeoStore["image_index"] = {
points: points,
ids_per_point: ids_per_point,
kdbush: new KDBush(points, p => p[0], p => p[1], 16, Float64Array)
};
resolve();
})
.catch(e => { reject(e); });
}); });
} }
@ -147,7 +133,7 @@ export function image_in_area(image_id, area_hash) {
// We want to have our own cache of named // We want to have our own cache of named
export function load_static_geo_polygons() { export function load_static_geo_polygons() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch('/continent_polygons.geojson') fetch('/continent_polygons_simplified.geojson')
.then(res => res.json()) .then(res => res.json())
.then(json => { .then(json => {
// The continents are stored as an array of features. Change it to a // The continents are stored as an array of features. Change it to a
@ -163,17 +149,17 @@ export function load_static_geo_polygons() {
export function query_geometry(query) { export function query_geometry(query) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const query_words = query.toLowerCase().trim().split(/\s+/); const query_words = query.toLowerCase().trim().split(/\s+/);
for(var key in g_GeoStore.static_areas) { for (var key in g_GeoStore.static_areas) {
const static_words = key.toLowerCase().trim().split(/\s+/); const static_words = key.toLowerCase().trim().split(/\s+/);
if(query_words.length == static_words.length) { if (query_words.length == static_words.length) {
var match = true; var match = true;
for(let i=0; i<static_words.length; i++) { for (let i = 0; i < static_words.length; i++) {
if(static_words[i] != query_words[i]) { if (static_words[i] != query_words[i]) {
match = false; match = false;
break; break;
} }
} }
if(match) { if (match) {
// Matched a static geometry in our store. // Matched a static geometry in our store.
resolve({ resolve({
display_name: key, display_name: key,
@ -185,14 +171,14 @@ export function query_geometry(query) {
} }
// No match in our store, query Nominatim instead // No match in our store, query Nominatim instead
fetch("https://nominatim.openstreetmap.org/search?polygon_geojson=1&polygon_threshold=0.001&format=json&limit=5&q=" + query) fetch("https://nominatim.openstreetmap.org/search?polygon_geojson=1&polygon_threshold=0.001&format=json&limit=5&q=" + query)
.then(res => res.json()) .then(res => res.json())
.then(jsonres => { .then(jsonres => {
if (Array.isArray(jsonres) && jsonres.length > 0) { if (Array.isArray(jsonres) && jsonres.length > 0) {
resolve(jsonres[0]); resolve(jsonres[0]);
} }
resolve(false); resolve(false);
return; return;
}) })
.catch(e => reject(e)); .catch(e => reject(e));
}) })
} }

@ -2,7 +2,7 @@
import { create_photo, create_album, create_tag } from './media.js'; import { create_photo, create_album, create_tag } from './media.js';
import { sqljs_async_queries } from './database.js'; import { sqljs_async_queries } from './database.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { add_geo_area_to_store } from './geo_store.js'; import { add_geo_area_to_store } from './geolocation';
export function escape_regex(s) { export function escape_regex(s) {
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');

@ -34,7 +34,7 @@ import {
TimeFilter, ImageTypeFilter, ImageTypeEnum, LocationFilter TimeFilter, ImageTypeFilter, ImageTypeEnum, LocationFilter
} from './queries.js' } from './queries.js'
import { Typography } from '@material-ui/core'; import { Typography } from '@material-ui/core';
import { query_geometry } from './geo_store.js'; import { query_geometry } from './geolocation.js';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: {}, root: {},

Loading…
Cancel
Save