265 lines
9.2 KiB
QML
265 lines
9.2 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|