import QtQuick 2.0 import Sailfish.Silica 1.0 import "../js/tides.js" as Tides import "../js/comuni.js" as Comuni Page { id: page // Messaggio mostrato nel footer quando la lista è vuota property string hintMsg: "" SilicaListView { id: list anchors.fill: parent model: ListModel { id: resultModel } PullDownMenu { MenuItem { text: qsTr("Settings") onClicked: page.openSettings() } } // Header della lista: titolo + ricerca (i campi qui scrollano con la // lista; le funzioni della pagina ricevono il testo come parametro, // perché gli id dell'header non sono visibili fuori dal componente) header: Column { width: parent.width PageHeader { title: "Tide" description: "v" + appVersion } SearchField { placeholderText: qsTr("Search a port or a town…") onTextChanged: page.updateList(text) EnterKey.enabled: text.trim().length > 0 EnterKey.onClicked: page.findOnline(text) } } footer: Item { width: parent.width height: resultModel.count === 0 ? Theme.itemSizeSmall * 1.8 : 0 Label { visible: resultModel.count === 0 anchors.fill: parent anchors.margins: Theme.horizontalPageMargin text: page.hintMsg color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } delegate: ListItem { contentHeight: Theme.itemSizeMedium highlighted: model.isPreferred // Due righe manuali (ListItemLayout non esiste su questo kit Qt 5.6) Column { anchors.left: parent.left anchors.leftMargin: Theme.horizontalPageMargin anchors.right: parent.right anchors.rightMargin: Theme.horizontalPageMargin + 80 anchors.verticalCenter: parent.verticalCenter Label { text: model.name color: model.isPreferred ? Theme.highlightColor : Theme.primaryColor width: parent.width truncationMode: TruncationMode.Fade } Label { text: model.country color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall width: parent.width truncationMode: TruncationMode.Fade } } Label { text: model.range color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall anchors.right: parent.right anchors.rightMargin: Theme.horizontalPageMargin anchors.verticalCenter: parent.verticalCenter } onClicked: { console.log("[tide] open result:", model.name, "kind:", model.kind) page.openResult(model.kind, model.portIndex, model.name, model.lat, model.lon) } } VerticalScrollDecorator {} } // --- Ricerca live (porti noti + tutti i comuni italiani) --- function norm(s) { // minuscolo + apostrofi tipografici -> dritti + accenti rimossi // ("citta" trova "Città", "sant'angelo" trova "Sant'Angelo") return s.toLowerCase() .replace(/[\u2018\u2019]/g, "'") .replace(/[àáâãä]/g, "a") .replace(/[èéêë]/g, "e") .replace(/[ìíîï]/g, "i") .replace(/[òóôõö]/g, "o") .replace(/[ùúûü]/g, "u") } function rangeOf(cost) { return "±" + (cost[1][0] + cost[2][0]).toFixed(2) + " m" } function updateList(q) { q = norm(q.trim()) // Rimozione SENZA clear(): clear() resetta il modello e su questo kit // ricrea l'header della lista (dove sta il SearchField) facendo // perdere il focus -> tastiera che si chiude a ogni lettera. if (resultModel.count > 0) resultModel.remove(0, resultModel.count) if (q === "") { hintMsg = qsTr("Type to search among all Italian towns and known ports.") return } hintMsg = qsTr("No local results — press Enter to search online.") var i, p, name // porti noti (match su nome o paese) for (i = 0; i < Tides.PORTS.length; i++) { p = Tides.PORTS[i] if (norm(p[0] + " " + p[1]).indexOf(q) !== -1) { resultModel.append({ name: p[0], country: p[1], range: rangeOf(p[4]), kind: "port", portIndex: i, lat: 0, lon: 0, isPreferred: (appSettings.preferredPort === p[0]) }) } } // comuni italiani (max 25 risultati) var n = 0 for (i = 0; i < Comuni.COMUNI.length && n < 25; i++) { name = Comuni.COMUNI[i][0] if (norm(name).indexOf(q) !== -1) { var lat = Comuni.COMUNI[i][1] var lon = Comuni.COMUNI[i][2] resultModel.append({ name: name, country: qsTr("Italy"), range: rangeOf(Tides.nearestPort(lat, lon).port[4]), kind: "comune", portIndex: -1, lat: lat, lon: lon, isPreferred: false }) n++ } } } // Navigazione centralizzata: pageStack si risolve solo nel contesto del // Page (nei delegate della lista non è visibile su questo kit). function openDetail(port, portIndex, custom) { console.log("[tide] openDetail:", port ? port[0] : "undefined", "custom:", custom) pageStack.push(Qt.resolvedUrl("DetailPage.qml"), { port: port, portIndex: portIndex, custom: custom }) } function openResult(kind, portIndex, name, lat, lon) { if (kind === "port") { page.openDetail(Tides.PORTS[portIndex], portIndex, false) return } // comune italiano: costanti dal porto noto entro 222 km, altrimenti // stima sintetica da lat/lon (etichettata nella pagina di dettaglio) var np = Tides.nearestPort(lat, lon) var cost, flag if (np.km < 222) { cost = np.port[4].slice() flag = "near:" + np.port[0] + ":" + (np.km / 111).toFixed(1) } else { cost = Tides.synthConst(lat, lon) flag = "synth" } page.openDetail([name, qsTr("Italy"), lat, lon, cost, flag], -1, true) } function openSettings() { console.log("[tide] openSettings") pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) } // --- Ricerca online (geocoding Photon) per città estere / non trovate --- function geoFound(name, lat, lon, src) { console.log("[tide] geoFound:", name, lat, lon, src) if (!name) return var np = Tides.nearestPort(lat, lon) var cost, flag if (np.km < 222) { cost = np.port[4].slice() flag = "near:" + np.port[0] + ":" + (np.km / 111).toFixed(1) } else { cost = Tides.synthConst(lat, lon) flag = "synth" } page.openDetail([name, "", lat, lon, cost, flag], -1, true) } function geoXhr(query, cb) { // Photon (komoot) invece di Nominatim: accetta qualsiasi User-Agent // (Nominatim risponde 403 all'UA generico inviato dalle XHR QML di Qt) // e supporta il filtro osm_tag=place:city per le città vere. console.log("[tide] geoXhr:", query) var xhr = new XMLHttpRequest() xhr.open("GET", "https://photon.komoot.io/api/?limit=1&osm_tag=place:city&q=" + encodeURIComponent(query), true) xhr.onreadystatechange = function() { if (xhr.readyState === 4) { console.log("[tide] geoXhr status:", xhr.status) if (xhr.status === 200) { try { var j = JSON.parse(xhr.responseText) if (j && j.features && j.features.length > 0) { var f = j.features[0] cb(f.properties.name || query, f.geometry.coordinates[1], f.geometry.coordinates[0]) } else { cb(null) } } catch (e) { cb(null) } } else { cb(null) } } } xhr.send() } function findOnline(q) { q = q.trim() console.log("[tide] findOnline:", q) if (q === "") return // se è un porto noto, apri direttamente var ql = norm(q) for (var i = 0; i < Tides.PORTS.length; i++) { if (norm(Tides.PORTS[i][0]) === ql) { page.openDetail(Tides.PORTS[i], i, false) return } } // se è un comune italiano esatto, apri direttamente for (i = 0; i < Comuni.COMUNI.length; i++) { if (norm(Comuni.COMUNI[i][0]) === ql) { page.openResult("comune", -1, Comuni.COMUNI[i][0], Comuni.COMUNI[i][1], Comuni.COMUNI[i][2]) return } } geoXhr(q, function(name, lat, lon) { if (!name) { console.log("[tide] online: non trovato") return } geoFound(name, lat, lon, "OpenStreetMap") }) } Component.onCompleted: { updateList("") var pref = appSettings.preferredPort if (pref !== "") { var found = false for (var i = 0; i < Tides.PORTS.length; i++) { if (Tides.PORTS[i][0] === pref) { page.openDetail(Tides.PORTS[i], i, false) found = true break } } // preferito custom (comune): ricostruisci e apri il dettaglio if (!found && appSettings.preferredLat >= 0) { var lat = appSettings.preferredLat var lon = appSettings.preferredLon var np = Tides.nearestPort(lat, lon) var cost = np.km < 222 ? np.port[4].slice() : Tides.synthConst(lat, lon) page.openDetail([pref, qsTr("Italy"), lat, lon, cost, np.km < 222 ? "near:" + np.port[0] : "synth"], -1, true) } } } }