import QtQuick 2.0 import Sailfish.Silica 1.0 import "../js/tides.js" as Tides Page { id: page property var customPorts: [] // porti creati dalla ricerca libera (sessione) // Campi di ricerca FUORI dal header della lista: gli id dichiarati nel // componente header hanno scope chiuso e le funzioni del Page non li // vedono (ReferenceError "filter is not defined"). Column { id: topCol width: parent.width PageHeader { title: "Tide" description: "v" + appVersion } SearchField { id: filter placeholderText: qsTr("Filter ports…") onTextChanged: rebuild() } Item { width: parent.width; height: Theme.paddingMedium } TextField { id: geoField width: parent.width - 2 * Theme.horizontalPageMargin anchors.horizontalCenter: parent.horizontalCenter placeholderText: qsTr("Free search: any city (online)…") EnterKey.enabled: text.length > 0 EnterKey.onClicked: findCity() } Button { text: qsTr("Find") anchors.horizontalCenter: parent.horizontalCenter onClicked: findCity() } Label { id: geoMsg visible: false width: parent.width - 2 * Theme.horizontalPageMargin anchors.horizontalCenter: parent.horizontalCenter wrapMode: Text.Wrap color: Theme.errorColor font.pixelSize: Theme.fontSizeSmall } } SilicaListView { id: list anchors.top: topCol.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom model: ListModel { id: portModel } header: SectionHeader { text: qsTr("Ports") } footer: Item { width: parent.width height: portModel.count === 0 ? Theme.itemSizeMedium : 0 Label { visible: portModel.count === 0 anchors.fill: parent anchors.margins: Theme.horizontalPageMargin text: qsTr("No ports match the filter. Try the free city search above.") color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } delegate: ListItem { contentHeight: Theme.itemSizeMedium highlighted: model.isPreferred Label { text: model.name color: model.isPreferred ? Theme.highlightColor : Theme.primaryColor font.pixelSize: Theme.fontSizeMedium anchors.left: parent.left anchors.leftMargin: Theme.horizontalPageMargin anchors.verticalCenter: parent.verticalCenter truncationMode: TruncationMode.Fade width: parent.width - 3 * Theme.horizontalPageMargin } Label { text: model.country color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall anchors.left: parent.left anchors.leftMargin: Theme.horizontalPageMargin anchors.top: parent.top anchors.topMargin: Theme.paddingSmall } 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 port:", model.name, "custom:", model.custom) page.pageStack.push(Qt.resolvedUrl("DetailPage.qml"), { port: model.port, portIndex: model.realIndex, custom: model.custom }) } } VerticalScrollDecorator {} } function rebuild() { portModel.clear() var f = filter.text.toLowerCase() var pref = appSettings.preferredPort var i, p for (i = 0; i < Tides.PORTS.length; i++) { p = Tides.PORTS[i] if (f !== "" && (p[0] + " " + p[1]).toLowerCase().indexOf(f) === -1) continue portModel.append({ name: p[0], country: p[1], range: "±" + (p[4][1][0] + p[4][2][0]).toFixed(2) + " m", port: p, realIndex: i, custom: false, isPreferred: (pref === p[0]) }) } for (i = 0; i < customPorts.length; i++) { p = customPorts[i] if (f !== "" && p[0].toLowerCase().indexOf(f) === -1) continue portModel.append({ name: p[0], country: qsTr("searched city"), range: "±" + (p[4][1][0] + p[4][2][0]).toFixed(2) + " m", port: p, realIndex: -1, custom: true, isPreferred: (pref === p[0]) }) } } // --- Ricerca libera (geocoding) --- function geoFail(msg) { geoMsg.text = msg || qsTr("City not found (offline and not in local dataset).") geoMsg.visible = true geoMsg.color = Theme.errorColor } function geoFound(name, lat, lon, src) { console.log("[tide] geoFound:", name, lat, lon, src) if (!name) { geoFail(); 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" } var port = [name, "", lat, lon, cost, flag] customPorts.push(port) rebuild() geoMsg.text = qsTr("Found %1 (via %2)").arg(name).arg(src) geoMsg.visible = true geoMsg.color = Theme.highlightColor try { page.pageStack.push(Qt.resolvedUrl("DetailPage.qml"), { port: port, portIndex: -1, custom: true }) } catch (e) { console.log("[tide] push DetailPage failed:", e) geoFail(qsTr("Could not open the detail page: %1").arg(e)) } } 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() var done = false var to = setTimeout(function() { if (!done) { done = true console.log("[tide] geoXhr timeout") cb(null, true) } }, 12000) 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) { if (done) return done = true clearTimeout(to) 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] var name = f.properties.name || query cb(name, f.geometry.coordinates[1], f.geometry.coordinates[0], "OpenStreetMap") } else { cb(null) } } catch (e) { cb(null) } } else { cb(null) } } } xhr.send() } function findCity() { var q = geoField.text.trim() console.log("[tide] findCity:", q) if (q === "") return geoMsg.text = qsTr("Searching…") geoMsg.visible = true geoMsg.color = Theme.secondaryColor var loc = Tides.findCityLocal(q) if (loc) { geoFound(loc[0], loc[2], loc[3], qsTr("local dataset")) } else { geoXhr(q, function(name, lat, lon, src, timeout) { if (timeout) { geoFail(qsTr("Geocoding service not reachable (timeout). Check the network.")) return } geoFound(name, lat, lon, src) }) } } Component.onCompleted: { rebuild() var pref = appSettings.preferredPort if (pref !== "") { for (var i = 0; i < Tides.PORTS.length; i++) { if (Tides.PORTS[i][0] === pref) { page.pageStack.push(Qt.resolvedUrl("DetailPage.qml"), { port: Tides.PORTS[i], portIndex: i, custom: false }) break } } } } }