diff --git a/README.md b/README.md index bd2f80e8..9b16faa0 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ network so that any user with a laptop/tablet/phone can connect and use the libr | Web Archives | yes | yes | yes | [Singlefile](https://github.com/gildas-lormeau/SingleFile) | | Wikipedia | yes | yes | yes | [Kiwix](https://www.kiwix.org) | | eBooks | yes | yes | planned | EPUB/PDF | -| Map | yes | planned | yes | [Protomaps](https://protomaps.com/) | +| Map | yes | yes | yes | [Protomaps](https://protomaps.com/) | | Podcasts/Audio | yes | planned | planned | | # Demo diff --git a/app/src/Events.js b/app/src/Events.js index fc9990bf..be4b6795 100644 --- a/app/src/Events.js +++ b/app/src/Events.js @@ -54,6 +54,14 @@ function handleEvents(events) { eventToast('Map import failed', message, 'error'); } + if (event === 'map_search_complete') { + eventToast('Map Search Index', message, 'success'); + } + + if (event === 'map_search_failed') { + eventToast('Map Search Index Failed', message, 'error'); + } + if (event === 'user_notify_message') { console.log(message, url); eventToast( diff --git a/app/src/api.js b/app/src/api.js index a0e87203..cb795d77 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -1580,6 +1580,44 @@ export async function setMapDefaultLocation(lat, lon, zoom) { } } +export async function searchMap(query, limit = 12, offset = 0, lat = null, lon = null) { + let url = `${API_URI}/map/search?q=${encodeURIComponent(query)}&limit=${limit}&offset=${offset}`; + if (lat !== null && lon !== null) { + url += `&lat=${lat}&lon=${lon}`; + } + const response = await apiGet(url); + if (!response.ok) { + return {results: [], total: 0}; + } + return await response.json(); +} + +export async function getMapSearchStatus() { + const response = await apiGet(`${API_URI}/map/search/status`); + if (!response.ok) { + return {indexed: [], missing: []}; + } + return await response.json(); +} + +export async function searchEstimateMap(search_str) { + if (!search_str) return {mapPlaces: 0}; + const response = await apiGet(`${API_URI}/map/search/estimate?q=${encodeURIComponent(search_str)}`); + if (!response.ok) return {mapPlaces: 0}; + const data = await response.json(); + return {mapPlaces: data.map_places || 0}; +} + +export async function rebuildMapSearchIndex(filename) { + const response = await apiPost(`${API_URI}/map/search/rebuild/${encodeURIComponent(filename)}`); + if (!response.ok) { + const message = await getErrorMessage(response, 'Could not rebuild search index'); + toast({type: 'error', title: 'Map Error', description: message, time: 5000}); + } else { + toast({type: 'success', title: 'Search Index', description: 'Search index build started', time: 3000}); + } +} + export async function clearCompletedDownloads() { const response = await apiPost(`${API_URI}/download/clear_completed`); if (!response.ok) { diff --git a/app/src/components/Map.js b/app/src/components/Map.js index 20b02f18..8bbc10de 100644 --- a/app/src/components/Map.js +++ b/app/src/components/Map.js @@ -13,7 +13,7 @@ import { import {Route, Routes, useNavigate} from "react-router"; import { deleteMapFile, deleteMapPin, fetchMapSubscriptions, getMapFiles, getMapPins, - mapSubscribe, mapUnsubscribe, updateMapPin + mapSubscribe, mapUnsubscribe, rebuildMapSearchIndex, updateMapPin } from "../api"; import { Button, @@ -25,7 +25,7 @@ import { import {SortableTable} from "./SortableTable"; import Message from "semantic-ui-react/dist/commonjs/collections/Message"; import {Header} from "./Theme"; -import {Media} from "../contexts/contexts"; +import {Media, StatusContext} from "../contexts/contexts"; import {MAP_VIEWER_URI} from "./Vars"; import {Modal} from "./Theme"; import MapViewer from "./MapViewer"; @@ -265,6 +265,8 @@ function ManageMap() { const [catalog, setCatalog] = useState([]); const [subscriptions, setSubscriptions] = useState([]); const [allRegionsOpen, setAllRegionsOpen] = useState(false); + const {status} = React.useContext(StatusContext); + const searchBuilding = status?.flags?.map_search_building; const fetchFiles = async () => { try { @@ -306,13 +308,31 @@ function ManageMap() { const fileHeaders = [ {key: 'name', text: 'File', sortBy: i => i.name.toLowerCase()}, {key: 'size', text: 'Size', sortBy: i => i.size || 0}, + {key: 'search', text: 'Search', sortBy: null}, {key: 'actions', text: '', sortBy: null}, ]; + const handleBuildSearch = async (filename) => { + await rebuildMapSearchIndex(filename); + await fetchFiles(); + }; + const fileRowFunc = (file) => { return {file.name} {humanFileSize(file.size)} + + {file.name.startsWith('terrain') + ? null + : file.has_search_index + ?