├── .gitignore ├── README.md ├── flex.lua ├── index.html ├── json.lua ├── package-lock.json ├── package.json ├── shapeComparison.c ├── shapeComparisonData.txt ├── src ├── App.vue ├── assets │ ├── Washington_US.jpg │ ├── base.css │ ├── categories.json │ ├── favicon.png │ ├── generalTools.js │ ├── main.css │ ├── queryCreator.js │ └── queryTools.js ├── components │ ├── DrawBar.vue │ ├── InputBar.vue │ ├── InteractiveCanvas.vue │ ├── PropertiesBar.vue │ ├── QueryPage.vue │ ├── RelationsBar.vue │ └── UploadBar.vue └── main.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | tests/*/actual-query.txt 2 | tests/*/actual-results.txt 3 | 4 | *.so 5 | *.osm.pbf 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | lerna-debug.log* 15 | 16 | node_modules 17 | .DS_Store 18 | dist 19 | dist-ssr 20 | coverage 21 | *.local 22 | 23 | /cypress/videos/ 24 | /cypress/screenshots/ 25 | 26 | # Editor directories and files 27 | .vscode/* 28 | .vscode/ 29 | !.vscode/extensions.json 30 | .idea 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OSM Finder 2 | 3 | ## The UI is now accessible from anywhere on Netlify: https://osm-finder.netlify.app/ 4 | ## To see OSM Finder in action, check out the blog: https://medium.com/@xetnus 5 | 6 | ## Description 7 | One of the most prominent tools that assists in geolocating images using OpenStreetMap data is [Overpass Turbo](https://overpass-turbo.eu/). This requires learning the [Overpass Query Language](https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL) and restricts your geolocation abilities to only what's permitted by the query language. 8 | 9 | This tool attempts to make it easier for researchers to find locations with a simple click-and-drag interface. No need to learn a new query language. It also adds capabilities not present in existing tools, like the ability to find locations by leveraging the angles created by line intersections and the ability to search for uniquely shaped features. 10 | 11 | This tool is in beta development. If you encounter any issues or have recommendations on how to improve the application, please feel free to open an [issue](https://github.com/Xetnus/osm-finder/issues). 12 | 13 | ## Usage Instructions 14 | Check out the [wiki](https://github.com/Xetnus/osm-finder/wiki/Getting-Started) for usage instructions and the [OSM Finder Blog](https://medium.com/@xetnus) for more concrete examples of how it can be used in investigations. 15 | 16 | ## Installation 17 | Instructions below are for Linux. OSM Finder has been installed and tested on Ubuntu 22.04.1 LTS and Windows Subsystem for Linux - Ubuntu 22.04.2 LTS edition. Your mileage may vary on other operating systems. 18 | 19 | **I. Install and Configure PostgreSQL Backend** 20 | 21 | Installation instructions have been moved to the [wiki](https://github.com/Xetnus/osm-finder/wiki/Installing-the-Backend-(Database)). 22 | 23 | **II. Install Web Frontend** 24 | 25 | Installation instructions have been moved to the [wiki](https://github.com/Xetnus/osm-finder/wiki/Installing-the-Frontend-(Website)). 26 | 27 | ## Roadmap 28 | ### Alpha 29 | - [x] **Start from scratch.** Because this was created during a hackathon, little emphasis was put on code quality and future maintenance. No standard JavaScript libraries were used and most of the code is inefficient in one way or another. 30 | - [x] **Add support for nodes.** Towers, buildings, and nodes of all types should be supported. 31 | - [x] **Update flex.lua.** Include more node and linestring types. Add "downsampling" capability such that ways can be queried as nodes. 32 | - [x] **Revamp UI.** Give the bottom input bar a more modern and functional appearance. 33 | ### Beta 34 | - [ ] **Improve image loader.** Find a way to reduce aliasing artifacts in images uploaded by user. 35 | - [ ] **Add support for polygons.** Many objects have unique shapes that should be queryable using carefully crafted PostgreSQL queries. 36 | - [x] **Closed polygons:** buildings, structures, etc. 37 | - [ ] **Open polygons:** roads, coastlines, and other types of waterfronts 38 | ### Future 39 | - [ ] **Host a public website and server.** Depending on cost, integrate and host both the frontend (UI) and backend (PostgreSQL) on a public-facing website. 40 | -------------------------------------------------------------------------------- /flex.lua: -------------------------------------------------------------------------------- 1 | -- Shape comparison options 2 | -- 0: disabled 3 | -- 1: enabled (all algorithms) 4 | -- 2: only Hu Moments (https://en.wikipedia.org/wiki/Image_moment#Rotation_invariants) 5 | shape_comparison = 0 6 | srid = 3857 7 | 8 | -- Imports json.lua (a helper file that can read JSON) 9 | json = require("json") 10 | 11 | local function read_file(path) 12 | local open = io.open 13 | local file = open(path, "r") 14 | if not file then return nil end 15 | local content = file:read("*a") 16 | file:close() 17 | return content 18 | end 19 | 20 | local categoriesFile = "src/assets/categories.json" 21 | local fileContent = read_file(categoriesFile); 22 | if fileContent == nil then 23 | print("Error: Please ensure that " .. categoriesFile .. " exists.") 24 | os.exit() 25 | end 26 | 27 | local categories = {} 28 | 29 | for category, v in pairs(json.parse(fileContent)) do 30 | for _, tableValue in ipairs(v) do 31 | local key = category 32 | local value = tableValue 33 | local index = string.find(tableValue, "=") 34 | if index ~= nil then 35 | key = string.sub(tableValue, 1, index - 1) 36 | value = string.sub(tableValue, index + 1) 37 | end 38 | categories[key .. "=" .. value] = category 39 | end 40 | end 41 | 42 | -- A list of keys that provide no geolocation value. These are removed from each element's tags. 43 | local delete_keys = { 44 | -- "mapper" keys 45 | 'attribution', 'comment', 'created_by', 'fixme', 'note', 'note:*', 'odbl', 'odbl:note', 'source', 'source:*', 'source_ref', 46 | 47 | -- "import" keys 48 | 'CLC:*', 'geobase:*', 'canvec:*', 'osak:*', 'kms:*', 'ngbe:*', 'it:fvg:*', 'KSJ2:*', 'yh:*', 'LINZ2OSM:*', 49 | 'linz2osm:*', 'LINZ:*', 'ref:linz:*', 'WroclawGIS:*', 'naptan:*', 'tiger:*', 'gnis:*', 'NHD:*', 'nhd:*', 50 | 'mvdgis:*', 'project:eurosha_2012', 'ref:UrbIS', 'accuracy:meters', 'sub_sea:type', 'waterway:type', 51 | 'statscan:rbuid', 'ref:ruian:addr', 'ref:ruian', 'building:ruian:type', 'dibavod:id', 'uir_adr:ADRESA_KOD', 52 | 'gst:feat_id', 'maaamet:ETAK', 'ref:FR:FANTOIR', '3dshapes:ggmodelk', 'AND_nosr_r', 'OPPDATERIN', 53 | 'addr:city:simc', 'addr:street:sym_ul', 'building:usage:pl', 'building:use:pl', 'teryt:simc', 'raba:id', 54 | 'dcgis:gis_id', 'nycdoitt:bin', 'chicago:building_id', 'lojic:bgnum', 'massgis:way_id', 'lacounty:*', 55 | 'at_bev:addr_date', 56 | 57 | -- misc 58 | 'import', 'import_uuid', 'OBJTYPE', 'SK53_bulk:load', 'mml:class' 59 | } 60 | 61 | local clean_tags = osm2pgsql.make_clean_tags_func(delete_keys) 62 | 63 | local tables = {} 64 | 65 | tables.nodes = osm2pgsql.define_table({ 66 | name = 'nodes', 67 | ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' }, 68 | columns = { 69 | { column = 'tags', type = 'jsonb' }, 70 | { column = 'category', type = 'text' }, 71 | { column = 'subcategory', type = 'text' }, 72 | { column = 'geom', type = 'point', projection = srid, not_null = true }, 73 | }, 74 | -- indexes = { 75 | -- { column = 'osm_id', method = 'hash' }, 76 | -- { column = 'category', method = 'hash' }, 77 | -- { column = 'subcategory', method = 'hash' } 78 | -- }, 79 | }) 80 | 81 | tables.shapes = osm2pgsql.define_table({ 82 | name = 'shapes', 83 | ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' }, 84 | columns = { 85 | { column = 'tags', type = 'jsonb' }, 86 | { column = 'category', type = 'text' }, 87 | { column = 'subcategory', type = 'text' }, 88 | { column = 'geom', type = 'polygon', projection = srid, not_null = true }, 89 | { column = 'hu1', sql_type = 'real' }, 90 | { column = 'hu2', sql_type = 'real' }, 91 | { column = 'hu3', sql_type = 'real' }, 92 | { column = 'hu4', sql_type = 'real' }, 93 | { column = 'hu5', sql_type = 'real' }, 94 | { column = 'hu6', sql_type = 'real' }, 95 | { column = 'hu7', sql_type = 'real' }, 96 | }, 97 | -- indexes = { 98 | -- { column = 'osm_id', method = 'hash' }, 99 | -- { column = 'category', method = 'hash' }, 100 | -- { column = 'subcategory', method = 'hash' } 101 | -- }, 102 | }) 103 | 104 | tables.linestrings = osm2pgsql.define_table({ 105 | name = 'linestrings', 106 | ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' }, 107 | columns = { 108 | { column = 'tags', type = 'jsonb' }, 109 | { column = 'category', type = 'text' }, 110 | { column = 'subcategory', type = 'text' }, 111 | { column = 'geom', type = 'linestring', projection = srid, not_null = true }, 112 | }, 113 | -- indexes = { 114 | -- { column = 'osm_id', method = 'hash' }, 115 | -- { column = 'category', method = 'hash' }, 116 | -- { column = 'subcategory', method = 'hash' } 117 | -- }, 118 | }) 119 | 120 | if shape_comparison ~= 0 then 121 | require('shapeComparison') 122 | end 123 | 124 | local function store_nodes(nodes) 125 | local node_count = 0 126 | for k, v in pairs(nodes) do 127 | node_count = node_count + 1 128 | end 129 | 130 | storeNodesBatch(node_count, nodes) 131 | end 132 | 133 | node_list = {} 134 | node_list_size = 0 135 | 136 | function osm2pgsql.process_node(object) 137 | -- Store a list of each node's latitude and longitude 138 | if shape_comparison ~= 0 then 139 | local longitude, latitude, same1, same2 = object:get_bbox() 140 | 141 | -- Converts longitude and latitude from epsg4326 to epsg3857 142 | local smRadius = 6378136.98 143 | local smRange = smRadius * math.pi * 2.0 144 | local smLonToX = smRange / 360.0 145 | local smRadiansOverDegrees = math.pi / 180.0 146 | local e = 2.7182818284590452353602874713527 147 | 148 | longitude = longitude * smLonToX; 149 | 150 | local y = latitude; 151 | 152 | if (y > 86.0) then 153 | latitude = smRange 154 | elseif (y < -86.0) then 155 | latitude = -smRange 156 | else 157 | y = y * smRadiansOverDegrees 158 | y = math.log(math.tan(y) + (1.0 / math.cos(y)), e) 159 | latitude = y * smRadius 160 | end 161 | 162 | local location = { x = longitude, y = latitude } 163 | node_list[object.id] = location 164 | node_list_size = node_list_size + 1 165 | 166 | if node_list_size >= 500000 then 167 | store_nodes(node_list) 168 | node_list = {} 169 | node_list_size = 0 170 | end 171 | end 172 | 173 | if clean_tags(object.tags) then 174 | return 175 | end 176 | 177 | local category 178 | local subcategory 179 | 180 | for k, v in pairs(object.tags) do 181 | category = categories[k .. "=" .. v] 182 | if category == nil then 183 | category = categories[k .. "=*"] 184 | end 185 | subcategory = v 186 | 187 | if category ~= nil then 188 | tables.nodes:insert({ 189 | tags = object.tags, 190 | category = category, 191 | subcategory = subcategory, 192 | geom = object:as_point() 193 | }) 194 | break 195 | end 196 | end 197 | end 198 | 199 | flag = 0 200 | function osm2pgsql.process_way(object) 201 | -- Stores the remaining nodes that didn't quite make the last batch's count threshold 202 | if shape_comparison ~= 0 and node_list_size > 0 then 203 | store_nodes(node_list) 204 | node_list = {} 205 | node_list_size = 0 206 | end 207 | 208 | if clean_tags(object.tags) then 209 | return 210 | end 211 | 212 | local category 213 | local subcategory 214 | 215 | for k, v in pairs(object.tags) do 216 | category = categories[k .. "=" .. v] 217 | if category == nil then 218 | category = categories[k .. "=*"] 219 | end 220 | subcategory = v 221 | 222 | if category ~= nil then 223 | if object.is_closed then 224 | insertPolygonalNode(object, category, subcategory) 225 | 226 | if shape_comparison == 1 or shape_comparison == 2 then 227 | insertHuMoments(object, category, subcategory) 228 | end 229 | end 230 | 231 | insertLinestring(object, category, subcategory) 232 | break 233 | end 234 | end 235 | end 236 | 237 | 238 | -- Converts a polygon (e.g. way) to a node by using the 239 | -- centroid as the node's point, then inserts it into 240 | -- the nodes table 241 | function insertPolygonalNode(object, category, subcategory) 242 | tables.nodes:insert({ 243 | tags = object.tags, 244 | category = category, 245 | subcategory = subcategory, 246 | geom = object:as_polygon():centroid() 247 | }) 248 | end 249 | 250 | -- Inserts a linestring as-is into the linestrings table 251 | function insertLinestring(object, category, subcategory) 252 | tables.linestrings:insert({ 253 | category = category, 254 | subcategory = subcategory, 255 | tags = object.tags, 256 | geom = object:as_linestring() 257 | }) 258 | end 259 | 260 | ---------------------------------------------------------------- 261 | -- Functions to calculate Hu Moments 262 | -- Based on explanation found here: 263 | -- https://en.wikipedia.org/wiki/Image_moment 264 | ---------------------------------------------------------------- 265 | 266 | function insertHuMoments(object, category, subcategory) 267 | -- if object.id ~= 355536359 then 268 | -- return 269 | -- end 270 | 271 | local count = 0 272 | for _, node_id in ipairs(object.nodes) do 273 | count = count + 1 274 | end 275 | 276 | -- C implementation of Hu Moments is at least 10x faster than Lua implementation 277 | local h1, h2, h3, h4, h5, h6, h7 = calculateHuMoments(count, object.nodes) 278 | 279 | tables.shapes:insert({ 280 | tags = object.tags, 281 | category = category, 282 | subcategory = subcategory, 283 | geom = object:as_polygon(), 284 | hu1 = h1, 285 | hu2 = h2, 286 | hu3 = h3, 287 | hu4 = h4, 288 | hu5 = h5, 289 | hu6 = h6, 290 | hu7 = h7, 291 | }) 292 | end -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OSM Finder 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /json.lua: -------------------------------------------------------------------------------- 1 | -- Thank tylerneylon! https://gist.github.com/tylerneylon/59f4bcf316be525b30ab 2 | 3 | --[[A compact pure-Lua JSON library. 4 | The main functions are: json.stringify, json.parse. 5 | ## json.stringify: 6 | This expects the following to be true of any tables being encoded: 7 | * They only have string or number keys. Number keys must be represented as 8 | strings in json; this is part of the json spec. 9 | * They are not recursive. Such a structure cannot be specified in json. 10 | A Lua table is considered to be an array if and only if its set of keys is a 11 | consecutive sequence of positive integers starting at 1. Arrays are encoded like 12 | so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json 13 | object, encoded like so: `{"key1": 2, "key2": false}`. 14 | Because the Lua nil value cannot be a key, and as a table value is considerd 15 | equivalent to a missing key, there is no way to express the json "null" value in 16 | a Lua table. The only way this will output "null" is if your entire input obj is 17 | nil itself. 18 | An empty Lua table, {}, could be considered either a json object or array - 19 | it's an ambiguous edge case. We choose to treat this as an object as it is the 20 | more general type. 21 | To be clear, none of the above considerations is a limitation of this code. 22 | Rather, it is what we get when we completely observe the json specification for 23 | as arbitrary a Lua object as json is capable of expressing. 24 | ## json.parse: 25 | This function parses json, with the exception that it does not pay attention to 26 | \u-escaped unicode code points in strings. 27 | It is difficult for Lua to return null as a value. In order to prevent the loss 28 | of keys with a null value in a json string, this function uses the one-off 29 | table value json.null (which is just an empty table) to indicate null values. 30 | This way you can check if a value is null with the conditional 31 | `val == json.null`. 32 | If you have control over the data and are using Lua, I would recommend just 33 | avoiding null values in your data to begin with. 34 | --]] 35 | 36 | 37 | local json = {} 38 | 39 | 40 | -- Internal functions. 41 | 42 | local function kind_of(obj) 43 | if type(obj) ~= 'table' then return type(obj) end 44 | local i = 1 45 | for _ in pairs(obj) do 46 | if obj[i] ~= nil then i = i + 1 else return 'table' end 47 | end 48 | if i == 1 then return 'table' else return 'array' end 49 | end 50 | 51 | local function escape_str(s) 52 | local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} 53 | local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} 54 | for i, c in ipairs(in_char) do 55 | s = s:gsub(c, '\\' .. out_char[i]) 56 | end 57 | return s 58 | end 59 | 60 | -- Returns pos, did_find; there are two cases: 61 | -- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. 62 | -- 2. Delimiter not found: pos = pos after leading space; did_find = false. 63 | -- This throws an error if err_if_missing is true and the delim is not found. 64 | local function skip_delim(str, pos, delim, err_if_missing) 65 | pos = pos + #str:match('^%s*', pos) 66 | if str:sub(pos, pos) ~= delim then 67 | if err_if_missing then 68 | error('Expected ' .. delim .. ' near position ' .. pos) 69 | end 70 | return pos, false 71 | end 72 | return pos + 1, true 73 | end 74 | 75 | -- Expects the given pos to be the first character after the opening quote. 76 | -- Returns val, pos; the returned pos is after the closing quote character. 77 | local function parse_str_val(str, pos, val) 78 | val = val or '' 79 | local early_end_error = 'End of input found while parsing string.' 80 | if pos > #str then error(early_end_error) end 81 | local c = str:sub(pos, pos) 82 | if c == '"' then return val, pos + 1 end 83 | if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end 84 | -- We must have a \ character. 85 | local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} 86 | local nextc = str:sub(pos + 1, pos + 1) 87 | if not nextc then error(early_end_error) end 88 | return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) 89 | end 90 | 91 | -- Returns val, pos; the returned pos is after the number's final character. 92 | local function parse_num_val(str, pos) 93 | local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) 94 | local val = tonumber(num_str) 95 | if not val then error('Error parsing number at position ' .. pos .. '.') end 96 | return val, pos + #num_str 97 | end 98 | 99 | 100 | -- Public values and functions. 101 | 102 | function json.stringify(obj, as_key) 103 | local s = {} -- We'll build the string as an array of strings to be concatenated. 104 | local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. 105 | if kind == 'array' then 106 | if as_key then error('Can\'t encode array as key.') end 107 | s[#s + 1] = '[' 108 | for i, val in ipairs(obj) do 109 | if i > 1 then s[#s + 1] = ', ' end 110 | s[#s + 1] = json.stringify(val) 111 | end 112 | s[#s + 1] = ']' 113 | elseif kind == 'table' then 114 | if as_key then error('Can\'t encode table as key.') end 115 | s[#s + 1] = '{' 116 | for k, v in pairs(obj) do 117 | if #s > 1 then s[#s + 1] = ', ' end 118 | s[#s + 1] = json.stringify(k, true) 119 | s[#s + 1] = ':' 120 | s[#s + 1] = json.stringify(v) 121 | end 122 | s[#s + 1] = '}' 123 | elseif kind == 'string' then 124 | return '"' .. escape_str(obj) .. '"' 125 | elseif kind == 'number' then 126 | if as_key then return '"' .. tostring(obj) .. '"' end 127 | return tostring(obj) 128 | elseif kind == 'boolean' then 129 | return tostring(obj) 130 | elseif kind == 'nil' then 131 | return 'null' 132 | else 133 | error('Unjsonifiable type: ' .. kind .. '.') 134 | end 135 | return table.concat(s) 136 | end 137 | 138 | json.null = {} -- This is a one-off table to represent the null value. 139 | 140 | function json.parse(str, pos, end_delim) 141 | pos = pos or 1 142 | if pos > #str then error('Reached unexpected end of input.') end 143 | local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. 144 | local first = str:sub(pos, pos) 145 | if first == '{' then -- Parse an object. 146 | local obj, key, delim_found = {}, true, true 147 | pos = pos + 1 148 | while true do 149 | key, pos = json.parse(str, pos, '}') 150 | if key == nil then return obj, pos end 151 | if not delim_found then error('Comma missing between object items.') end 152 | pos = skip_delim(str, pos, ':', true) -- true -> error if missing. 153 | obj[key], pos = json.parse(str, pos) 154 | pos, delim_found = skip_delim(str, pos, ',') 155 | end 156 | elseif first == '[' then -- Parse an array. 157 | local arr, val, delim_found = {}, true, true 158 | pos = pos + 1 159 | while true do 160 | val, pos = json.parse(str, pos, ']') 161 | if val == nil then return arr, pos end 162 | if not delim_found then error('Comma missing between array items.') end 163 | arr[#arr + 1] = val 164 | pos, delim_found = skip_delim(str, pos, ',') 165 | end 166 | elseif first == '"' then -- Parse a string. 167 | return parse_str_val(str, pos + 1) 168 | elseif first == '-' or first:match('%d') then -- Parse a number. 169 | return parse_num_val(str, pos) 170 | elseif first == end_delim then -- End of an object or array. 171 | return nil, pos + 1 172 | else -- Parse true, false, or null. 173 | local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} 174 | for lit_str, lit_val in pairs(literals) do 175 | local lit_end = pos + #lit_str - 1 176 | if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end 177 | end 178 | local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) 179 | error('Invalid json syntax starting at ' .. pos_info_str) 180 | end 181 | end 182 | 183 | return json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osm-finder", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview --port 4173", 8 | "test": "vitest" 9 | }, 10 | "dependencies": { 11 | "@quasar/extras": "^1.15.9", 12 | "konva": "^8.3.13", 13 | "polygon-clipping": "^0.15.3", 14 | "vue": "^3.2.38", 15 | "vue-konva": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@quasar/vite-plugin": "^1.3.0", 19 | "@vitejs/plugin-vue": "^3.0.3", 20 | "vite": "^3.0.9", 21 | "vitest": "^0.24.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shapeComparison.c: -------------------------------------------------------------------------------- 1 | #define _FILE_OFFSET_BITS 64 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define max(a,b) \ 12 | ({ __typeof__ (a) _a = (a); \ 13 | __typeof__ (b) _b = (b); \ 14 | _a > _b ? _a : _b; }) 15 | 16 | #define min(a,b) \ 17 | ({ __typeof__ (a) _a = (a); \ 18 | __typeof__ (b) _b = (b); \ 19 | _a < _b ? _a : _b; }) 20 | 21 | const int MAX_X = 29; 22 | const int MAX_Y = 29; 23 | const int LINE_WIDTH = 50; 24 | const char* DATA_FILE_NAME = "shapeComparisonData.txt"; 25 | 26 | int totalNodeCount = 0; 27 | 28 | /* 29 | ****************************************************************************** 30 | * Merge Sort Implementation 31 | ****************************************************************************** 32 | */ 33 | 34 | void merge(char** arr, int left, int mid, int right) { 35 | int i, j, k; 36 | int n1 = mid - left + 1; 37 | int n2 = right - mid; 38 | 39 | char** leftArr = (char**)malloc(n1 * sizeof(char*)); 40 | char** rightArr = (char**)malloc(n2 * sizeof(char*)); 41 | 42 | for (i = 0; i < n1; i++) 43 | leftArr[i] = arr[left + i]; 44 | for (j = 0; j < n2; j++) 45 | rightArr[j] = arr[mid + 1 + j]; 46 | 47 | i = 0; 48 | j = 0; 49 | k = left; 50 | 51 | char *leftCopy = calloc(LINE_WIDTH, sizeof(char)); 52 | char *rightCopy = calloc(LINE_WIDTH, sizeof(char)); 53 | 54 | while (i < n1 && j < n2) { 55 | snprintf(leftCopy, LINE_WIDTH, "%s", leftArr[i]); 56 | long int leftId = strtol(strtok(leftCopy, " "), NULL, 10); 57 | snprintf(rightCopy, LINE_WIDTH, "%s", rightArr[j]); 58 | long int rightId = strtol(strtok(rightCopy, " "), NULL, 10); 59 | 60 | if (leftId <= rightId) { 61 | arr[k] = leftArr[i]; 62 | i++; 63 | } else { 64 | arr[k] = rightArr[j]; 65 | j++; 66 | } 67 | k++; 68 | } 69 | 70 | free(leftCopy); 71 | free(rightCopy); 72 | 73 | while (i < n1) { 74 | arr[k] = leftArr[i]; 75 | i++; 76 | k++; 77 | } 78 | while (j < n2) { 79 | arr[k] = rightArr[j]; 80 | j++; 81 | k++; 82 | } 83 | 84 | free(leftArr); 85 | free(rightArr); 86 | } 87 | 88 | void mergeSort(char** arr, int left, int right) { 89 | if (left < right) { 90 | int mid = left + (right - left) / 2; 91 | 92 | mergeSort(arr, left, mid); 93 | mergeSort(arr, mid + 1, right); 94 | 95 | merge(arr, left, mid, right); 96 | } 97 | } 98 | 99 | // Reads a line at the given index from the given file and stores it in *line. 100 | static int readLine(char *line, int index, FILE *dataFile) { 101 | int status = fseeko(dataFile, (off_t) index * (LINE_WIDTH - 1), SEEK_SET); 102 | if (status != 0) { 103 | printf("Error seeking data file.\n"); 104 | return -1; 105 | } 106 | status = fread(line, LINE_WIDTH - 1, 1, dataFile); 107 | if (status < 1) { 108 | printf("Error reading data file.\n"); 109 | return -1; 110 | } 111 | 112 | return 0; 113 | } 114 | 115 | /* 116 | ****************************************************************************** 117 | * Binary Search Implementation 118 | ****************************************************************************** 119 | */ 120 | 121 | int binarySearchFile(FILE *dataFile, int l, int r, long int x) 122 | { 123 | char line[LINE_WIDTH]; 124 | 125 | while (l <= r) { 126 | int m = l + (r - l) / 2; 127 | 128 | int status = readLine(line, m, dataFile); 129 | if (status != 0) { 130 | printf("Could not locate %ld due to bad read.\n", x); 131 | return -1; 132 | } 133 | 134 | long int id = strtol(strtok(line, " "), NULL, 10); 135 | 136 | if (id == x) { 137 | return m; 138 | } else if (id < x) { 139 | l = m + 1; 140 | } else { 141 | r = m - 1; 142 | } 143 | } 144 | 145 | // If we reach here, then element was not present 146 | return -1; 147 | } 148 | 149 | /** 150 | * Stores a batch of nodes in the data file, in ascending order by the node ID. 151 | * Params: 152 | * 1. number of nodes being passed 153 | * 2. table of nodes 154 | */ 155 | static int l_storeNodesBatch(lua_State *L) { 156 | if (!lua_isnumber(L, 1) || !lua_istable(L, 2)) { 157 | printf("Incorrect arguments provided.\n"); 158 | return 0; 159 | } 160 | 161 | int nodeCount = lua_tointeger(L, 1); 162 | char *nodes[nodeCount]; 163 | int i = 0; 164 | 165 | lua_pushnil(L); 166 | 167 | // Read node table into nodes array 168 | while (lua_next(L, -2)) { 169 | lua_pushvalue(L, -2); 170 | const char *key = lua_tostring(L, -1); 171 | lua_getfield(L, -2, "x"); 172 | lua_getfield(L, -3, "y"); 173 | const char *x = lua_tostring(L, -2); 174 | const char *y = lua_tostring(L, -1); 175 | lua_pop(L, 4); 176 | 177 | size_t realLength = strlen(key) + strlen(x) + strlen(y) + 4; 178 | int paddingLength = LINE_WIDTH - (int) realLength; 179 | char *buf = calloc(LINE_WIDTH, sizeof(char)); 180 | // Format: " [paddingLength number of spaces]" 181 | int s = snprintf(buf, LINE_WIDTH, "%s %s %s%*s\n", key, x, y, -paddingLength, ""); 182 | if (s > LINE_WIDTH || s < 0) { 183 | free(buf); 184 | printf("Improper buffer assignment.\n"); 185 | return 0; 186 | } 187 | nodes[i] = buf; 188 | i++; 189 | } 190 | 191 | // Sorts the new batch of nodes by ID 192 | mergeSort(nodes, 0, nodeCount - 1); 193 | 194 | // Ensures that the data file exists 195 | if (totalNodeCount == 0) { 196 | FILE *dataFile = fopen(DATA_FILE_NAME, "w"); 197 | if (dataFile != NULL) { 198 | fclose(dataFile); 199 | } 200 | } 201 | 202 | // Temporary file for pseudo "in-place" sorting. 203 | // We loop through the lines in the permanent data file, copying each line 204 | // into the temporary file, and inserting any of the new nodes into their 205 | // appropriate sorted locations. Then renames the temp file to be the perm file. 206 | char tempDataFileName[strlen(DATA_FILE_NAME) + 2]; 207 | snprintf(tempDataFileName, sizeof(tempDataFileName), ".%s", DATA_FILE_NAME); 208 | 209 | FILE *dataFile = fopen(DATA_FILE_NAME, "r"); 210 | FILE *tempDataFile = fopen(tempDataFileName, "w"); 211 | if (dataFile == NULL || tempDataFile == NULL) { 212 | printf("Error opening data or temp file.\n"); 213 | return 0; 214 | } 215 | 216 | int position = 0; 217 | char *lineCopy = calloc(LINE_WIDTH, sizeof(char)); 218 | char *nodeCopy = calloc(LINE_WIDTH, sizeof(char)); 219 | 220 | for (int i = 0; i < nodeCount; i++) { 221 | snprintf(nodeCopy, LINE_WIDTH, "%s", nodes[i]); 222 | long int newId = strtol(strtok(nodes[i], " "), NULL, 10); 223 | 224 | // Goes line by line through the data file, copying each line into the temp file. 225 | // Inserts nodes from the nodes array as appropriate to ensure correct ordering. 226 | int flag = 0; 227 | while (!flag && position < totalNodeCount) { 228 | char line[LINE_WIDTH]; 229 | int status = readLine(line, position, dataFile); 230 | if (status != 0) { 231 | printf("Skipping line at position %d due to bad read.\n", position); 232 | position++; 233 | continue; 234 | } 235 | 236 | snprintf(lineCopy, LINE_WIDTH, "%s", line); 237 | long int lineId = strtol(strtok(line, " "), NULL, 10); 238 | 239 | if (lineId > newId) { 240 | flag = 1; 241 | } else { 242 | fputs(lineCopy, tempDataFile); 243 | position++; 244 | } 245 | } 246 | fputs(nodeCopy, tempDataFile); 247 | } 248 | 249 | free(lineCopy); 250 | free(nodeCopy); 251 | 252 | for (int i = 0; i < nodeCount; i++) { 253 | free(nodes[i]); 254 | } 255 | 256 | // Even though all of the nodes in the nodes array have been added to the temp file, there 257 | // may still be lines from the permanent data file that haven't yet been copied. 258 | // Loop until all lines are copied. 259 | for (; position < totalNodeCount; position++) { 260 | char line[LINE_WIDTH]; 261 | int status = readLine(line, position, dataFile); 262 | if (status != 0) { 263 | printf("Skipping final line at position %d due to bad read.\n", position); 264 | } else { 265 | fputs(line, tempDataFile); 266 | } 267 | } 268 | 269 | totalNodeCount += nodeCount; 270 | 271 | fclose(dataFile); 272 | fclose(tempDataFile); 273 | 274 | remove(DATA_FILE_NAME); 275 | int status = rename(tempDataFileName, DATA_FILE_NAME); 276 | if (status != 0) { 277 | printf("Error renaming data file.\n"); 278 | return 0; 279 | } 280 | 281 | return 0; 282 | } 283 | 284 | /* 285 | ****************************************************************************** 286 | * Hu Moment Implementation 287 | ****************************************************************************** 288 | */ 289 | 290 | static double dist2(double v[], double w[]) { 291 | return pow(v[0] - w[0], 2) + pow(v[1] - w[1], 2); 292 | } 293 | 294 | static double distToSegmentSquared(double p[], double v[], double w[]) { 295 | double l2 = dist2(v, w); 296 | if (l2 == 0) { 297 | return dist2(p, v); 298 | } 299 | 300 | double t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2; 301 | t = max(0, min(1, t)); 302 | 303 | double new_w[] = { (v[0] + t * (w[0] - v[0])), (v[1] + t * (w[1] - v[1])) }; 304 | return dist2(p, new_w); 305 | } 306 | 307 | // Calculates shortest distance between point and a line segment 308 | // p: point whose distance to line segment will be measured 309 | // v: point at one end of line segment 310 | // w: point at other end of line segment 311 | // Translated into C from this answer: 312 | // https://stackoverflow.com/a/1501725/1941353 313 | static double distToSegment(double p[], double v[], double w[]) { 314 | return sqrt(distToSegmentSquared(p, v, w)); 315 | } 316 | 317 | // Determines binary value at given coordinates 318 | static int calculateI(int numNodes, double nodes[][2], int x, int y) { 319 | double p[] = {(double) x, (double) y}; 320 | 321 | for (int i = 0; i < numNodes - 1; i++) { 322 | double segmentP1[] = {nodes[i][0], nodes[i][1]}; 323 | double segmentP2[] = {nodes[i + 1][0], nodes[i + 1][1]}; 324 | 325 | if (distToSegment(p, segmentP1, segmentP2) <= 0.5) { 326 | return 1; 327 | } 328 | } 329 | 330 | return 0; 331 | } 332 | 333 | static int calculateM(int numNodes, double nodes[][2], int p, int q) { 334 | int m = 0; 335 | 336 | for (int x = 0; x <= MAX_X; x++) { 337 | for (int y = 0; y <= MAX_Y; y++) { 338 | m += (pow(x, p) * pow(y, q) * calculateI(numNodes, nodes, x, y)); 339 | } 340 | } 341 | 342 | return m; 343 | } 344 | 345 | // Greek letter mu 346 | static double calculateMu(int numNodes, double nodes[][2], double centroidX, double centroidY, int p, int q) { 347 | double mu = 0; 348 | 349 | for (int x = 0; x <= MAX_X; x++) { 350 | for (int y = 0; y <= MAX_Y; y++) { 351 | mu += (pow((x - centroidX), p) * pow((y - centroidY), q) * calculateI(numNodes, nodes, x, y)); 352 | } 353 | } 354 | 355 | return mu; 356 | } 357 | 358 | // Greek letter eta 359 | static double calculateEta(int numNodes, double nodes[][2], double centroidX, double centroidY, double muDenominator, int p, int q) { 360 | double denominator = pow(muDenominator, (double) (1.0 + (p + q) / 2.0)); 361 | double numerator = calculateMu(numNodes, nodes, centroidX, centroidY, p, q); 362 | return (numerator / denominator); 363 | } 364 | 365 | 366 | // Function to calculate Hu Moments based on explanation found here: 367 | // https://en.wikipedia.org/wiki/Image_moment 368 | // Liberties were taken to optimize the algorithm for C 369 | static int l_calculateHuMoments(lua_State *L) { 370 | if (!lua_isnumber(L, 1) || !lua_istable(L, 2)) { 371 | printf("Incorrect arguments provided.\n"); 372 | return 0; 373 | } 374 | 375 | int count = lua_tonumber(L, 1); 376 | lua_pushnil(L); 377 | 378 | long int node_ids[count]; 379 | int i = 0; 380 | while (lua_next(L, -2)) { 381 | node_ids[i] = (long int) lua_tonumber(L, -1); 382 | i++; 383 | lua_pop(L, 1); 384 | } 385 | lua_pop(L, 1); 386 | 387 | FILE *dataFile; 388 | char line[LINE_WIDTH]; 389 | size_t len = 0; 390 | ssize_t read; 391 | 392 | dataFile = fopen(DATA_FILE_NAME, "r"); 393 | if (dataFile == NULL) { 394 | printf("Error opening data file while calculating Hu Moments.\n"); 395 | return 0; 396 | } 397 | 398 | double nodes[count][2]; 399 | for (int i = 0; i < count; i++) { 400 | int index = binarySearchFile(dataFile, 0, totalNodeCount - 1, node_ids[i]); 401 | 402 | if (index > 0) { 403 | char line[LINE_WIDTH]; 404 | int status = readLine(line, index, dataFile); 405 | if (status != 0) { 406 | printf("Skipping node %ld due to bad read.\n", node_ids[i]); 407 | continue; 408 | } 409 | strtok(line, " "); // Ignores the ID 410 | nodes[i][0] = strtof(strtok(NULL, " "), NULL); 411 | nodes[i][1] = strtof(strtok(NULL, " "), NULL); 412 | } 413 | } 414 | 415 | fclose(dataFile); 416 | 417 | double maxCoords[] = {nodes[0][0], nodes[0][1]}; 418 | double minCoords[] = {nodes[0][0], nodes[0][1]}; 419 | for(int i = 0; i < count; i++) { 420 | if (nodes[i][0] > maxCoords[0]) { 421 | maxCoords[0] = nodes[i][0]; 422 | } else if (nodes[i][0] < minCoords[0]) { 423 | minCoords[0] = nodes[i][0]; 424 | } 425 | 426 | if (nodes[i][1] > maxCoords[1]) { 427 | maxCoords[1] = nodes[i][1]; 428 | } else if (nodes[i][1] < minCoords[1]) { 429 | minCoords[1] = nodes[i][1]; 430 | } 431 | } 432 | 433 | double xRange = maxCoords[0] - minCoords[0]; 434 | double yRange = maxCoords[1] - minCoords[1]; 435 | 436 | double xRatio = MAX_X / xRange; 437 | double yRatio = MAX_Y / yRange; 438 | double scale = min(xRatio, yRatio); 439 | 440 | for(int i = 0; i < count; i++) { 441 | nodes[i][0] -= minCoords[0]; 442 | nodes[i][1] -= minCoords[1]; 443 | 444 | nodes[i][0] *= scale; 445 | nodes[i][1] *= scale; 446 | } 447 | 448 | double h1 = 0, h2 = 0, h3 = 0, h4 = 0, h5 = 0, h6 = 0, h7 = 0; 449 | const double mDenominator = calculateM(count, nodes, 0, 0); 450 | 451 | if (mDenominator != 0) { // mDenominator = 0 if and only if grid is completely empty 452 | const double centroidX = calculateM(count, nodes, 1, 0) / mDenominator; 453 | const double centroidY = calculateM(count, nodes, 0, 1) / mDenominator; 454 | 455 | const double muDenominator = mDenominator; 456 | 457 | double eta20 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 2, 0); 458 | double eta02 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 0, 2); 459 | double eta11 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 1, 1); 460 | double eta30 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 3, 0); 461 | double eta12 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 1, 2); 462 | double eta03 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 0, 3); 463 | double eta21 = calculateEta(count, nodes, centroidX, centroidY, muDenominator, 2, 1); 464 | 465 | h1 = eta20 + eta02; 466 | 467 | h2 = pow( 468 | (eta20 - eta02) 469 | , 2 470 | ) + 471 | 4 * pow( 472 | eta11 473 | , 2 474 | ); 475 | 476 | h3 = pow( 477 | (eta30 - 3 * eta12) 478 | , 2 479 | ) + 480 | pow( 481 | (3 * eta21 - eta03) 482 | , 2 483 | ); 484 | 485 | h4 = pow( 486 | (eta30 + eta12) 487 | , 2 488 | ) + 489 | pow( 490 | (eta21 + eta03) 491 | , 2 492 | ); 493 | 494 | h5 = (eta30 - 3 * eta12) * 495 | (eta30 + eta12) * 496 | ( 497 | pow( 498 | (eta30 + eta12) 499 | , 2 500 | ) - 501 | 3 * pow( 502 | (eta21 + eta03) 503 | , 2 504 | ) 505 | ) + 506 | (3 * eta21 - eta03) * 507 | (eta21 + eta03) * 508 | ( 509 | 3 * pow( 510 | (eta30 + eta12) 511 | , 2 512 | ) - 513 | pow( 514 | (eta21 + eta03) 515 | , 2 516 | ) 517 | ); 518 | 519 | h6 = (eta20 - eta02) * 520 | ( 521 | pow( 522 | (eta30 + eta12) 523 | , 2 524 | ) - 525 | pow( 526 | (eta21 + eta03) 527 | , 2 528 | ) 529 | ) + 530 | 4 * eta11 * 531 | (eta30 + eta12) * 532 | (eta21 + eta03); 533 | 534 | h7 = (3 * eta21 - eta03) * 535 | (eta30 + eta12) * 536 | ( 537 | pow( 538 | (eta30 + eta12) 539 | , 2 540 | ) - 541 | 3 * pow( 542 | (eta21 + eta03) 543 | , 2 544 | ) 545 | ) - 546 | (eta30 - 3 * eta12) * 547 | (eta21 + eta03) * 548 | ( 549 | 3 * pow( 550 | (eta30 + eta12) 551 | , 2 552 | ) - 553 | pow( 554 | (eta21 + eta03) 555 | , 2 556 | ) 557 | ); 558 | } 559 | 560 | lua_pushnumber(L, h1); 561 | lua_pushnumber(L, h2); 562 | lua_pushnumber(L, h3); 563 | lua_pushnumber(L, h4); 564 | lua_pushnumber(L, h5); 565 | lua_pushnumber(L, h6); 566 | lua_pushnumber(L, h7); 567 | return 7; /* number of results */ 568 | } 569 | 570 | int luaopen_shapeComparison(lua_State *L) { 571 | lua_register( 572 | L, /* Lua state variable */ 573 | "calculateHuMoments", /* func name as known in Lua */ 574 | l_calculateHuMoments /* func name in this file */ 575 | ); 576 | lua_register( 577 | L, 578 | "storeNodesBatch", 579 | l_storeNodesBatch 580 | ); 581 | return 0; 582 | } -------------------------------------------------------------------------------- /shapeComparisonData.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetnus/osm-finder/14377594cf732c214280db9b0e46615805ab204a/shapeComparisonData.txt -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 112 | 113 | 157 | 158 | 184 | -------------------------------------------------------------------------------- /src/assets/Washington_US.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetnus/osm-finder/14377594cf732c214280db9b0e46615805ab204a/src/assets/Washington_US.jpg -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | margin: 0; 6 | position: relative; 7 | font-weight: normal; 8 | } 9 | 10 | html { 11 | height: 100%; 12 | } 13 | 14 | body { 15 | height: 100%; 16 | background-color: #333; 17 | color: #c9d6df; 18 | } 19 | 20 | .input-bar-flex { 21 | display: flex; 22 | gap: 20px; 23 | flex-direction: row; 24 | flex-wrap: wrap; 25 | justify-content: center; 26 | } 27 | 28 | .input-bar-flex:first-child { 29 | padding-bottom: 10px; 30 | } 31 | 32 | .input-title { 33 | font-weight: bold; 34 | font-size: 20px; 35 | text-align: center; 36 | margin-bottom: 0.4em; 37 | } 38 | 39 | /* Removes the arrows on input[type=number] elements */ 40 | input::-webkit-outer-spin-button, 41 | input::-webkit-inner-spin-button { 42 | -webkit-appearance: none; 43 | margin: 0; 44 | } 45 | 46 | input[type=number] { 47 | -moz-appearance:textfield; /* Firefox */ 48 | } -------------------------------------------------------------------------------- /src/assets/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "building": [ 3 | "yes", 4 | "house", 5 | "residential", 6 | "garage", 7 | "detached", 8 | "apartments", 9 | "shed", 10 | "hut", 11 | "industrial", 12 | "farm_auxiliary", 13 | "roof", 14 | "terrace", 15 | "commercial", 16 | "semidetached_house", 17 | "school", 18 | "retail", 19 | "garages", 20 | "construction", 21 | "barn", 22 | "cabin", 23 | "outbuilding", 24 | "greenhouse", 25 | "service", 26 | "church", 27 | "farm", 28 | "static_caravan", 29 | "warehouse", 30 | "bungalow", 31 | "office", 32 | "civic", 33 | "ruins", 34 | "carport", 35 | "allotment_house", 36 | "university", 37 | "public", 38 | "hospital", 39 | "boathouse", 40 | "hotel", 41 | "manufacture", 42 | "kindergarten", 43 | "chapel", 44 | "ger", 45 | "mosque", 46 | "hangar", 47 | "storage_tank", 48 | "collapsed", 49 | "dormitory", 50 | "bunker", 51 | "train_station", 52 | "college" 53 | ], 54 | "water_body": [ 55 | "natural=water", 56 | "natural=coastline", 57 | "leisure=swimming_pool" 58 | ], 59 | "railway": [ 60 | "rail", 61 | "level_crossing", 62 | "switch", 63 | "abandoned", 64 | "signal", 65 | "buffer_stop", 66 | "crossing", 67 | "platform", 68 | "disused", 69 | "milestone", 70 | "stop", 71 | "tram", 72 | "station", 73 | "subway", 74 | "tram_level_crossing", 75 | "narrow_gauge", 76 | "razed", 77 | "tram_stop", 78 | "tram_crossing", 79 | "subway_entrance", 80 | "halt", 81 | "light_rail", 82 | "construction", 83 | "railway_crossing", 84 | "derail", 85 | "proposed", 86 | "miniature", 87 | "facility", 88 | "junction", 89 | "signal_box", 90 | "preserved", 91 | "platform_edge", 92 | "train_station_entrance", 93 | "monorail", 94 | "yard", 95 | "turntable", 96 | "phone", 97 | "spur_junction", 98 | "radio", 99 | "funicular", 100 | "service_station", 101 | "dismantled", 102 | "vacancy_detection", 103 | "power_supply", 104 | "rail_brake", 105 | "owner_change" 106 | ], 107 | "power": [ 108 | "tower", 109 | "pole", 110 | "generator", 111 | "line", 112 | "minor_line", 113 | "substation", 114 | "portal", 115 | "transformer", 116 | "catenary_mast", 117 | "switch", 118 | "plant", 119 | "cable", 120 | "terminal", 121 | "insulator", 122 | "heliostat", 123 | "cable_distribution_cabinet", 124 | "connection", 125 | "switchgear", 126 | "compensator", 127 | "sub_station", 128 | "circuit", 129 | "busbar", 130 | "station", 131 | "converter", 132 | "branch", 133 | "marker" 134 | ], 135 | "roadway": [ 136 | "highway=motorway", 137 | "highway=motorway_link", 138 | "highway=trunk", 139 | "highway=trunk_link", 140 | "highway=primary", 141 | "highway=primary_link", 142 | "highway=secondary", 143 | "highway=secondary_link", 144 | "highway=tertiary", 145 | "highway=tertiary_link", 146 | "highway=unclassified", 147 | "highway=residential", 148 | "highway=track", 149 | "highway=living_street", 150 | "highway=service", 151 | "highway=road", 152 | "highway=busway", 153 | "highway=bus_guideway", 154 | "highway=raceway", 155 | "highway=escape", 156 | "highway=construction" 157 | ], 158 | "walkway": [ 159 | "highway=footway", 160 | "highway=pedestrian", 161 | "highway=bridleway", 162 | "highway=steps", 163 | "highway=cycleway", 164 | "highway=path", 165 | "highway=sidewalk" 166 | ] 167 | } 168 | -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetnus/osm-finder/14377594cf732c214280db9b0e46615805ab204a/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/generalTools.js: -------------------------------------------------------------------------------- 1 | import PolygonClipping from 'polygon-clipping'; 2 | 3 | /* 4 | Thanks vbarbarosh! 5 | https://stackoverflow.com/a/38977789/1941353 6 | */ 7 | function calculateIntersection(line1, line2) { 8 | if (line1.length != 2 || line2.length != 2) 9 | return; 10 | const [x1, y1] = line1[0]; 11 | const [x2, y2] = line1[1]; 12 | const [x3, y3] = line2[0]; 13 | const [x4, y4] = line2[1]; 14 | let ua, ub, denom = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1); 15 | if (denom == 0) { 16 | return null; 17 | } 18 | ua = ((x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3))/denom; 19 | ub = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3))/denom; 20 | return { 21 | // Returns true if the two lines intersect, false otherwise. 22 | intersects: ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1, 23 | // Returns the X and Y coords of the (potential) intersection 24 | x: x1 + ua * (x2 - x1), 25 | y: y1 + ua * (y2 - y1) 26 | }; 27 | } 28 | 29 | function getLineLength(x1, y1, x2, y2) { 30 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 31 | } 32 | 33 | function getPointAtDistance(point1, point2, distance) { 34 | const d = getLineLength(point1[0], point1[1], point2[0], point2[1]); 35 | const t = distance / d; 36 | const x = (1 - t) * point1[0] + t * point2[0]; 37 | const y = (1 - t) * point1[1] + t * point2[1]; 38 | return {x: x, y: y}; 39 | } 40 | 41 | // Thanks Ondrej! https://www.freecodecamp.org/news/javascript-debounce-example/ 42 | function debounce(func, timeout = 300) { 43 | let timer; 44 | return (...args) => { 45 | clearTimeout(timer); 46 | timer = setTimeout(() => { func.apply(this, args); }, timeout); 47 | }; 48 | } 49 | 50 | function getUniquePairs(list) { 51 | let pairs = [] 52 | let primaryRemaining = list.slice(0); 53 | let secondaryRemaining = []; 54 | let current1 = null; 55 | let current2 = null; 56 | 57 | while (primaryRemaining.length > 1) { 58 | if (secondaryRemaining.length > 0) { 59 | current2 = secondaryRemaining.pop(); 60 | } else { 61 | current1 = primaryRemaining.pop(); 62 | secondaryRemaining = primaryRemaining.slice(0); 63 | current2 = secondaryRemaining.pop(); 64 | } 65 | pairs.push({first: current1, second: current2}); 66 | } 67 | return pairs; 68 | } 69 | 70 | // Thanks pragmar! https://stackoverflow.com/a/43747218/1941353 71 | function calculatePolygonCentroid(points) { 72 | let first = points[0], last = points[points.length-1]; 73 | if (first[0] != last[0] || first[1] != last[1]) points.push(first); 74 | let twicearea=0, x=0, y=0, nPts = points.length, p1, p2, f; 75 | for ( let i=0, j=nPts-1 ; i 0) { 104 | intersectingShapes.push(currentShapes[i]); 105 | } 106 | } 107 | 108 | let newShape = []; 109 | if (intersectingShapes.length > 0) { 110 | if (mode === 'add') { 111 | let intersectingPoints = intersectingShapes.map((s) => [s.points]); 112 | intersectingShapes[0].points = PolygonClipping.union([newShapePoints], ...intersectingPoints); 113 | for (let i = 1; i < intersectingShapes.length; i++) { 114 | intersectingShapes[i].points = []; 115 | } 116 | } else if (mode === 'sub') { 117 | for (let i = 0; i < intersectingShapes.length; i++) { 118 | let p = PolygonClipping.difference([intersectingShapes[i].points], [newShapePoints]); 119 | intersectingShapes[i].points = p; 120 | } 121 | } 122 | } else { 123 | newShape = newShapePoints; 124 | } 125 | 126 | // Thanks Andrii! https://stackoverflow.com/a/33670691/1941353 127 | const calculatePolygonArea = (vertices) => { 128 | let total = 0; 129 | for (let i = 0, l = vertices.length; i < l; i++) { 130 | let addX = vertices[i][0]; 131 | let addY = vertices[i == vertices.length - 1 ? 0 : i + 1][1]; 132 | let subX = vertices[i == vertices.length - 1 ? 0 : i + 1][0]; 133 | let subY = vertices[i][1]; 134 | total += (addX * addY * 0.5); 135 | total -= (subX * subY * 0.5); 136 | } 137 | return Math.abs(total); 138 | } 139 | 140 | // If the clipping operation resulted in a polygon being split into multiple polygons, 141 | // we only want to keep the polygon with the highest area, since we don't support 142 | // multipolygonal shapes at this point. 143 | for (let i = 0; i < intersectingShapes.length; i++) { 144 | let maxArea = 0; 145 | let pointSets = intersectingShapes[i].points; 146 | let newPoints = []; 147 | for (let j = 0; j < pointSets.length; j++) { 148 | let area = calculatePolygonArea(pointSets[j][0]); 149 | if (area > maxArea) { 150 | maxArea = area; 151 | newPoints = pointSets[j][0]; 152 | } 153 | } 154 | intersectingShapes[i].points = newPoints; 155 | } 156 | 157 | return {'existingShapes': intersectingShapes, 'newShape': newShape} 158 | } 159 | 160 | export {calculateIntersection, calculatePolygonCentroid, 161 | getLineLength, getPointAtDistance, getUniquePairs, 162 | debounce, clipPolygons} -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | height: 100%; 5 | } 6 | 7 | @media (hover: hover) { 8 | a:hover { 9 | background-color: hsla(160, 100%, 37%, 0.2); 10 | } 11 | } 12 | 13 | @media (min-width: 1024px) { 14 | body { 15 | } 16 | 17 | #app { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/queryCreator.js: -------------------------------------------------------------------------------- 1 | import {calculateIntersection} from './generalTools.js' 2 | import {calculateBounds, createTagsQuery, createMaxDistanceQuery, createMinDistanceQuery, createNoOverlappingQuery, calculateHuMoments} from './queryTools.js' 3 | 4 | // Example query for single object 5 | /* 6 | WITH sorted_shapes AS 7 | ( 8 | SELECT 9 | sqrt(pow(hu1 - 1.579253696117602, 2) + pow(hu2 - 0.10631286212389285, 2) + pow(hu3 - 0.13545548639569183, 2) + pow(hu4 - 0.013422893459921623, 2) + pow(hu5 - 0.000292619813566153, 2) + pow(hu6 - 0.002071577493100666, 2) + pow(hu7 - -0.0004919013709091156, 2)) AS distance, 10 | replace(replace(shape1.osm_type, 'N', 'www.openstreetmap.org/node/'), 'W', 'www.openstreetmap.org/way/') || shape1.osm_id AS shape1_id 11 | FROM shapes AS shape1 12 | WHERE shape1.category = 'building' 13 | ORDER BY distance ASC 14 | ), 15 | filtered_shapes AS 16 | ( 17 | SELECT 18 | sorted_shapes.shape1_id, COUNT(*) OVER () AS count, ROW_NUMBER() OVER () AS row 19 | FROM sorted_shapes 20 | ) 21 | SELECT filtered_shapes.shape1_id 22 | FROM filtered_shapes 23 | WHERE filtered_shapes.row < GREATEST((filtered_shapes.count * 0.01), 1000); 24 | */ 25 | 26 | function constructSingleObjectQuery(annotations, shapes, displayUrls) { 27 | let query = ''; 28 | if (annotations[0].geometryType === 'shape') { 29 | query += 'WITH sorted_shapes AS\n'; 30 | query += '(\n'; 31 | query += ' '; 32 | } 33 | 34 | query += 'SELECT\n'; 35 | 36 | if (annotations[0].geometryType === 'shape') { 37 | let moments = calculateHuMoments(annotations[0].points); 38 | query += ' sqrt(pow(' + annotations[0].name + '.hu1 - ' + moments[0] + ', 2) + pow(' + annotations[0].name + '.hu2 - ' + moments[1] + ', 2) + pow(' + annotations[0].name + '.hu3 - ' + moments[2] + ', 2) + pow(' + annotations[0].name + '.hu4 - ' + moments[3] + ', 2) + pow(' + annotations[0].name + '.hu5 - ' + moments[4] + ', 2) + pow(' + annotations[0].name + '.hu6 - ' + moments[5] + ', 2) + pow(' + annotations[0].name + '.hu7 - ' + moments[6] + ', 2)) AS distance,\n'; 39 | query += ' '; 40 | } 41 | 42 | let wayPrefix = displayUrls ? 'www.openstreetmap.org/way/' : 'way '; 43 | let nodePrefix = displayUrls ? 'www.openstreetmap.org/node/' : 'node '; 44 | query += ' replace(replace(' + annotations[0].name + ".osm_type, 'N', '" + nodePrefix + "'), 'W', '" + wayPrefix + "') || "; 45 | query += annotations[0].name + '.osm_id AS ' + annotations[0].name + '_id\n'; 46 | 47 | if (annotations[0].geometryType === 'shape') { 48 | query += ' '; 49 | } 50 | 51 | query += 'FROM '; 52 | query += annotations[0].geometryType + 's AS ' + annotations[0].name + '\n'; 53 | 54 | if (annotations[0].geometryType === 'shape') { 55 | query += ' '; 56 | } 57 | 58 | query += 'WHERE '; 59 | // Filters results by category and subcategory 60 | let category = annotations[0].category; 61 | let subcategory = annotations[0].subcategory; 62 | let subcategoryString = (subcategory == '' ? '' : annotations[0].name + '.subcategory = \'' + subcategory + '\' AND '); 63 | query += annotations[0].name + '.category = \'' + category + '\' AND ' + subcategoryString; 64 | 65 | // Adds any additional tags entered by the user to the WHERE 66 | if (annotations[0].tags.length != 0) { 67 | query += createTagsQuery(annotations[0]); 68 | } 69 | query = query.slice(0, query.length - 5); // remove last AND 70 | 71 | if (shapes.length > 0) { 72 | // Filters by min and max areas 73 | if (annotations[0].minArea) { 74 | query += ' AND ST_Area(ST_Transform(' + annotations[0].name + '.geom, 4326)::geography) >= ' + annotations[0].minArea; 75 | } 76 | if (annotations[0].maxArea) { 77 | query += ' AND ST_Area(ST_Transform(' + annotations[0].name + '.geom, 4326)::geography) <= ' + annotations[0].maxArea; 78 | } 79 | 80 | query += '\n'; 81 | query += '),\n'; 82 | query += 'filtered_shapes AS\n'; 83 | query += '(\n'; 84 | query += ' SELECT\n'; 85 | query += ' sorted_shapes.' + annotations[0].name + '_id, COUNT(*) OVER () AS count, ROW_NUMBER() OVER (ORDER BY distance ASC) AS row\n'; 86 | query += ' FROM sorted_shapes\n'; 87 | query += ')\n'; 88 | query += 'SELECT filtered_shapes.' + annotations[0].name + '_id\n'; 89 | query += 'FROM filtered_shapes\n'; 90 | query += 'WHERE filtered_shapes.row < GREATEST((filtered_shapes.count * 0.01), 1000)\n'; 91 | query += 'ORDER BY row ASC'; 92 | } 93 | query += ';'; 94 | 95 | return query; 96 | } 97 | 98 | // Example Query for Disjoint 99 | /* 100 | WITH sorted_shapes AS 101 | ( 102 | SELECT 103 | sqrt 104 | ( 105 | pow( 106 | sqrt(pow(shape1.hu1 - 1.5733173076923075, 2) + pow(shape1.hu2 - 0.324551808672033, 2) + pow(shape1.hu3 - 0.9423223732061282, 2) + pow(shape1.hu4 - 0.04076357752790922, 2) + pow(shape1.hu5 - 0.007773110612955471, 2) + pow(shape1.hu6 - 0.021820327987814064, 2) + pow(shape1.hu7 - -0.0018459434208921142, 2)) 107 | , 2 108 | ) + 109 | pow( 110 | sqrt(pow(shape2.hu1 - 1.5846498873027794, 2) + pow(shape2.hu2 - 0.017399393958774146, 2) + pow(shape2.hu3 - 1.202141013334282, 2) + pow(shape2.hu4 - 0.003305741482351561, 2) + pow(shape2.hu5 - 0.00013059068294749503, 2) + pow(shape2.hu6 - -0.00003860122739888982, 2) + pow(shape2.hu7 - -0.00016239857863435821, 2)) 111 | , 2 112 | ) 113 | ) AS distance, 114 | replace(replace(shape1.osm_type, 'N', 'www.openstreetmap.org/node/'), 'W', 'www.openstreetmap.org/way/') || shape1.osm_id AS shape1_id, 115 | replace(replace(shape2.osm_type, 'N', 'www.openstreetmap.org/node/'), 'W', 'www.openstreetmap.org/way/') || shape2.osm_id AS shape2_id 116 | FROM shapes AS shape1, shapes AS shape2 117 | WHERE shape1.category = 'building' AND shape2.category = 'building' AND shape2.osm_id != shape1.osm_id AND ST_DWithin(shape1.geom, shape2.geom, 100) 118 | ORDER BY distance ASC 119 | ), 120 | filtered_shapes AS 121 | ( 122 | SELECT 123 | sorted_shapes.shape1_id, sorted_shapes.shape2_id, COUNT(*) OVER () AS count, ROW_NUMBER() OVER () AS row 124 | FROM sorted_shapes 125 | ) 126 | SELECT filtered_shapes.shape1_id, filtered_shapes.shape2_id 127 | FROM filtered_shapes 128 | WHERE filtered_shapes.row < GREATEST((filtered_shapes.count * 0.01), 1000); 129 | */ 130 | 131 | function constructDisjointQuery(annotations, lines, shapes, displayUrls) { 132 | let query = ''; 133 | 134 | if (shapes.length > 0) { 135 | query += 'WITH sorted_shapes AS\n'; 136 | query += '(\n'; 137 | query += ' '; 138 | } 139 | 140 | query += 'SELECT\n'; 141 | 142 | if (shapes.length > 1) { 143 | query += ' sqrt\n'; 144 | query += ' (\n'; 145 | for (let i = 0; i < shapes.length; i++) { 146 | let moments = calculateHuMoments(shapes[i].points); 147 | query += ' pow(\n'; 148 | query += ' sqrt(pow(' + shapes[i].name + '.hu1 - ' + moments[0] + ', 2) + pow(' + shapes[i].name + '.hu2 - ' + moments[1] + ', 2) + pow(' + shapes[i].name + '.hu3 - ' + moments[2] + ', 2) + pow(' + shapes[i].name + '.hu4 - ' + moments[3] + ', 2) + pow(' + shapes[i].name + '.hu5 - ' + moments[4] + ', 2) + pow(' + shapes[i].name + '.hu6 - ' + moments[5] + ', 2) + pow(' + shapes[i].name + '.hu7 - ' + moments[6] + ', 2))\n'; 149 | query += ' , 2\n'; 150 | const plus = (i == shapes.length - 1) ? '' : ' +'; 151 | query += ' )' + plus + '\n'; 152 | } 153 | query += ' ) AS distance,\n'; 154 | } else if (shapes.length == 1) { 155 | let moments = calculateHuMoments(shapes[0].points); 156 | query += ' sqrt(pow(' + shapes[0].name + '.hu1 - ' + moments[0] + ', 2) + pow(' + shapes[0].name + '.hu2 - ' + moments[1] + ', 2) + pow(' + shapes[0].name + '.hu3 - ' + moments[2] + ', 2) + pow(' + shapes[0].name + '.hu4 - ' + moments[3] + ', 2) + pow(' + shapes[0].name + '.hu5 - ' + moments[4] + ', 2) + pow(' + shapes[0].name + '.hu6 - ' + moments[5] + ', 2) + pow(' + shapes[0].name + '.hu7 - ' + moments[6] + ', 2)) AS distance,\n'; 157 | } 158 | 159 | for (let i = 0; i < annotations.length; i++) { 160 | const comma = (i == annotations.length - 1) ? '' : ', '; 161 | let wayPrefix = displayUrls ? 'www.openstreetmap.org/way/' : 'way '; 162 | let nodePrefix = displayUrls ? 'www.openstreetmap.org/node/' : 'node '; 163 | query += ' replace(replace(' + annotations[i].name + ".osm_type, 'N', '" + nodePrefix + "'), 'W', '" + wayPrefix + "') || "; 164 | query += annotations[i].name + '.osm_id AS ' + annotations[i].name + '_id' + comma + '\n'; 165 | } 166 | 167 | query += 'FROM '; 168 | for (let i = 0; i < annotations.length; i++) { 169 | const comma = (i == annotations.length - 1) ? '\n' : ', '; 170 | query += annotations[i].geometryType + 's AS ' + annotations[i].name + comma; 171 | } 172 | 173 | query += 'WHERE '; 174 | for (let i = 0; i < annotations.length; i++) { 175 | // Filters results by category and subcategory 176 | let category = annotations[i].category; 177 | let subcategory = annotations[i].subcategory; 178 | let subcategoryString = (subcategory == '' ? '' : annotations[i].name + '.subcategory = \'' + subcategory + '\' AND '); 179 | query += annotations[i].name + '.category = \'' + category + '\' AND ' + subcategoryString; 180 | 181 | // Adds any additional tags entered by the user to the WHERE 182 | if (annotations[i].tags.length != 0) { 183 | query += createTagsQuery(annotations[i]); 184 | } 185 | } 186 | 187 | query += createNoOverlappingQuery(annotations); 188 | query += createMaxDistanceQuery(annotations); 189 | query += createMinDistanceQuery(annotations); 190 | 191 | for (let i = 0; i < shapes.length; i++) { 192 | // Filters by min and max areas 193 | if (shapes[i].minArea) { 194 | query += 'ST_Area(ST_Transform(' + shapes[i].name + '.geom, 4326)::geography) >= ' + shapes[i].minArea; 195 | query += ' AND '; 196 | } 197 | if (shapes[i].maxArea) { 198 | query += 'ST_Area(ST_Transform(' + shapes[i].name + '.geom, 4326)::geography) <= ' + shapes[i].maxArea; 199 | query += ' AND '; 200 | } 201 | } 202 | 203 | query += '\n'; 204 | 205 | // Calculates the angle and error parameters and adds them to the query 206 | for (let i = 0; i < lines.length; i++) { 207 | const keys = Object.keys(lines[i].relations) 208 | for (let k = 0; k < keys.length; k++) { 209 | const relation = lines[i].relations[keys[k]]; 210 | 211 | if (relation.angle != null) { 212 | let angle = relation.angle; 213 | let error = relation.error ? relation.error : 0; 214 | 215 | let bounds = calculateBounds(angle, error); 216 | 217 | query += '(\n'; 218 | for (let b = 0; b < bounds.lower.length; b++) { 219 | query += ' (\n'; 220 | query += ' abs((\n'; 221 | query += ' degrees(ST_Azimuth(ST_StartPoint(' + keys[k] + '.geom), ST_EndPoint(' + keys[k] + '.geom)))\n'; 222 | query += ' -\n'; 223 | query += ' degrees(ST_Azimuth(ST_StartPoint(' + lines[i].name + '.geom), ST_EndPoint(' + lines[i].name + '.geom)))\n'; 224 | query += ' )::decimal % 180.0) '; 225 | 226 | if (bounds.lower[b] === bounds.upper[b]) 227 | query += '= ' + bounds.lower[b] + '\n'; 228 | else 229 | query += 'BETWEEN ' + bounds.lower[b] + ' AND ' + bounds.upper[b] + '\n'; 230 | 231 | query += ' ) ' + ((bounds.lower.length > 1 && b == 0) ? '\n OR' : '') + '\n'; 232 | } 233 | query += ')\n'; 234 | query += ' AND \n'; 235 | } 236 | } 237 | } 238 | 239 | query = query.slice(0, query.length - 6); // remove last AND 240 | 241 | if (shapes.length > 0) { 242 | query += '\n'; 243 | query += '),\n'; 244 | query += 'filtered_shapes AS\n'; 245 | query += '(\n'; 246 | query += ' SELECT\n'; 247 | query += ' '; 248 | for (let i = 0; i < annotations.length; i++) { 249 | query += 'sorted_shapes.' + annotations[i].name + '_id, '; 250 | } 251 | query += 'COUNT(*) OVER () AS count, ROW_NUMBER() OVER (ORDER BY distance ASC) AS row\n'; 252 | query += ' FROM sorted_shapes\n'; 253 | query += ')\n'; 254 | query += 'SELECT ' 255 | for (let i = 0; i < annotations.length; i++) { 256 | const comma = (i == annotations.length - 1) ? '\n' : ', '; 257 | query += 'filtered_shapes.' + annotations[i].name + '_id' + comma; 258 | } 259 | query += 'FROM filtered_shapes\n'; 260 | query += 'WHERE filtered_shapes.row < GREATEST((filtered_shapes.count * 0.01), 1000)\n'; 261 | query += 'ORDER BY row ASC'; 262 | } 263 | 264 | query += ';' 265 | 266 | return query; 267 | } 268 | 269 | // Example Query for Intersecting 270 | /* 271 | WITH intersections AS 272 | ( 273 | SELECT 274 | ((ST_DumpPoints( 275 | ST_Intersection(line1.geom, line3.geom) 276 | )).geom) AS intersection1, 277 | ((ST_DumpPoints( 278 | ST_Intersection(line2.geom, line3.geom) 279 | )).geom) AS intersection2, 280 | line1.geom AS line1_geom, 281 | line2.geom AS line2_geom, 282 | line3.geom AS line3_geom, 283 | line1.id AS line1_id, 284 | line2.id AS line2_id, 285 | line3.id AS line3_id 286 | FROM linestrings AS line1, linestrings AS line2, linestrings as line3 287 | WHERE line1.tags->>'bridge' = 'yes' AND line1.category = 'highway' AND line1.subcategory = 'vehicle' AND 288 | line2.tags->>'bridge' = 'yes' AND line2.category = 'highway' AND line2.subcategory = 'vehicle' AND 289 | line1.id != line2.id AND line3.category = 'highway' AND line3.subcategory = 'vehicle' AND 290 | ST_Intersects(line1.geom, line3.geom) AND ST_Intersects(line2.geom, line3.geom) AND 291 | ST_DWithin(line1.geom, line2.geom, 1000) AND ST_Distance(line1.geom, line2.geom) > 200 292 | ), 293 | buffers AS 294 | ( 295 | SELECT 296 | intersections.intersection1, 297 | intersections.intersection2, 298 | ST_ExteriorRing(ST_Buffer(intersections.intersection1, 0.5)) AS ring1, 299 | ST_ExteriorRing(ST_Buffer(intersections.intersection2, 0.5)) AS ring2, 300 | intersections.line1_geom, 301 | intersections.line2_geom, 302 | intersections.line3_geom, 303 | intersections.line1_id, 304 | intersections.line2_id, 305 | intersections.line3_id 306 | FROM intersections 307 | ), 308 | points AS 309 | ( 310 | SELECT 311 | ST_GeometryN 312 | ( 313 | ST_Intersection(buffers.ring1, buffers.line1_geom) 314 | , 1 315 | ) AS ring1_p1, 316 | ST_GeometryN 317 | ( 318 | ST_Intersection(buffers.ring1, buffers.line3_geom) 319 | , 1 320 | ) AS ring1_p2, 321 | ST_GeometryN 322 | ( 323 | ST_Intersection(buffers.ring2, buffers.line2_geom) 324 | , 1 325 | ) AS ring2_p1, 326 | ST_GeometryN 327 | ( 328 | ST_Intersection(buffers.ring2, buffers.line3_geom) 329 | , 1 330 | ) AS ring2_p2, 331 | buffers.intersection1, 332 | buffers.intersection2, 333 | buffers.ring1, 334 | buffers.ring2, 335 | buffers.line1_geom, 336 | buffers.line2_geom, 337 | buffers.line3_geom, 338 | buffers.line1_id, 339 | buffers.line2_id, 340 | buffers.line3_id 341 | FROM buffers 342 | ) 343 | SELECT 344 | points.line1_id, 345 | points.line2_id, 346 | points.line3_id 347 | FROM points 348 | WHERE 349 | ( 350 | abs(round(degrees( 351 | ST_Azimuth(points.ring1_p2, points.intersection1) 352 | - 353 | ST_Azimuth(points.ring1_p1, points.intersection1) 354 | ))::decimal % 180.0) BETWEEN 88 AND 92 355 | ) 356 | AND 357 | ( 358 | abs(round(degrees( 359 | ST_Azimuth(points.ring2_p2, points.intersection2) 360 | - 361 | ST_Azimuth(points.ring2_p1, points.intersection2) 362 | ))::decimal % 180.0) BETWEEN 45 AND 85 363 | OR 364 | abs(round(degrees( 365 | ST_Azimuth(points.ring2_p2, points.intersection2) 366 | - 367 | ST_Azimuth(points.ring2_p1, points.intersection2) 368 | ))::decimal % 180.0) BETWEEN 95 AND 135 369 | ) 370 | ; 371 | */ 372 | function constructIntersectingQuery(annotations, nodes, lines, shapes, displayUrls) { 373 | // Generates an array that consists of pairs of intersecting lines 374 | let intersections = []; 375 | let dbd = []; 376 | for (let i = 0; i < lines.length; i++) { 377 | for (let k = 0; k < lines.length; k++) { 378 | if (i == k) continue; 379 | 380 | // The line whose name comes first when sorted holds the relation information. Find it. 381 | const index1 = lines[i].name > lines[k].name ? k : i; 382 | const index2 = lines[i].name > lines[k].name ? i : k; 383 | const relation = lines[index1].relations[lines[index2].name]; 384 | 385 | const line1 = lines[i].points; 386 | const line2 = lines[k].points; 387 | const intersection = calculateIntersection(line1, line2); 388 | if (intersection && intersection.intersects) { 389 | // Ensures that the pair hasn't already been pushed 390 | const pair = [lines[i].name, lines[k].name]; 391 | const match = intersections.filter(a => pair.includes(a.line1) && pair.includes(a.line2)); 392 | if (match.length != 0) continue; 393 | 394 | let isAngle = false; 395 | let angle = null; 396 | let error = null; 397 | 398 | if (relation && relation.angle != null) { 399 | isAngle = true; 400 | angle = relation.angle; 401 | error = relation.error; 402 | } 403 | 404 | const description = { 405 | id: intersections.filter(a => a.isAngle == true).length + 1, 406 | isPseudo: false, 407 | isAngle: isAngle, 408 | node: null, 409 | line1: lines[i].name, 410 | line2: lines[k].name, 411 | angle: angle, 412 | error: error, 413 | pseudoAngle: null, 414 | pseudoError: null, 415 | minDistance: null, 416 | maxDistance: null, 417 | } 418 | intersections.push(description) 419 | } else if (relation && relation.angle != null) { 420 | // Handles disjoint but directional (DBD) relations 421 | 422 | // Ensures that the pair hasn't already been pushed 423 | const pair = [lines[i].name, lines[k].name]; 424 | const match = dbd.filter(a => pair.includes(a.line1) && pair.includes(a.line2)); 425 | if (match.length != 0) continue; 426 | 427 | dbd.push({ 428 | line1: lines[i].name, 429 | line2: lines[k].name, 430 | angle: relation.angle, 431 | error: relation.error, 432 | }); 433 | } 434 | } 435 | } 436 | 437 | // Generates an array containing the 'pseudo' intersections created by temporary lines drawn 438 | // between individual nodes/shapes and intersections. 439 | let pseudoNodes = nodes.concat(shapes); 440 | for (let i = 0; i < pseudoNodes.length; i++) { 441 | const keys = Object.keys(pseudoNodes[i].relations); 442 | for (let k = 0; k < keys.length; k++) { 443 | if (!keys[k].includes('&')) continue; 444 | 445 | const key = keys[k]; 446 | const line1 = key.split('&')[0]; 447 | const line2 = key.split('&')[1]; 448 | const relation = pseudoNodes[i].relations[key]; 449 | 450 | if (relation) { 451 | const pair = [line1, line2]; 452 | const index = intersections.findIndex(a => pair.includes(a.line1) && pair.includes(a.line2)); 453 | 454 | let isAngle = relation.angle ? true : false; 455 | if (index >= 0 && isAngle == false) 456 | isAngle = intersections[index].isAngle; 457 | 458 | const id = intersections.filter(a => a.isAngle == true).length + 1; 459 | 460 | const description = { 461 | id: index >= 0 ? intersections[index].id : id, 462 | isPseudo: true, 463 | isAngle: isAngle, 464 | node: pseudoNodes[i].name, 465 | line1: line1, 466 | line2: line2, 467 | angle: index >= 0 ? intersections[index].angle : null, 468 | error: index >= 0 ? intersections[index].error : null, 469 | pseudoAngle: relation.angle, 470 | pseudoError: relation.error, 471 | minDistance: relation.minDistance, 472 | maxDistance: relation.maxDistance, 473 | } 474 | 475 | if (index >= 0) { 476 | intersections[index] = description; 477 | } else { 478 | intersections.push(description); 479 | } 480 | } 481 | } 482 | } 483 | 484 | let query = 'WITH intersections AS\n'; 485 | query += '(\n'; 486 | query += ' SELECT\n'; 487 | 488 | for (let i = 0; i < intersections.length; i++) { 489 | const item = intersections[i]; 490 | 491 | query += ' ((ST_DumpPoints(\n'; 492 | query += ' ST_Intersection(' + item.line1 + '.geom, ' + item.line2 + '.geom)\n'; 493 | query += ' )).geom) AS intersection' + item.id + ',\n'; 494 | } 495 | 496 | if (shapes.length > 1) { 497 | query += ' sqrt\n'; 498 | query += ' (\n'; 499 | for (let i = 0; i < shapes.length; i++) { 500 | let moments = calculateHuMoments(shapes[i].points); 501 | query += ' pow(\n'; 502 | query += ' sqrt(pow(' + shapes[i].name + '.hu1 - ' + moments[0] + ', 2) + pow(' + shapes[i].name + '.hu2 - ' + moments[1] + ', 2) + pow(' + shapes[i].name + '.hu3 - ' + moments[2] + ', 2) + pow(' + shapes[i].name + '.hu4 - ' + moments[3] + ', 2) + pow(' + shapes[i].name + '.hu5 - ' + moments[4] + ', 2) + pow(' + shapes[i].name + '.hu6 - ' + moments[5] + ', 2) + pow(' + shapes[i].name + '.hu7 - ' + moments[6] + ', 2))\n'; 503 | query += ' , 2\n'; 504 | const plus = (i == shapes.length - 1) ? '' : ' +'; 505 | query += ' )' + plus + '\n'; 506 | } 507 | query += ' ) AS distance,\n'; 508 | } else if (shapes.length == 1) { 509 | let moments = calculateHuMoments(shapes[0].points); 510 | query += ' sqrt(pow(' + shapes[0].name + '.hu1 - ' + moments[0] + ', 2) + pow(' + shapes[0].name + '.hu2 - ' + moments[1] + ', 2) + pow(' + shapes[0].name + '.hu3 - ' + moments[2] + ', 2) + pow(' + shapes[0].name + '.hu4 - ' + moments[3] + ', 2) + pow(' + shapes[0].name + '.hu5 - ' + moments[4] + ', 2) + pow(' + shapes[0].name + '.hu6 - ' + moments[5] + ', 2) + pow(' + shapes[0].name + '.hu7 - ' + moments[6] + ', 2)) AS distance,\n'; 511 | } 512 | 513 | for (let i = 0; i < shapes.length; i++) { 514 | if (intersections.some(a => a.isPseudo && a.node === shapes[i].name)) { 515 | // We only need to carry the geometries of shapes that intersect at defined angles 516 | query += ' ' + shapes[i].name + '.geom AS ' + shapes[i].name + '_geom,\n'; 517 | } 518 | const comma = (i == shapes.length - 1 && lines.length == 0 && nodes.length == 0) ? '' : ','; 519 | query += ' ' + shapes[i].name + '.osm_id AS ' + shapes[i].name + '_id' + comma + '\n'; 520 | query += ' ' + shapes[i].name + '.osm_type AS ' + shapes[i].name + '_type' + comma + '\n'; 521 | } 522 | 523 | for (let i = 0; i < lines.length; i++) { 524 | if (intersections.some(a => a.isAngle && [a.line1, a.line2].includes(lines[i].name))) { 525 | // We only need to carry the geometries of lines that intersect at defined angles 526 | query += ' ' + lines[i].name + '.geom AS ' + lines[i].name + '_geom,\n'; 527 | } 528 | const comma = (i == lines.length - 1 && nodes.length == 0) ? '' : ','; 529 | query += ' ' + lines[i].name + '.osm_id AS ' + lines[i].name + '_id,\n'; 530 | query += ' ' + lines[i].name + '.osm_type AS ' + lines[i].name + '_type' + comma + '\n'; 531 | } 532 | 533 | for (let i = 0; i < nodes.length; i++) { 534 | if (intersections.some(a => a.isPseudo && a.node === nodes[i].name)) { 535 | // We only need to carry the geometries of nodes that intersect at defined angles 536 | query += ' ' + nodes[i].name + '.geom AS ' + nodes[i].name + '_geom,\n'; 537 | } 538 | const comma = (i == nodes.length - 1) ? '' : ','; 539 | query += ' ' + nodes[i].name + '.osm_id AS ' + nodes[i].name + '_id,\n'; 540 | query += ' ' + nodes[i].name + '.osm_type AS ' + nodes[i].name + '_type' + comma + '\n'; 541 | } 542 | 543 | query += ' FROM '; 544 | 545 | for (let i = 0; i < shapes.length; i++) { 546 | const comma = (i == shapes.length - 1 && lines.length == 0 && nodes.length == 0) ? '\n' : ', '; 547 | query += shapes[i].geometryType + 's AS ' + shapes[i].name + comma; 548 | } 549 | 550 | for (let i = 0; i < lines.length; i++) { 551 | const comma = (i == lines.length - 1 && nodes.length == 0) ? '\n' : ', '; 552 | query += lines[i].geometryType + 's AS ' + lines[i].name + comma; 553 | } 554 | 555 | for (let i = 0; i < nodes.length; i++) { 556 | const comma = (i == nodes.length - 1) ? '\n' : ', '; 557 | query += nodes[i].geometryType + 's AS ' + nodes[i].name + comma; 558 | } 559 | 560 | query += ' WHERE '; 561 | 562 | for (let i = 0; i < annotations.length; i++) { 563 | // Filters results by category and subcategory 564 | let category = annotations[i].category; 565 | let subcategory = annotations[i].subcategory; 566 | let subcategoryString = (subcategory == '' ? '' : annotations[i].name + '.subcategory = \'' + subcategory + '\' AND '); 567 | query += annotations[i].name + '.category = \'' + category + '\' AND ' + subcategoryString; 568 | 569 | // Adds any additional tags entered by the user to the WHERE 570 | if (annotations[i].tags.length != 0) { 571 | query += createTagsQuery(annotations[i]); 572 | } 573 | } 574 | 575 | for (let i = 0; i < shapes.length; i++) { 576 | // Filters by min and max areas 577 | if (shapes[i].minArea) { 578 | query += 'ST_Area(ST_Transform(' + shapes[i].name + '.geom, 4326)::geography) >= ' + shapes[i].minArea; 579 | query += ' AND '; 580 | } 581 | if (shapes[i].maxArea) { 582 | query += 'ST_Area(ST_Transform(' + shapes[i].name + '.geom, 4326)::geography) <= ' + shapes[i].maxArea; 583 | query += ' AND '; 584 | } 585 | } 586 | 587 | query += createNoOverlappingQuery(annotations); 588 | 589 | // Filters results early by ensuring that all of the lines that are supposed to intersect, do intersect 590 | for (let i = 0; i < intersections.length; i++) { 591 | const item = intersections[i]; 592 | if (item.isAngle == false || (item.isPseudo == true && item.angle == null)) continue; 593 | 594 | query += 'ST_Intersects(' + item.line1 + '.geom, ' + item.line2 + '.geom) AND ' 595 | } 596 | 597 | query += createMaxDistanceQuery(annotations); 598 | query += createMinDistanceQuery(annotations); 599 | 600 | // Accounts for all of the lines that have angle information entered, but don't intersect. 601 | // In other words, disjoint but directional (dbd) relations. 602 | for (let i = 0; i < dbd.length; i++) { 603 | const item = dbd[i]; 604 | if (item.angle != null) { 605 | const error = item.error ? item.error : 0; 606 | 607 | let bounds = calculateBounds(item.angle, error); 608 | 609 | query += '\n (\n'; 610 | for (let b = 0; b < bounds.lower.length; b++) { 611 | query += ' (\n'; 612 | query += ' abs(round(degrees(\n'; 613 | query += ' ST_Azimuth(ST_StartPoint(' + item.line1 + '.geom), ST_EndPoint(' + item.line2 + '.geom))\n'; 614 | query += ' -\n'; 615 | query += ' ST_Azimuth(ST_StartPoint(' + item.line1 + '.geom), ST_EndPoint(' + item.line2 + '.geom))\n'; 616 | query += ' ))::decimal % 180.0) '; 617 | 618 | if (bounds.lower[b] === bounds.upper[b]) 619 | query += '= ' + bounds.lower[b] + '\n'; 620 | else 621 | query += 'BETWEEN ' + bounds.lower[b] + ' AND ' + bounds.upper[b] + '\n'; 622 | 623 | query += ' ) ' + ((bounds.lower.length > 1 && b == 0) ? '\n OR' : '') + '\n'; 624 | } 625 | query += ' )\n'; 626 | query += ' AND'; 627 | } 628 | } 629 | 630 | query = query.slice(0, query.length - 4); // remove last AND 631 | query += '\n'; 632 | 633 | query += '),\n'; 634 | query += 'buffers AS\n'; 635 | query += '(\n'; 636 | query += ' SELECT\n'; 637 | 638 | for (let i = 0; i < intersections.length; i++) { 639 | const item = intersections[i]; 640 | if (item.isAngle == false) continue; 641 | 642 | query += ' intersections.intersection' + item.id + ',\n'; 643 | query += ' ST_ExteriorRing(ST_Buffer(intersections.intersection' + item.id + ', 0.5)) AS ring' + item.id + ',\n'; 644 | } 645 | 646 | if (shapes.length > 0) { 647 | query += ' intersections.distance,\n'; 648 | } 649 | 650 | for (let i = 0; i < lines.length; i++) { 651 | if (intersections.some(a => a.isAngle && [a.line1, a.line2].includes(lines[i].name))) { 652 | // We only need to carry the geometries of lines that intersect at defined angles 653 | query += ' intersections.' + lines[i].name + '_geom,\n'; 654 | } 655 | } 656 | 657 | for (let i = 0; i < pseudoNodes.length; i++) { 658 | if (intersections.some(a => a.isAngle && a.isPseudo && a.node === pseudoNodes[i].name)) { 659 | // We only need to carry the geometries of nodes and shapes that intersect at defined angles 660 | query += ' intersections.' + pseudoNodes[i].name + '_geom,\n'; 661 | } 662 | } 663 | 664 | for (let i = 0; i < annotations.length; i++) { 665 | query += ' intersections.' + annotations[i].name + '_id,\n'; 666 | const comma = (i == annotations.length - 1) ? '' : ',' 667 | query += ' intersections.' + annotations[i].name + '_type' + comma + '\n'; 668 | } 669 | 670 | query += ' FROM intersections\n'; 671 | 672 | // Queries for nodes that are defined distances from intersections 673 | const minDistances = intersections.filter(a => a.isPseudo && a.minDistance != null) 674 | const maxDistances = intersections.filter(a => a.isPseudo && a.maxDistance != null) 675 | 676 | if (minDistances.length > 0 || maxDistances.length > 0) { 677 | query += ' WHERE ' 678 | 679 | for (let i = 0; i < maxDistances.length; i++) { 680 | const item = maxDistances[i]; 681 | query += 'ST_DWithin(intersections.intersection' + item.id + ', intersections.' + item.node + '_geom, ' + item.maxDistance + ') AND '; 682 | } 683 | for (let i = 0; i < minDistances.length; i++) { 684 | const item = minDistances[i]; 685 | query += 'ST_Distance(intersections.intersection' + item.id + ', intersections.' + item.node + '_geom) > ' + item.minDistance + ' AND '; 686 | } 687 | 688 | query = query.slice(0, query.length - 5); // remove last AND 689 | query += '\n'; 690 | } 691 | 692 | query += '),\n'; 693 | 694 | query += 'points AS\n'; 695 | query += '(\n'; 696 | query += ' SELECT\n'; 697 | 698 | for (let i = 0; i < intersections.length; i++) { 699 | const item = intersections[i]; 700 | if (item.isAngle == false) continue; 701 | 702 | query += ' ST_GeometryN\n'; 703 | query += ' (\n'; 704 | query += ' ST_Intersection(buffers.ring' + item.id + ', buffers.' + item.line1 + '_geom)\n'; 705 | query += ' , 1\n'; 706 | query += ' ) AS ring' + item.id + '_p1,\n'; 707 | query += ' ST_GeometryN\n'; 708 | query += ' (\n'; 709 | query += ' ST_Intersection(buffers.ring' + item.id + ', buffers.' + item.line2 + '_geom)\n'; 710 | query += ' , 1\n'; 711 | query += ' ) AS ring' + item.id + '_p2,\n'; 712 | } 713 | 714 | if (shapes.length > 0) { 715 | query += ' buffers.distance,\n'; 716 | } 717 | 718 | for (let i = 0; i < intersections.length; i++) { 719 | const item = intersections[i]; 720 | if (item.isAngle == false) continue; 721 | 722 | query += ' buffers.intersection' + item.id + ',\n'; 723 | query += ' buffers.ring' + item.id + ',\n'; 724 | } 725 | 726 | for (let i = 0; i < lines.length; i++) { 727 | if (intersections.some(a => a.isAngle && [a.line1, a.line2].includes(lines[i].name))) { 728 | // We only need to carry the geometries of lines that intersect at defined angles 729 | query += ' buffers.' + lines[i].name + '_geom,\n'; 730 | } 731 | } 732 | 733 | for (let i = 0; i < pseudoNodes.length; i++) { 734 | if (intersections.some(a => a.isAngle && a.isPseudo && a.node === pseudoNodes[i].name)) { 735 | // We only need to carry the geometries of nodes and shapes that intersect at defined angles 736 | query += ' buffers.' + pseudoNodes[i].name + '_geom,\n'; 737 | } 738 | } 739 | 740 | for (let i = 0; i < annotations.length; i++) { 741 | query += ' buffers.' + annotations[i].name + '_id,\n'; 742 | const comma = (i == annotations.length - 1) ? '' : ',' 743 | query += ' buffers.' + annotations[i].name + '_type' + comma + '\n'; 744 | } 745 | 746 | query += ' FROM buffers\n'; 747 | query += ')'; 748 | 749 | if (shapes.length > 0) { 750 | query += ',\n'; 751 | query += 'IDs AS\n'; 752 | query += '('; 753 | } 754 | 755 | query += '\n'; 756 | query += 'SELECT\n'; 757 | 758 | if (shapes.length > 0) { 759 | query += ' points.distance,\n'; 760 | } 761 | 762 | for (let i = 0; i < annotations.length; i++) { 763 | const comma = (i == annotations.length - 1) ? '' : ',' 764 | let wayPrefix = displayUrls ? 'www.openstreetmap.org/way/' : 'way '; 765 | let nodePrefix = displayUrls ? 'www.openstreetmap.org/node/' : 'node '; 766 | query += ' replace(replace(points.' + annotations[i].name + "_type, 'N', '" + nodePrefix + "'), 'W', '" + wayPrefix + "') || "; 767 | query += 'points.' + annotations[i].name + '_id AS ' + annotations[i].name + '_id' + comma + '\n'; 768 | } 769 | 770 | query += 'FROM points\n'; 771 | query += 'WHERE\n'; 772 | 773 | // Performs final angle comparison between normal intersections with defined angles 774 | for (let i = 0; i < intersections.length; i++) { 775 | const item = intersections[i]; 776 | if (item.isAngle == false) continue; 777 | 778 | if (item.angle != null) { 779 | const error = item.error ? item.error : 0; 780 | 781 | let bounds = calculateBounds(item.angle, error); 782 | 783 | query += '(\n'; 784 | for (let b = 0; b < bounds.lower.length; b++) { 785 | query += ' (\n'; 786 | query += ' abs(round(degrees(\n'; 787 | query += ' ST_Azimuth(points.ring' + item.id + '_p2, points.intersection' + item.id + ')\n'; 788 | query += ' -\n'; 789 | query += ' ST_Azimuth(points.ring' + item.id + '_p1, points.intersection' + item.id + ')\n'; 790 | query += ' ))::decimal % 180.0) '; 791 | 792 | if (bounds.lower[b] === bounds.upper[b]) 793 | query += '= ' + bounds.lower[b] + '\n'; 794 | else 795 | query += 'BETWEEN ' + bounds.lower[b] + ' AND ' + bounds.upper[b] + '\n'; 796 | 797 | query += ' ) ' + ((bounds.lower.length > 1 && b == 0) ? '\n OR' : '') + '\n'; 798 | } 799 | query += ')\n'; 800 | query += 'AND \n'; 801 | } 802 | } 803 | 804 | for (let i = 0; i < intersections.length; i++) { 805 | const item = intersections[i]; 806 | if (item.isAngle == false || item.isPseudo == false) continue; 807 | 808 | if (item.pseudoAngle != null) { 809 | const error = item.pseudoError ? item.pseudoError : 0; 810 | 811 | let bounds = calculateBounds(item.pseudoAngle, error); 812 | 813 | query += '(\n'; 814 | for (let b = 0; b < bounds.lower.length; b++) { 815 | let nodeIsShape = shapes.some((sh) => sh.name === item.node); 816 | 817 | query += ' (\n'; 818 | query += ' abs(round(degrees(\n'; 819 | query += ' ST_Azimuth(points.ring' + item.id + '_p1, points.intersection' + item.id + ')\n'; 820 | query += ' -\n'; 821 | if (nodeIsShape) { 822 | query += ' ST_Azimuth(points.intersection' + item.id + ', ST_Centroid(' + item.node + '_geom))\n'; 823 | } else { 824 | query += ' ST_Azimuth(points.intersection' + item.id + ', ' + item.node + '_geom)\n'; 825 | } 826 | query += ' ))::decimal % 180.0) '; 827 | 828 | if (bounds.lower[b] === bounds.upper[b]) 829 | query += '= ' + bounds.lower[b] + '\n'; 830 | else 831 | query += 'BETWEEN ' + bounds.lower[b] + ' AND ' + bounds.upper[b] + '\n'; 832 | 833 | query += ' ) \n OR\n'; 834 | } 835 | 836 | for (let b = 0; b < bounds.lower.length; b++) { 837 | let nodeIsShape = shapes.some((sh) => sh.name === item.node); 838 | 839 | query += ' (\n'; 840 | query += ' abs(round(degrees(\n'; 841 | query += ' ST_Azimuth(points.ring' + item.id + '_p2, points.intersection' + item.id + ')\n'; 842 | query += ' -\n'; 843 | if (nodeIsShape) { 844 | query += ' ST_Azimuth(points.intersection' + item.id + ', ST_Centroid(' + item.node + '_geom))\n'; 845 | } else { 846 | query += ' ST_Azimuth(points.intersection' + item.id + ', ' + item.node + '_geom)\n'; 847 | } 848 | query += ' ))::decimal % 180.0) '; 849 | 850 | if (bounds.lower[b] === bounds.upper[b]) 851 | query += '= ' + bounds.lower[b] + '\n'; 852 | else 853 | query += 'BETWEEN ' + bounds.lower[b] + ' AND ' + bounds.upper[b] + '\n'; 854 | 855 | query += ' ) ' + ((bounds.lower.length > 1 && b == 0) ? '\n OR' : '') + '\n'; 856 | } 857 | query += ')\n'; 858 | query += 'AND \n'; 859 | } 860 | } 861 | 862 | query = query.slice(0, query.length - 6); // remove last AND 863 | 864 | if (shapes.length > 0) { 865 | query += '),\n'; 866 | query += 'numbered AS\n'; 867 | query += '(\n'; 868 | query += ' SELECT\n'; 869 | 870 | for (let i = 0; i < annotations.length; i++) { 871 | query += ' IDs.' + annotations[i].name + '_id,\n'; 872 | } 873 | 874 | query += ' COUNT(*) OVER () AS count, ROW_NUMBER() OVER (ORDER BY IDs.distance) AS row\n'; 875 | 876 | query += ' FROM IDs\n'; 877 | query += ')\n'; 878 | query += 'SELECT\n'; 879 | 880 | for (let i = 0; i < annotations.length; i++) { 881 | const comma = (i == annotations.length - 1) ? '' : ',' 882 | query += ' numbered.' + annotations[i].name + '_id' + comma + '\n'; 883 | } 884 | 885 | query += 'FROM numbered\n'; 886 | query += 'WHERE numbered.row < GREATEST((numbered.count * 0.01), 1000)\n'; 887 | query += 'ORDER BY numbered.row ASC'; 888 | } 889 | 890 | query += ';\n' 891 | 892 | return query; 893 | } 894 | 895 | function constructQuery(annotations, displayUrls = true) { 896 | // Uncomment to print the details of annotations, which can be used by the unit testing script. 897 | // console.log(JSON.stringify(annotations)); 898 | 899 | let nodes = []; 900 | let lines = []; 901 | let shapes = []; 902 | 903 | for (let i = 0; i < annotations.length; i++) { 904 | if (annotations[i].geometryType == 'linestring') { 905 | lines.push(annotations[i]); 906 | } else if (annotations[i].geometryType == 'node') { 907 | nodes.push(annotations[i]); 908 | } else if (annotations[i].geometryType == 'shape') { 909 | shapes.push(annotations[i]); 910 | } 911 | } 912 | 913 | if (annotations.length == 1) { 914 | return constructSingleObjectQuery(annotations, shapes, displayUrls); 915 | } 916 | 917 | // If at least two of the lines intersect, then call constructIntersectingQuery 918 | for (let i = 0; i < lines.length; i++) { 919 | for (let k = 0; k < lines.length; k++) { 920 | if (i == k) continue; 921 | 922 | const intersection = calculateIntersection(lines[i].points, lines[k].points); 923 | if (intersection && intersection.intersects) { 924 | return constructIntersectingQuery(annotations, nodes, lines, shapes, displayUrls); 925 | } 926 | } 927 | } 928 | 929 | return constructDisjointQuery(annotations, lines, shapes, displayUrls); 930 | } 931 | 932 | export {constructQuery} -------------------------------------------------------------------------------- /src/assets/queryTools.js: -------------------------------------------------------------------------------- 1 | 2 | // Returns a partial query that filters by maximum distance 3 | function createMaxDistanceQuery(annotations) { 4 | let query = ''; 5 | for (let i = 0; i < annotations.length; i++) { 6 | const keys = Object.keys(annotations[i].relations); 7 | for (let k = 0; k < keys.length; k++) { 8 | const relation = annotations[i].relations[keys[k]]; 9 | const dist = parseInt(relation.maxDistance); 10 | if (dist && !keys[k].includes('&')) 11 | query += 'ST_DWithin(' + annotations[i].name + '.geom, ' + keys[k] + '.geom, ' + dist + ') AND '; 12 | } 13 | } 14 | return query; 15 | } 16 | 17 | // Returns a partial query that filters by minimum distance 18 | function createMinDistanceQuery(annotations) { 19 | let query = ''; 20 | for (let i = 0; i < annotations.length; i++) { 21 | const keys = Object.keys(annotations[i].relations); 22 | for (let k = 0; k < keys.length; k++) { 23 | const relation = annotations[i].relations[keys[k]]; 24 | const dist = parseInt(relation.minDistance); 25 | if (dist && !keys[k].includes('&')) 26 | query += 'ST_Distance(' + annotations[i].name + '.geom, ' + keys[k] + '.geom' + ') > ' + dist + ' AND '; 27 | } 28 | } 29 | return query; 30 | } 31 | 32 | // Returns a partial query that filters out overlapping comparisons 33 | function createNoOverlappingQuery(annotations) { 34 | let identicalTypes = {}; 35 | 36 | for (let i = 0; i < annotations.length; i++) { 37 | let category = annotations[i].category; 38 | let subcategory = annotations[i].subcategory; 39 | let key = category + ' ' + subcategory; 40 | 41 | if (identicalTypes[key] == undefined) { 42 | identicalTypes[key] = [annotations[i].name]; 43 | } else { 44 | identicalTypes[key].push(annotations[i].name); 45 | } 46 | } 47 | 48 | let query = ''; 49 | let keys = Object.keys(identicalTypes); 50 | for (let i = 0; i < keys.length; i++) { 51 | let primaryRemaining = identicalTypes[keys[i]].slice(0); 52 | let secondaryRemaining = []; 53 | let current1 = null; 54 | let current2 = null; 55 | 56 | while (primaryRemaining.length > 1) { 57 | if (secondaryRemaining.length > 0) { 58 | current2 = secondaryRemaining.pop(); 59 | } else { 60 | current1 = primaryRemaining.pop(); 61 | secondaryRemaining = primaryRemaining.slice(0); 62 | current2 = secondaryRemaining.pop(); 63 | } 64 | query += current1 + '.osm_id != ' + current2 + '.osm_id AND '; 65 | } 66 | } 67 | return query; 68 | } 69 | 70 | // Returns a partial query that filters by OSM tags 71 | function createTagsQuery(ann) { 72 | let query = ''; 73 | for (var j = 0; j < ann.tags.length; j++) { 74 | // Splits the tag into a key and value. Also escapes single 75 | // quotes so PostgreSQL doesn't get confused. 76 | let tag = ann.tags[j].replaceAll("'", "''").split('='); 77 | 78 | if (tag.length == 1) { 79 | query += ann.name + '.tags ? \'' + tag[0] + '\' '; 80 | } else if (tag.length == 2) { 81 | query += ann.name + '.tags->>\'' + tag[0] + '\' '; 82 | query += '= \'' + tag[1] + '\' '; 83 | } 84 | 85 | query += 'AND '; 86 | } 87 | return query; 88 | } 89 | 90 | // Calculates the lower and upper bounds given an angle and error, 91 | // assuming maximum angle is 180 and minimum angle is 0. 92 | function calculateBounds(angle, error) { 93 | angle = Math.abs(angle); 94 | error = Math.abs(error); 95 | let lowerBounds = [angle - error]; 96 | let upperBounds = [angle + error]; 97 | 98 | if (upperBounds[0] > 180) { 99 | if (lowerBounds[0] > 180) { 100 | lowerBounds[0] %= 180; 101 | upperBounds[0] %= 180; 102 | } else if (lowerBounds[0] < 0) { 103 | lowerBounds[0] = 0; 104 | upperBounds[0] = 180; 105 | } else { 106 | const up = upperBounds[0]; 107 | upperBounds[0] = 180; 108 | lowerBounds[1] = 0; 109 | upperBounds[1] = up % 180; 110 | } 111 | } else if (lowerBounds[0] < 0) { 112 | lowerBounds[1] = 180 + lowerBounds[0]; 113 | lowerBounds[0] = 0; 114 | upperBounds[1] = 180; 115 | } 116 | 117 | if (lowerBounds.length == 1 && (upperBounds[0] < 90 || lowerBounds[0] > 90)) { 118 | lowerBounds[1] = 180 - upperBounds[0]; 119 | upperBounds[1] = 180 - lowerBounds[0]; 120 | } 121 | 122 | return {lower: lowerBounds, upper: upperBounds}; 123 | } 124 | 125 | function calculateHuMoments(nodes) { 126 | let maxX = 29; 127 | let maxY = 29; 128 | 129 | function dist2(v, w) { 130 | return Math.pow(v.x - w.x, 2) + Math.pow(v.y - w.y, 2) 131 | } 132 | 133 | function distToSegmentSquared(p, v, w) { 134 | var l2 = dist2(v, w); 135 | if (l2 == 0) { 136 | return dist2(p, v); 137 | } 138 | var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; 139 | t = Math.max(0, Math.min(1, t)); 140 | 141 | return dist2(p, { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) }); 142 | } 143 | 144 | // Calculates shortest distance between point and a line segment 145 | // p: point whose distance to line segment will be measured 146 | // v: point at one end of line segment 147 | // w: point at other end of line segment 148 | // https://stackoverflow.com/a/1501725/1941353 149 | function distToSegment(p, v, w) { 150 | return Math.sqrt(distToSegmentSquared(p, v, w)); 151 | } 152 | 153 | // Determines binary value at given coordinates 154 | function calculateI(nodes, x, y) { 155 | let p = {x: x, y: y}; 156 | 157 | for (let i = 0; i < nodes.length - 1; i++) { 158 | let segmentP1 = {x: nodes[i][0], y: nodes[i][1]}; 159 | let segmentP2 = {x: nodes[i + 1][0], y: nodes[i + 1][1]}; 160 | 161 | if (distToSegment(p, segmentP1, segmentP2) <= 0.5) { 162 | return 1; 163 | } 164 | } 165 | 166 | return 0; 167 | } 168 | 169 | function calculateM(nodes, p, q) { 170 | let m = 0; 171 | 172 | for (let x = 0; x <= maxX; x++) { 173 | for (let y = 0; y <= maxY; y++) { 174 | m += (Math.pow(x, p) * Math.pow(y, q) * calculateI(nodes, x, y)); 175 | } 176 | } 177 | 178 | return m; 179 | } 180 | 181 | // Greek letter mu 182 | function calculateMu(nodes, centroidX, centroidY, p, q) { 183 | let mu = 0; 184 | 185 | for (let x = 0; x <= maxX; x++) { 186 | for (let y = 0; y <= maxY; y++) { 187 | mu += (Math.pow((x - centroidX), p) * Math.pow((y - centroidY), q) * calculateI(nodes, x, y)); 188 | } 189 | } 190 | 191 | return mu; 192 | } 193 | 194 | // Greek letter eta 195 | function calculateEta(nodes, centroidX, centroidY, muDenominator, p, q) { 196 | let denominator = Math.pow(muDenominator, (1 + (p + q) / 2)); 197 | let numerator = calculateMu(nodes, centroidX, centroidY, p, q); 198 | return numerator / denominator; 199 | } 200 | 201 | let maxCoords = [nodes[0][0], nodes[0][1]]; 202 | let minCoords = [nodes[0][0], nodes[0][1]]; 203 | for(let i = 0; i < nodes.length; i++) { 204 | if (nodes[i][0] > maxCoords[0]) { 205 | maxCoords[0] = nodes[i][0]; 206 | } else if (nodes[i][0] < minCoords[0]) { 207 | minCoords[0] = nodes[i][0]; 208 | } 209 | 210 | if (nodes[i][1] > maxCoords[1]) { 211 | maxCoords[1] = nodes[i][1]; 212 | } else if (nodes[i][1] < minCoords[1]) { 213 | minCoords[1] = nodes[i][1]; 214 | } 215 | } 216 | 217 | let xRange = maxCoords[0] - minCoords[0]; 218 | let yRange = maxCoords[1] - minCoords[1]; 219 | 220 | let xRatio = maxX / xRange; 221 | let yRatio = maxY / yRange; 222 | let scale = Math.min(xRatio, yRatio); 223 | 224 | for(let i = 0; i < nodes.length; i++) { 225 | nodes[i][0] -= minCoords[0]; 226 | nodes[i][1] -= minCoords[1]; 227 | 228 | nodes[i][0] *= scale; 229 | nodes[i][1] *= scale; 230 | } 231 | 232 | let h1 = 0, h2 = 0, h3 = 0, h4 = 0, h5 = 0, h6 = 0, h7 = 0; 233 | let mDenominator = calculateM(nodes, 0, 0); 234 | 235 | if (mDenominator != 0) { 236 | const centroidX = calculateM(nodes, 1, 0) / mDenominator; 237 | const centroidY = calculateM(nodes, 0, 1) / mDenominator; 238 | 239 | // Slight hack: effectively, both muDenominator and mDenominator would return the same 240 | // results for the values p=0 and q=0, so instead of making a second function call, 241 | // we just re-use the result. 242 | let muDenominator = mDenominator; 243 | 244 | let eta20 = calculateEta(nodes, centroidX, centroidY, muDenominator, 2, 0); 245 | let eta02 = calculateEta(nodes, centroidX, centroidY, muDenominator, 0, 2); 246 | let eta11 = calculateEta(nodes, centroidX, centroidY, muDenominator, 1, 1); 247 | let eta30 = calculateEta(nodes, centroidX, centroidY, muDenominator, 3, 0); 248 | let eta12 = calculateEta(nodes, centroidX, centroidY, muDenominator, 1, 2); 249 | let eta03 = calculateEta(nodes, centroidX, centroidY, muDenominator, 0, 3); 250 | let eta21 = calculateEta(nodes, centroidX, centroidY, muDenominator, 2, 1); 251 | 252 | h1 = eta20 + eta02; 253 | 254 | h2 = Math.pow( 255 | (eta20 - eta02) 256 | , 2 257 | ) + 258 | 4 * Math.pow( 259 | eta11 260 | , 2 261 | ); 262 | 263 | h3 = Math.pow( 264 | (eta30 - 3 * eta12) 265 | , 2 266 | ) + 267 | Math.pow( 268 | (3 * eta21 - eta03) 269 | , 2 270 | ); 271 | 272 | h4 = Math.pow( 273 | (eta30 + eta12) 274 | , 2 275 | ) + 276 | Math.pow( 277 | (eta21 + eta03) 278 | , 2 279 | ); 280 | 281 | h5 = (eta30 - 3 * eta12) * 282 | (eta30 + eta12) * 283 | ( 284 | Math.pow( 285 | (eta30 + eta12) 286 | , 2 287 | ) - 288 | 3 * Math.pow( 289 | (eta21 + eta03) 290 | , 2 291 | ) 292 | ) + 293 | (3 * eta21 - eta03) * 294 | (eta21 + eta03) * 295 | ( 296 | 3 * Math.pow( 297 | (eta30 + eta12) 298 | , 2 299 | ) - 300 | Math.pow( 301 | (eta21 + eta03) 302 | , 2 303 | ) 304 | ); 305 | 306 | h6 = (eta20 - eta02) * 307 | ( 308 | Math.pow( 309 | (eta30 + eta12) 310 | , 2 311 | ) - 312 | Math.pow( 313 | (eta21 + eta03) 314 | , 2 315 | ) 316 | ) + 317 | 4 * eta11 * 318 | (eta30 + eta12) * 319 | (eta21 + eta03); 320 | 321 | h7 = (3 * eta21 - eta03) * 322 | (eta30 + eta12) * 323 | ( 324 | Math.pow( 325 | (eta30 + eta12) 326 | , 2 327 | ) - 328 | 3 * Math.pow( 329 | (eta21 + eta03) 330 | , 2 331 | ) 332 | ) - 333 | (eta30 - 3 * eta12) * 334 | (eta21 + eta03) * 335 | ( 336 | 3 * Math.pow( 337 | (eta30 + eta12) 338 | , 2 339 | ) - 340 | Math.pow( 341 | (eta21 + eta03) 342 | , 2 343 | ) 344 | ); 345 | } 346 | 347 | let moments = [h1, h2, h3, h4, h5, h6, h7]; 348 | 349 | // Prevents moments being displayed in scientific notation 350 | // Also removes any trailing 0s at the end 351 | moments = moments.map((m) => { 352 | let temp = m.toFixed(30); 353 | let i = temp.length - 1; 354 | for (i; i >= 0; i--) { 355 | if (temp.charAt(i) !== '0') break; 356 | } 357 | if (temp.charAt(i) === '.') i--; 358 | return temp.slice(0, i + 1); 359 | }); 360 | return moments; 361 | } 362 | 363 | export {createMaxDistanceQuery, createMinDistanceQuery, createNoOverlappingQuery, createTagsQuery, calculateBounds, calculateHuMoments} -------------------------------------------------------------------------------- /src/components/DrawBar.vue: -------------------------------------------------------------------------------- 1 | 143 | 144 | 201 | 202 | -------------------------------------------------------------------------------- /src/components/InputBar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 46 | 47 | 65 | 66 | -------------------------------------------------------------------------------- /src/components/InteractiveCanvas.vue: -------------------------------------------------------------------------------- 1 | 517 | 518 | 540 | 541 | 544 | -------------------------------------------------------------------------------- /src/components/PropertiesBar.vue: -------------------------------------------------------------------------------- 1 | 271 | 272 | 351 | 352 | 372 | -------------------------------------------------------------------------------- /src/components/QueryPage.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 75 | 76 | 89 | -------------------------------------------------------------------------------- /src/components/RelationsBar.vue: -------------------------------------------------------------------------------- 1 | 274 | 275 | 318 | 319 | 324 | -------------------------------------------------------------------------------- /src/components/UploadBar.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 88 | 89 | 94 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import VueKonva from 'vue-konva'; 4 | import { Quasar } from 'quasar'; 5 | 6 | // Import icon libraries 7 | import '@quasar/extras/material-icons/material-icons.css' 8 | // Import Quasar css 9 | import 'quasar/dist/quasar.css' 10 | 11 | import './assets/main.css' 12 | 13 | const app = createApp(App); 14 | app.use(VueKonva); 15 | app.use(Quasar, { 16 | config: { 17 | brand: { 18 | primary: '#124559', 19 | // secondary: '#ececec', 20 | // accent: '#9C27B0', 21 | // 'dark-page': '#121212', 22 | dark: '#333', 23 | // positive: '#21BA45', 24 | // negative: '#C10015', 25 | // info: '#31CCEC', 26 | // warning: '#F2C037' 27 | } 28 | } 29 | }); 30 | app.mount('#app'); 31 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import { quasar } from '@quasar/vite-plugin' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [vue(), quasar()], 10 | resolve: { 11 | alias: { 12 | '@': fileURLToPath(new URL('./src', import.meta.url)) 13 | } 14 | }, 15 | }) 16 | --------------------------------------------------------------------------------