diff --git a/src/js/compare-landmark.js b/src/js/compare-landmark.js index 32564717..7c7b79f7 100644 --- a/src/js/compare-landmark.js +++ b/src/js/compare-landmark.js @@ -133,7 +133,7 @@ class compareLandmark { */ #generateGeoJson() { let id = -1; - if (this.compareLandmarkId !== null && this.compareLandmarkId >= 0) { + if (this.compareLandmarkId !== null && (this.compareLandmarkId >= 0 || typeof this.compareLandmarkId === "string")) { id = this.compareLandmarkId; } return { diff --git a/src/js/compare-poi/compare-poi.js b/src/js/compare-poi/compare-poi.js index 1922ff75..f42877f1 100644 --- a/src/js/compare-poi/compare-poi.js +++ b/src/js/compare-poi/compare-poi.js @@ -128,7 +128,7 @@ class ComparePoi { setData(comparePoi) { this.dom.advancedBtn.classList.add("d-none"); this.comparePoiId = -1; - if (comparePoi.id !== null && comparePoi.id >= 0) { + if (comparePoi.id !== null && (comparePoi.id >= 0 || typeof comparePoi.id === "string")) { this.comparePoiId = comparePoi.id; } this.compareConfig = { @@ -207,6 +207,7 @@ class ComparePoi { const comparePoi = this.map.queryRenderedFeatures(e.point, {layers: layers})[0]; comparePoi.properties.opacity = 0.6; comparePoi.properties.radiusRatio = 0; + comparePoi.id = comparePoi.properties.id; const source = this.map.getSource("selected-compare-poi"); source.setData({ "type": "FeatureCollection", diff --git a/src/js/index.js b/src/js/index.js index 0e263dc2..cf85f87e 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -27,7 +27,6 @@ import { SafeAreaController } from "@aashu-dubey/capacitor-statusbar-safe-area"; import { TextZoom } from "@capacitor/text-zoom"; import { Device } from "@capacitor/device"; import { App } from "@capacitor/app"; -import { Preferences } from "@capacitor/preferences"; import { Toast } from "@capacitor/toast"; import { Protocol } from "pmtiles"; @@ -46,35 +45,6 @@ defineCustomElements(window); * Fonction définissant l'application */ function app() { - // REMOVEME : rétrocompatibilité des itinéraires / PR / PR comparer : migration du localStorage vers Preferences - if (localStorage.getItem("savedRoutes")) { - Preferences.set({ - key: "savedRoutes", - value: localStorage.getItem("savedRoutes"), - }).then( () => { - localStorage.removeItem("savedRoutes"); - }); - } - - if (localStorage.getItem("savedLandmarks")) { - Preferences.set({ - key: "savedLandmarks", - value: localStorage.getItem("savedLandmarks"), - }).then( () => { - localStorage.removeItem("savedLandmarks"); - }); - } - - if (localStorage.getItem("savedCompareLandmarks")) { - Preferences.set({ - key: "savedCompareLandmarks", - value: localStorage.getItem("savedCompareLandmarks"), - }).then( () => { - localStorage.removeItem("savedCompareLandmarks"); - }); - } - // END REMOVEME - // Ajout du protocole PM Tiles let protocol = new Protocol(); maplibregl.addProtocol("pmtiles", protocol.tile); diff --git a/src/js/landmark.js b/src/js/landmark.js index 5f5e6802..01334917 100644 --- a/src/js/landmark.js +++ b/src/js/landmark.js @@ -130,7 +130,7 @@ class Landmark { */ #generateGeoJson() { let id = -1; - if (this.landmarkId !== null && this.landmarkId >= 0) { + if (this.landmarkId !== null && (this.landmarkId >= 0 || typeof this.landmarkId === "string")) { id = this.landmarkId; } return { diff --git a/src/js/my-account/my-account.js b/src/js/my-account/my-account.js index 18a8ae12..24440062 100644 --- a/src/js/my-account/my-account.js +++ b/src/js/my-account/my-account.js @@ -14,6 +14,7 @@ import domUtils from "../utils/dom-utils"; import ActionSheet from "../action-sheet"; import Location from "../services/location"; import DOM from "../dom"; +import fileStorage from "../utils/file-storage"; import { Share } from "@capacitor/share"; import { Toast } from "@capacitor/toast"; @@ -26,6 +27,7 @@ import Sortable from "sortablejs"; import { kml, gpx } from "@tmcw/togeojson"; import { DOMParser } from "@xmldom/xmldom"; import GeoJsonToGpx from "@dwayneparton/geojson-to-gpx"; +import { v4 as uuidv4 } from "uuid"; import LineSlice from "@turf/line-slice"; import CleanCoords from "@turf/clean-coords"; @@ -79,27 +81,29 @@ class MyAccount { // itinéraires this.routes = []; + this.routesOrder = []; // points de repère this.landmarks = []; + this.landmarksOrder = []; this.compareLandmarks = []; + this.compareLandmarksOrder = []; // cartes téléchargées this.offlineMaps = []; this.#addSourcesAndLayers(); - // Identifiant unique pour les itinéraires - this.lastRouteId = 0; - this.lastLandmarkId = 0; - this.lastCompareLandmarkId = 0; - + // REMOVEME : rétrocompatibilité des entités enregistrées : migration de préférences à fichier local (post-3.3.35) // récupération des itinéraires enregistrés en local let promiseRoutes = Preferences.get( { key: "savedRoutes"} ).then( (resp) => { if (resp.value) { var localRoutes = JSON.parse(resp.value); - this.routes = this.routes.concat(localRoutes.filter( route => !route.type)); - this.#updateSources(); + localRoutes.forEach( (route) => { + fileStorage.save(route, `route-${route.id}`); + this.routesOrder.push(route.id); + }); + Preferences.remove({ key: "savedRoutes" }); } }); @@ -107,8 +111,11 @@ class MyAccount { let promiseLandmarks = Preferences.get( { key: "savedLandmarks"} ).then( (resp) => { if (resp.value) { var localLandmarks = JSON.parse(resp.value); - this.landmarks = this.landmarks.concat(localLandmarks); - this.#updateSources(); + localLandmarks.forEach( (landmark) => { + fileStorage.save(landmark, `landmark-${landmark.id}`); + this.landmarksOrder.push(landmark.id); + }); + Preferences.remove({ key: "savedLandmarks" }); } }); @@ -116,10 +123,55 @@ class MyAccount { let promiseCompareLandmarks = Preferences.get( { key: "savedCompareLandmarks"} ).then( (resp) => { if (resp.value) { var localCompareLandmarks = JSON.parse(resp.value); - this.compareLandmarks = this.compareLandmarks.concat(localCompareLandmarks); - this.#updateSources(); + localCompareLandmarks.forEach( (compareLandmark) => { + fileStorage.save(compareLandmark, `comparelandmark-${compareLandmark.id}`); + this.compareLandmarksOrder.push(compareLandmark.id); + }); + Preferences.remove({ key: "savedCompareLandmarks" }); } }); + // END REMOVEME + + let fileStoragePromise; + let routeOrderStoragePromise; + let landmarkOrderStoragePromise; + let compareLandmarkOrderStoragePromise; + + // REMOVEME + Promise.all([promiseCompareLandmarks, promiseLandmarks, promiseRoutes]).then( () => { + // END REMOVEME + // chargement des enregistrements stockés en local + fileStoragePromise = fileStorage.list().then( (files) => { + files.forEach( (file) => { + if (file.id.startsWith("route-")) { + this.routes.push(file.data); + } else if (file.id.startsWith("landmark-")) { + this.landmarks.push(file.data); + } else if (file.id.startsWith("comparelandmark-")) { + this.compareLandmarks.push(file.data); + } + }); + }); + + // chargement de l'ordre des routes, landmarks et compareLandmarks + routeOrderStoragePromise = Preferences.get( { key: "myaccount_routes_order"} ).then( (resp) => { + if (resp.value) { + this.routesOrder = JSON.parse(resp.value); + } + }); + landmarkOrderStoragePromise = Preferences.get( { key: "myaccount_landmarks_order"} ).then( (resp) => { + if (resp.value) { + this.landmarksOrder = JSON.parse(resp.value); + } + }); + compareLandmarkOrderStoragePromise = Preferences.get( { key: "myaccount_comparelandmarks_order"} ).then( (resp) => { + if (resp.value) { + this.compareLandmarksOrder = JSON.parse(resp.value); + } + }); + // REMOVEME + }); + // END REMOVEME this.map.loadImage(LandmarkIconSaved).then((image) => { this.map.addImage("landmark-icon-saved", image.data); @@ -147,37 +199,19 @@ class MyAccount { }); // récupération des infos et rendu graphique - Promise.all([this.compute(), promiseCompareLandmarks, promiseLandmarks, promiseRoutes, Globals.offlineMaps.loadPromise]).then(() => { - // Ajout d'identifiant unique aux routes - this.routes.forEach((route) => { - route.id = this.lastRouteId; - this.lastRouteId++; - }); - // Ajout d'identifiant unique aux landmarks - this.landmarks.forEach((landmark) => { - landmark.id = this.lastLandmarkId; - this.lastLandmarkId++; - }); - // Ajout d'identifiant unique aux compareLandmarks - this.compareLandmarks.forEach((compareLandmark) => { - compareLandmark.id = this.lastCompareLandmarkId; - this.lastCompareLandmarkId++; - }); + Promise.all([ + this.compute(), fileStoragePromise, + routeOrderStoragePromise, landmarkOrderStoragePromise, compareLandmarkOrderStoragePromise, + Globals.offlineMaps.loadPromise, + ]).then(() => { + // Mise en ordre des routes, landmarks et compareLandmarks + jsUtils.sortArrayByAnotherArray(this.routes, this.routesOrder, "id"); + jsUtils.sortArrayByAnotherArray(this.landmarks, this.landmarksOrder, "id"); + jsUtils.sortArrayByAnotherArray(this.compareLandmarks, this.compareLandmarksOrder, "id"); + this.render(); this.#listeners(); this.#updateSources(); - Preferences.set({ - key: "savedRoutes", - value: JSON.stringify(this.routes), - }); - Preferences.set({ - key: "savedLandmarks", - value: JSON.stringify(this.landmarks), - }); - Preferences.set({ - key: "savedCompareLandmarks", - value: JSON.stringify(this.compareLandmarks), - }); }); this.lauchUrl = null; @@ -260,7 +294,7 @@ class MyAccount { const landmarkMap = this.map.queryRenderedFeatures(e.point, {layers: [MyAccountLayers["landmark-casing"].id]})[0]; const landmark = { type: "Feature", - id: landmarkMap.id, + id: landmarkMap.properties.id, geometry: landmarkMap.geometry, properties: landmarkMap.properties, }; @@ -611,18 +645,19 @@ class MyAccount { * @param {*} drawRouteSaveOptions */ addRoute(drawRouteSaveOptions) { - if (typeof drawRouteSaveOptions.id !== "undefined" && drawRouteSaveOptions.id >= 0) { + if (typeof drawRouteSaveOptions.id === "undefined" || drawRouteSaveOptions.id < 0) { + drawRouteSaveOptions.id = uuidv4(); + this.routes.unshift(drawRouteSaveOptions); + this.routesOrder.unshift(drawRouteSaveOptions.id); + } else { for (let i = 0; i < this.routes.length; i++) { if (this.routes[i].id === drawRouteSaveOptions.id){ this.routes[i] = drawRouteSaveOptions; break; } } - } else { - drawRouteSaveOptions.id = this.lastRouteId; - this.lastRouteId++; - this.routes.unshift(drawRouteSaveOptions); } + fileStorage.save(drawRouteSaveOptions, `route-${drawRouteSaveOptions.id}`); this.__updateAccountRoutesContainerDOMElement(this.routes); this.#updateSources(); let coordinates = []; @@ -653,19 +688,20 @@ class MyAccount { * @param {*} landmarkGeojson */ addLandmark(landmarkGeojson) { - if (typeof landmarkGeojson.id !== "undefined" && landmarkGeojson.id >= 0) { + if (typeof landmarkGeojson.id === "undefined" || landmarkGeojson.id < 0) { + landmarkGeojson = JSON.parse(JSON.stringify(landmarkGeojson)); + landmarkGeojson.id = uuidv4(); + this.landmarks.unshift(landmarkGeojson); + this.landmarksOrder.unshift(landmarkGeojson.id); + } else { for (let i = 0; i < this.landmarks.length; i++) { if (this.landmarks[i].id === landmarkGeojson.id){ this.landmarks[i] = JSON.parse(JSON.stringify(landmarkGeojson)); break; } } - } else { - const newlandmark = JSON.parse(JSON.stringify(landmarkGeojson)); - newlandmark.id = this.lastLandmarkId; - this.lastLandmarkId++; - this.landmarks.unshift(newlandmark); } + fileStorage.save(landmarkGeojson, `landmark-${landmarkGeojson.id}`); this.__updateAccountLandmarksContainerDOMElement(this.landmarks); this.#updateSources(); } @@ -675,19 +711,20 @@ class MyAccount { * @param {*} compareLandmarkGeojson */ addCompareLandmark(compareLandmarkGeojson) { - if (typeof compareLandmarkGeojson.id !== "undefined" && compareLandmarkGeojson.id >= 0) { + if (typeof compareLandmarkGeojson.id === "undefined" || compareLandmarkGeojson.id < 0) { + compareLandmarkGeojson = JSON.parse(JSON.stringify(compareLandmarkGeojson)); + compareLandmarkGeojson.id = uuidv4(); + this.compareLandmarks.unshift(compareLandmarkGeojson); + this.compareLandmarksOrder.unshift(compareLandmarkGeojson.id); + } else { for (let i = 0; i < this.compareLandmarks.length; i++) { if (this.compareLandmarks[i].id === compareLandmarkGeojson.id){ this.compareLandmarks[i] = JSON.parse(JSON.stringify(compareLandmarkGeojson)); break; } } - } else { - const newlandmark = JSON.parse(JSON.stringify(compareLandmarkGeojson)); - newlandmark.id = this.lastCompareLandmarkId; - this.lastCompareLandmarkId++; - this.compareLandmarks.unshift(newlandmark); } + fileStorage.save(compareLandmarkGeojson, `comparelandmark-${compareLandmarkGeojson.id}`); this.__updateAccountCompareLandmarksContainerDOMElement(this.compareLandmarks); this.#updateSources(); } @@ -709,11 +746,8 @@ class MyAccount { continue; } this.routes.splice(i, 1); + fileStorage.delete(`route-${routeId}`); this.__updateAccountRoutesContainerDOMElement(this.routes); - Preferences.set({ - key: "savedRoutes", - value: JSON.stringify(this.routes), - }); this.#updateSources(); break; } @@ -729,11 +763,8 @@ class MyAccount { continue; } this.landmarks.splice(i, 1); + fileStorage.delete(`landmark-${landmarkId}`); this.__updateAccountLandmarksContainerDOMElement(this.landmarks); - Preferences.set({ - key: "savedLandmarks", - value: JSON.stringify(this.landmarks), - }); this.#updateSources(); break; } @@ -749,11 +780,8 @@ class MyAccount { continue; } this.compareLandmarks.splice(i, 1); + fileStorage.delete(`comparelandmark-${compareLandmarkId}`); this.__updateAccountCompareLandmarksContainerDOMElement(this.compareLandmarks); - Preferences.set({ - key: "savedCompareLandmarks", - value: JSON.stringify(this.compareLandmarks), - }); this.#updateSources(); break; } @@ -775,6 +803,7 @@ class MyAccount { const route = this.routes[oldIndex]; this.routes.splice(oldIndex, 1); this.routes.splice(newIndex, 0, route); + this.#updateRoutesOrder(); this.#updateSources(); } @@ -787,6 +816,7 @@ class MyAccount { const landmark = this.landmarks[oldIndex]; this.landmarks.splice(oldIndex, 1); this.landmarks.splice(newIndex, 0, landmark); + this.#updateLandmarksOrder(); this.#updateSources(); } @@ -799,6 +829,7 @@ class MyAccount { const compareLandmark = this.compareLandmarks[oldIndex]; this.compareLandmarks.splice(oldIndex, 1); this.compareLandmarks.splice(newIndex, 0, compareLandmark); + this.#updateCompareLandmarksOrder(); this.#updateSources(); } @@ -811,6 +842,36 @@ class MyAccount { Globals.offlineMaps.changeMapIndex(offlineMapId, oldIndex, newIndex); } + /** + * Met à jour l'ordre d'affichage des routes + */ + #updateRoutesOrder() { + this.routesOrder = []; + this.routes.forEach((route) => { + this.routesOrder.push(route.id); + }); + } + + /** + * Met à jour l'ordre d'affichage des points de repère + */ + #updateLandmarksOrder() { + this.landmarksOrder = []; + this.landmarks.forEach((landmark) => { + this.landmarksOrder.push(landmark.id); + }); + } + + /** + * Met à jour l'ordre d'affichage des points de repère Comparer + */ + #updateCompareLandmarksOrder() { + this.compareLandmarksOrder = []; + this.compareLandmarks.forEach((compareLandmark) => { + this.compareLandmarksOrder.push(compareLandmark.id); + }); + } + /** * Ouvre l'outil de tracé d'itinéraire pour modifier un itinéraire * @param {*} route @@ -1513,8 +1574,8 @@ ${props.text}`, throw new Error("Null route ID"); } let route; - for (let i = 0; i < Globals.myaccount.routes.length; i++) { - route = Globals.myaccount.routes[i]; + for (let i = 0; i < this.routes.length; i++) { + route = this.routes[i]; if (route.id === routeId) { break; } @@ -1536,8 +1597,8 @@ ${props.text}`, throw new Error("Null landmark ID"); } let landmark; - for (let i = 0; i < Globals.myaccount.landmarks.length; i++) { - landmark = Globals.myaccount.landmarks[i]; + for (let i = 0; i < this.landmarks.length; i++) { + landmark = this.landmarks[i]; if (landmark.id === landmarkId) { break; } @@ -1559,8 +1620,8 @@ ${props.text}`, throw new Error("Null compareLandmark ID"); } let compareLandmark; - for (let i = 0; i < Globals.myaccount.compareLandmarks.length; i++) { - compareLandmark = Globals.myaccount.compareLandmarks[i]; + for (let i = 0; i < this.compareLandmarks.length; i++) { + compareLandmark = this.compareLandmarks[i]; if (compareLandmark.id === compareLandmarkId) { break; } @@ -1849,40 +1910,53 @@ ${props.text}`, * met à jour les sources de données pour l'affichage */ #updateSources() { - var linesource = this.map.getSource(this.configuration.linesource); + const linesource = this.map.getSource(this.configuration.linesource); linesource.setData({ type: "FeatureCollection", features: this.#getRouteLines(), }); - var pointsource = this.map.getSource(this.configuration.pointsource); + const pointsource = this.map.getSource(this.configuration.pointsource); pointsource.setData({ type: "FeatureCollection", features: this.#getRoutePoints(), }); - var landmarksource = this.map.getSource(this.configuration.landmarksource); + const landmarksource = this.map.getSource(this.configuration.landmarksource); + const landmarksWithIds = []; + this.landmarks.forEach((landmark) => { + const landmarkCopy = JSON.parse(JSON.stringify(landmark)); + landmarkCopy.properties.id = landmarkCopy.id; + landmarksWithIds.push(landmarkCopy); + }); + landmarksource.setData({ type: "FeatureCollection", - features: this.landmarks, + features: landmarksWithIds, }); - var compareLandmarksource = this.map.getSource(this.configuration.compareLandmarksource); + const compareLandmarksource = this.map.getSource(this.configuration.compareLandmarksource); + const compareLandmarksWithIds = []; + this.compareLandmarks.forEach((compareLandmark) => { + const compareLandmarkCopy = JSON.parse(JSON.stringify(compareLandmark)); + compareLandmarkCopy.properties.id = compareLandmarkCopy.id; + compareLandmarksWithIds.push(compareLandmarkCopy); + }); compareLandmarksource.setData({ type: "FeatureCollection", - features: this.compareLandmarks, + features: compareLandmarksWithIds, }); Preferences.set({ - key: "savedRoutes", - value: JSON.stringify(this.routes), + key: "myaccount_routes_order", + value: JSON.stringify(this.routesOrder), }); Preferences.set({ - key: "savedLandmarks", - value: JSON.stringify(this.landmarks), + key: "myaccount_landmarks_order", + value: JSON.stringify(this.landmarksOrder), }); Preferences.set({ - key: "savedCompareLandmarks", - value: JSON.stringify(this.compareLandmarks), + key: "myaccount_comparelandmarks_order", + value: JSON.stringify(this.compareLandmarksOrder), }); } diff --git a/src/js/offline-maps.js b/src/js/offline-maps.js index 169f92f0..fdcf3d8a 100644 --- a/src/js/offline-maps.js +++ b/src/js/offline-maps.js @@ -83,7 +83,9 @@ class OfflineMaps { "PLAN.IGN": "https://data.geopf.fr/tms/1.0.0/PLAN.IGN/{z}/{x}/{y}.pbf", }; - if (!Capacitor.isNativePlatform()) { + this.isNative = Capacitor.isNativePlatform(); + + if (!this.isNative) { this.dbPromise = openDB("tile-store", 1, { upgrade(db) { db.createObjectStore("tiles"); @@ -590,7 +592,7 @@ class OfflineMaps { * Saves the metadata of the current tileset */ async #saveOfflineMapMetadata(metadata, id) { - if (Capacitor.isNativePlatform()) { + if (this.isNative) { // Save metadata as a JSON file in Capacitor FileSystem const metadataString = JSON.stringify(metadata); await Filesystem.writeFile({ @@ -613,7 +615,7 @@ class OfflineMaps { * Get the metadata of a tileset */ async #getOfflineMapMetadata(id) { - if (Capacitor.isNativePlatform()) { + if (this.isNative) { // Read metadata JSON file from Capacitor FileSystem try { const result = await Filesystem.readFile({ @@ -638,7 +640,7 @@ class OfflineMaps { */ async #getAllOfflineMapsMetadata() { const results = {}; - if (Capacitor.isNativePlatform()) { + if (this.isNative) { // Read metadata JSON file from Capacitor FileSystem try { const files = (await Filesystem.readdir({ @@ -684,7 +686,7 @@ class OfflineMaps { } }); } - if (window.Capacitor && window.Capacitor.isNativePlatform()) { + if (this.isNative) { // Delete metadata JSON file in Capacitor FileSystem try { await Filesystem.deleteFile({ @@ -740,7 +742,7 @@ class OfflineMaps { try { let tileData; - if (Capacitor.isNativePlatform()) { + if (this.isNative) { try { const result = await Filesystem.readFile({ path: `tiles/${layer}/${zoom}/${x}/${y}.pbf`, @@ -784,7 +786,7 @@ class OfflineMaps { * @param {string} data base64 string representing the data */ async #storeVectorTile(tilePath, data) { - if (Capacitor.isNativePlatform()) { + if (this.isNative) { // Use Capacitor FileSystem on mobile const path = `tiles/${tilePath}.pbf`; await Filesystem.writeFile({ @@ -806,7 +808,7 @@ class OfflineMaps { * @param {String} tilepath */ async #deleteTile(tilePath) { - if (Capacitor.isNativePlatform()) { + if (this.isNative) { // Use Capacitor FileSystem on mobile const path = `tiles/${tilePath}.pbf`; await Filesystem.deleteFile({ diff --git a/src/js/position.js b/src/js/position.js index 8ce3eda7..d9144fba 100644 --- a/src/js/position.js +++ b/src/js/position.js @@ -344,8 +344,8 @@ class Position { // Récupération de l'id du landmark let landmarkId = -1; [...shadowContainer.getElementById("landmarkPositionTitle").classList].forEach((cl) => { - if (cl.split("-")[0] === "landmarkPosition") { - landmarkId = parseInt(cl.split("-")[1]); + if (cl.startsWith("landmarkPosition-")) { + landmarkId = cl.replace("landmarkPosition-", ""); } }); shadowContainer.getElementById("position-landmark-show-advanced-tools").addEventListener("click", () => { diff --git a/src/js/route-draw/route-draw.js b/src/js/route-draw/route-draw.js index b28ea78f..353564b7 100644 --- a/src/js/route-draw/route-draw.js +++ b/src/js/route-draw/route-draw.js @@ -424,7 +424,7 @@ class RouteDraw { name = this.name; } let id = -1; - if (this.routeId !== null && this.routeId >= 0) { + if (this.routeId !== null && (this.routeId >= 0 || typeof this.routeId === "string")) { id = this.routeId; } this.routeDrawSave = new RouteDrawSave(null, { diff --git a/src/js/utils/file-storage.js b/src/js/utils/file-storage.js new file mode 100644 index 00000000..4108d854 --- /dev/null +++ b/src/js/utils/file-storage.js @@ -0,0 +1,212 @@ +/** + * Copyright (c) Institut national de l'information géographique et forestière + * + * This program and the accompanying materials are made available under the terms of the GPL License, Version 3.0. + */ + +import { Capacitor } from "@capacitor/core"; +import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; +import { Preferences } from "@capacitor/preferences"; +import { v4 as uuidv4 } from "uuid"; + +/** + * FileStorage + * + * A unified class for storing and managing json objects across + * Capacitor platforms. Uses the Filesystem plugin on native (iOS/Android) + * and falls back to the Preferences API on Web. + * + * Each geometry is saved as a separate `.json` file inside a specified + * application directory (default: `geometries/`). + * + * Supports full CRUD operations: + * - Create (save) + * - Read (load) + * - Delete (remove individual) + * - List (load all geometries) + * + * Works on both iOS and Android via Capacitor's sandboxed app storage. + */ +class FileStorage { + /** + * @param {string} [folderName='geometries'] - Folder name inside the app's data directory. + */ + constructor(folderName = "geometries") { + this.folder = folderName; + this.isNative = Capacitor.isNativePlatform(); // true on iOS/Android + } + + /** + * Ensures the geometry folder exists, creating it if necessary. + * Called automatically by other methods. + * @returns {Promise} + * @private + */ + async #ensureFolder() { + if (!this.isNative) return; + try { + await Filesystem.mkdir({ + path: this.folder, + directory: Directory.Data, + recursive: true, + }); + } catch (error) { + if (error.message?.includes("exists")) return; + console.error("[FileStorage] Error ensuring folder:", error); + } + } + + /** + * Saves a json object to storage. + * If `id` is not provided, a UUID is generated. + * + * @param {object} json - A json object (e.g. a route or a landmark (geojson feature)). + * @param {string} [id] - Optional custom ID to use for the filename. + * @returns {Promise} The ID used to save the file. + */ + async save(json, id) { + const geometryId = id || uuidv4(); + + if (!this.isNative) { + // Web fallback: use Preferences + try { + await Preferences.set({ + key: `${this.folder}_${geometryId}`, + value: JSON.stringify(json), + }); + return geometryId; + } catch (error) { + console.error("[FileStorage] Error saving geometry (web):", error); + throw error; + } + } + + await this.#ensureFolder(); + const filePath = `${this.folder}/${geometryId}.json`; + try { + await Filesystem.writeFile({ + path: filePath, + data: JSON.stringify(json), + directory: Directory.Data, + encoding: Encoding.UTF8, + }); + return geometryId; + } catch (error) { + console.error("[FileStorage] Error saving geometry:", error); + throw error; + } + } + + /** + * Loads a GeoJSON file from storage by its ID. + * + * @param {string} id - The geometry ID (filename without `.json`). + * @returns {Promise} The parsed json object, or null if not found. + */ + async load(id) { + if (!this.isNative) { + const result = await Preferences.get({ key: `${this.folder}_${id}` }); + return result.value ? JSON.parse(result.value) : null; + } + try { + const result = await Filesystem.readFile({ + path: `${this.folder}/${id}.json`, + directory: Directory.Data, + encoding: Encoding.UTF8, + }); + return JSON.parse(result.data); + } catch (error) { + console.warn(`[FileStorage] Could not load geometry ${id}:`, error); + return null; + } + } + + /** + * Deletes a file from storage. + * + * @param {string} id - The json ID. + * @returns {Promise} + */ + async delete(id) { + if (!this.isNative) { + await Preferences.remove({ key: `${this.folder}_${id}` }); + } + try { + await Filesystem.deleteFile({ + path: `${this.folder}/${id}.json`, + directory: Directory.Data, + }); + } catch (error) { + console.error(`[FileStorage] Error deleting geometry ${id}:`, error); + } + } + + /** + * Lists all stored obekcts. + * + * @returns {Promise>} + * An array of objects containing: + * - `id`: The file ID (filename without `.json`). + * - `data`: The parsed JSON object. + */ + async list() { + if (!this.isNative) { + // Web fallback: list all Preferences keys (simulated folder) + const allKeys = await this.#getAllWebKeys(); + const geometries = []; + for (const key of allKeys) { + if (!key.startsWith(`${this.folder}_`)) { + continue; + } + const id = key.replace(`${this.folder}_`, ""); + const value = await Preferences.get({ key }); + if (value.value) { + geometries.push({ id, data: JSON.parse(value.value) }); + } + } + return geometries; + } + await this.#ensureFolder(); + const geometries = []; + try { + const result = await Filesystem.readdir({ + path: this.folder, + directory: Directory.Data, + }); + + for (const file of result.files) { + if (!file.name.endsWith(".json")) { + continue; + } + const id = file.name.replace(".json", ""); + const content = await this.load(id); + if (content) { + geometries.push({ id, data: content }); + } + } + return geometries; + } catch (error) { + console.error("[FileStorage] Error listing geometries:", error); + } + return geometries; + } + + /** + * (Web only) Returns all available keys from Preferences. + * @private + * @returns {Promise} + */ + async #getAllWebKeys() { + try { + const { keys } = await Preferences.keys(); + return keys; + } catch (error) { + console.error("[FileStorage] Error reading web keys:", error); + return []; + } + } + +} + +const fileStorage = new FileStorage(); +export default fileStorage; diff --git a/src/js/utils/js-utils.js b/src/js/utils/js-utils.js index e8cdda57..2f1b700d 100644 --- a/src/js/utils/js-utils.js +++ b/src/js/utils/js-utils.js @@ -31,6 +31,14 @@ let jsUtils = { document.body.appendChild(element); element.click(); document.body.removeChild(element); + }, + + sortArrayByAnotherArray(arrToSort, arrReference, property) { + arrToSort.sort( (a, b) => { + let indexA = arrReference.findIndex( item => item === a[property] ); + let indexB = arrReference.findIndex( item => item === b[property] ); + return indexA - indexB; + }); } };