├── .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 |
114 |
115 |
116 |
117 | OSM Finder
118 |
119 |
120 |
121 |
122 |
123 |
124 | View source code
125 |
126 |
127 |
128 |
129 |
130 | View instructions
131 |
132 |
133 |
134 |
135 |
139 |
140 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | {{warningMessage}}
152 |
153 |
154 |
155 |
156 |
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 |
145 |
146 |
147 |
148 |
149 | Click and drag to draw a line.
150 |
151 |
152 |
153 |
154 | Click once on the canvas to place a node.
155 |
156 |
157 |
158 |
159 | Draw the outline of a uniquely shaped map item.
160 |
161 |
162 |
163 |
164 |
165 |
167 |
168 | Toggle between subtractive or additive mode.
169 |
170 |
171 |
172 |
173 | Click and drag to draw a rectangle.
174 |
175 |
176 |
177 |
178 | Click and drag to draw a circle.
179 |
180 |
181 |
182 |
183 | Click to drop points around the outline of the polygonal shape.
184 |
185 |
186 |
187 |
188 | Click to complete the shape.
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
--------------------------------------------------------------------------------
/src/components/InputBar.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
46 |
47 |
48 |
49 |
51 |
52 |
55 |
56 |
59 |
60 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/components/InteractiveCanvas.vue:
--------------------------------------------------------------------------------
1 |
517 |
518 |
519 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
544 |
--------------------------------------------------------------------------------
/src/components/PropertiesBar.vue:
--------------------------------------------------------------------------------
1 |
271 |
272 |
273 | Properties for {{currentAnn.name}}
274 |
350 |
351 |
352 |
372 |
--------------------------------------------------------------------------------
/src/components/QueryPage.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 |
45 |
46 | Run this query in your PostgreSQL terminal.
47 |
48 |
57 |
58 | Copy Query
59 |
60 |
61 | Close
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
{{ getQuery(displayUrls) }}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
89 |
--------------------------------------------------------------------------------
/src/components/RelationsBar.vue:
--------------------------------------------------------------------------------
1 |
274 |
275 |
276 | Relationship between {{current1.name}} and {{getReadableName(current2)}}
277 |
278 |
279 |
284 |
285 | Minimum distance between the objects in metres (optional)
286 |
287 |
288 |
293 |
294 | Maximum distance between the objects in metres (strongly recommended)
295 |
296 |
297 |
302 |
303 | Angle between the objects (optional)
304 |
305 |
306 |
311 |
312 | Estimated error of the angle between the objects (optional)
313 |
314 |
315 |
316 |
317 |
318 |
319 |
324 |
--------------------------------------------------------------------------------
/src/components/UploadBar.vue:
--------------------------------------------------------------------------------
1 |
65 |
66 |
67 |
68 |
69 |
70 | Click here or drag and drop to upload an image.
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | Drag and drop to upload an image.
83 |
84 |
85 |
86 |
87 |
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 |
--------------------------------------------------------------------------------