Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search API #779

Merged
merged 21 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0795c8e
Rewrite window.search.Query as an ES6 class
modos189 Nov 1, 2024
7971c99
Splitting search.Query into separate functions
modos189 Nov 1, 2024
e8831dc
Search UI in a separate class
modos189 Nov 1, 2024
7c08c69
Moved event handling for search result interactions from the UI class…
modos189 Nov 1, 2024
4e08080
Moving window.search to IITC.search namespace with backward compatibi…
modos189 Nov 1, 2024
0daf8eb
Moving functions that respond to the "search" hook to a separate file
modos189 Nov 2, 2024
78d7a5b
Moving IITC.search.Query to a separate file `search_query.js`
modos189 Nov 2, 2024
96413b9
Rename IITC.search.UI to IITC.search.QueryResultsView and moving to a…
modos189 Nov 2, 2024
3e62361
Refactoring of IITC.search
modos189 Nov 2, 2024
8133318
Refactoring of IITC.search.QueryResultsView
modos189 Nov 3, 2024
a617cfa
The geolocation button is always placed in the search field
modos189 Nov 3, 2024
d18dc82
Added an accordion to expand and collapse search results without jQuery
modos189 Nov 3, 2024
d1540d7
Updated current geolocation icon, added search icon next to the searc…
modos189 Nov 3, 2024
20d30e4
Added an icon to cancel a search
modos189 Nov 3, 2024
f15662f
Move IITC.search.addSearchResult to IITC.search.Query.addPortalResult
modos189 Nov 3, 2024
e949280
Update jsdoc for search
modos189 Nov 3, 2024
7d49165
Refactoring of search_hooks.js
modos189 Nov 3, 2024
ba248b0
Search supports input in both decimal format (e.g., 51.5074, -0.1278)…
modos189 Nov 3, 2024
ee5a5f3
Methods of the search class that are not explicitly intended to be us…
modos189 Nov 4, 2024
d382e37
Fix for keyboard selection of search results
modos189 Nov 4, 2024
44c0d20
Tests for IITC.search.Query
modos189 Nov 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
564 changes: 93 additions & 471 deletions core/code/search.js

Large diffs are not rendered by default.

200 changes: 200 additions & 0 deletions core/code/search_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* global L -- eslint */

/**
* Handles search-related hooks for the IITC.search module, adding various search result types.
*
* These functions supply default search results to the IITC search system by responding to `search` hooks with
* data for portals, geographic coordinates, OpenStreetMap locations, and portal GUIDs.
*
* @namespace hooks
* @memberof IITC.search
*/

/**
* Searches for portals by matching the query term against portal titles and adds matched results.
*
* @param {Object} query - The search query object.
* @fires hook#search
*/
window.addHook('search', (query) => {
const term = query.term.toLowerCase();

for (const [guid, portal] of Object.entries(window.portals)) {
const data = portal.options.data;
if (!data.title) continue;

if (data.title.toLowerCase().includes(term)) {
window.search.addSearchResult(query, data, guid);
}
}
});

/**
* Searches for geographical coordinates formatted as latitude, longitude and adds the results.
* Supports both decimal format (e.g., 51.5074, -0.1278) and DMS format (e.g., 50°31'03.8"N 7°59'05.3"E).
*
* @param {Object} query - The search query object.
* @fires hook#search
*/
window.addHook('search', (query) => {
const added = new Set();

// Regular expression for decimal coordinates
const decimalRegex = /[+-]?\d+\.\d+, ?[+-]?\d+\.\d+/g;
// Regular expression for DMS coordinates
const dmsRegex = /(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)?"\s*([NS]),?\s*(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)?"\s*([EW])/g;

// Convert DMS to decimal format
const parseDMS = (deg, min, sec, dir) => {
const decimal = parseFloat(deg) + parseFloat(min) / 60 + parseFloat(sec) / 3600;
return dir === 'S' || dir === 'W' ? -decimal : decimal;
};

// Universal function for adding search result
const addResult = (lat, lng) => {
const latLngString = `${lat.toFixed(6)},${lng.toFixed(6)}`;
if (added.has(latLngString)) return;
added.add(latLngString);

query.addResult({
title: latLngString,
description: 'geo coordinates',
position: L.latLng(lat, lng),
onSelected: (result) => {
for (const [guid, portal] of Object.entries(window.portals)) {
const { lat: pLat, lng: pLng } = portal.getLatLng();
if (`${pLat.toFixed(6)},${pLng.toFixed(6)}` === latLngString) {
window.renderPortalDetails(guid);
return;
}
}
window.urlPortalLL = [result.position.lat, result.position.lng];
},
});
};

// Search and process decimal coordinates
const decimalMatches = query.term.replace(/%2C/gi, ',').match(decimalRegex);
if (decimalMatches) {
decimalMatches.forEach((location) => {
const [lat, lng] = location.split(',').map(Number);
addResult(lat, lng);
});
}

// Search and process DMS coordinates
const dmsMatches = Array.from(query.term.matchAll(dmsRegex));
dmsMatches.forEach((match) => {
const lat = parseDMS(match[1], match[2], match[3], match[4]);
const lng = parseDMS(match[5], match[6], match[7], match[8]);
addResult(lat, lng);
});
});

/**
* Searches for results on OpenStreetMap based on the query term, considering map view boundaries.
*
* @param {Object} query - The search query object.
* @fires hook#search
*/
window.addHook('search', async (query) => {
if (!query.confirmed) return;

const mapBounds = window.map.getBounds();
const viewbox = `&viewbox=${mapBounds.getSouthWest().lng},${mapBounds.getSouthWest().lat},${mapBounds.getNorthEast().lng},${mapBounds.getNorthEast().lat}`;
// Bounded search allows amenity-only searches (e.g. "amenity=toilet") via special phrases
// https://wiki.openstreetmap.org/wiki/Nominatim/Special_Phrases/EN
const bounded = '&bounded=1';

const resultMap = new Set();
let resultCount = 0;

async function fetchResults(isViewboxResult) {
try {
const response = await fetch(`${window.NOMINATIM}${encodeURIComponent(query.term)}${isViewboxResult ? viewbox + bounded : viewbox}`);
const data = await response.json();

if (isViewboxResult && data.length === 0) {
// If no results found within the viewbox, try a broader search
await fetchResults(false);
return;
} else if (!isViewboxResult && resultCount === 0 && data.length === 0) {
// If no results at all
query.addResult({
title: 'No results on OpenStreetMap',
icon: '//www.openstreetmap.org/favicon.ico',
onSelected: () => true,
});
return;
}

resultCount += data.length;

data.forEach((item) => {
if (resultMap.has(item.place_id)) return; // duplicate
resultMap.add(item.place_id);

const result = {
title: item.display_name,
description: `Type: ${item.type}`,
position: L.latLng(parseFloat(item.lat), parseFloat(item.lon)),
icon: item.icon,
};

if (item.geojson) {
result.layer = L.geoJson(item.geojson, {
interactive: false,
color: 'red',
opacity: 0.7,
weight: 2,
fill: false,
pointToLayer: (featureData, latLng) =>
L.marker(latLng, {
icon: L.divIcon.coloredSvg('red'),
title: item.display_name,
}),
});
}

if (item.boundingbox) {
const [south, north, west, east] = item.boundingbox;
result.bounds = new L.LatLngBounds(L.latLng(parseFloat(south), parseFloat(west)), L.latLng(parseFloat(north), parseFloat(east)));
}

query.addResult(result);
});
} catch (error) {
console.error('Error fetching OSM data:', error);
}
}

// Start with viewbox-bounded search
await fetchResults(true);
});

/**
* Searches by GUID in the query term.
*
* @param {Object} query - The search query object.
* @fires hook#search
*/
window.addHook('search', async (query) => {
const guidRegex = /[0-9a-f]{32}\.[0-9a-f]{2}/;
const match = query.term.match(guidRegex);

if (match) {
const guid = match[0];
const data = window.portalDetail.get(guid);

if (data) {
window.search.addSearchResult(query, data, guid);
} else {
try {
const fetchedData = await window.portalDetail.request(guid);
window.search.addSearchResult(query, fetchedData, guid);
} catch (error) {
console.error('Error fetching portal details:', error);
}
}
}
});
Loading
Loading