harbour-maree 0.1.0: motore armonico 7 costituenti, 16 porti, ricerca libera (Nominatim+locale), grafico 24/48/72h, EN/IT, preferenze (lingua, porto, unità, intervallo), icone harbour
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
build/
|
||||
build-*/
|
||||
*.rpm
|
||||
*.user
|
||||
Makefile
|
||||
.qmake.stash
|
||||
*.qm
|
||||
@@ -0,0 +1,72 @@
|
||||
# harbour-maree
|
||||
|
||||
Previsioni di marea **universali** per Sailfish OS — client nativo Silica con
|
||||
motore armonico locale (nessuna API di maree: calcolo offline, per sempre).
|
||||
|
||||
## Funzionalità
|
||||
|
||||
- **Motore armonico a 7 costituenti** (M2, S2, N2, K1, O1, P1, K2):
|
||||
`h(t) = Z₀ + Σ Aᵢ·cos(ωᵢt + φᵢ)`, calcolato localmente
|
||||
- **16 porti noti** con costanti realistiche (Italia + Londra, New York,
|
||||
San Francisco, Tokyo, Sydney, Honolulu)
|
||||
- **Ricerca libera delle città**: geocoding OpenStreetMap (Nominatim) con
|
||||
fallback su dataset locale offline; le costanti arrivano dal porto noto
|
||||
entro 222 km, altrimenti da una stima sintetica da lat/lon (etichettata)
|
||||
- **Card "porto più vicino"** in evidenza quando si apre una città cercata
|
||||
- **Grafico 24/48/72 h** con alte/basse maree marcate e orari, tap per
|
||||
altezza in un punto
|
||||
- **Fase lunare e alba/tramonto** calcolati
|
||||
- **Italiano e inglese** (selettore lingua nelle impostazioni, applicata al
|
||||
riavvio; catalogo `.qm` nel data dir dell'app)
|
||||
- **Preferenze salvate**: lingua, porto preferito (aperto all'avvio e
|
||||
mostrato sulla cover), unità (m/ft), intervallo del grafico
|
||||
|
||||
## Build
|
||||
|
||||
Serve il [Sailfish SDK](https://sailfishos.org/wiki/Sailfish_SDK).
|
||||
|
||||
```sh
|
||||
sfdk target install SailfishOS-5.0.0.62-aarch64 # o il target del dispositivo
|
||||
sfdk target use SailfishOS-5.0.0.62-aarch64
|
||||
sfdk build # produce l'RPM in RPMS/
|
||||
sfdk deploy # installa sul dispositivo connesso
|
||||
```
|
||||
|
||||
Target consigliati: `aarch64` (Jolla Phone 2026, Xperia 10 II/III),
|
||||
`armv7hl` (Xperia X, Jolla C), `x86_64` (emulatore).
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
src/ C++: main.cpp (lingua + translator), settings
|
||||
(QSettings: language, preferredPort, unit, days)
|
||||
qml/js/tides.js motore armonico (ES5 puro, compatibile Qt 5.6):
|
||||
costanti, predizione, estremi, stima sintetica,
|
||||
geocoding locale, luna, sole
|
||||
qml/pages/ MainPage (porti + ricerca libera), DetailPage
|
||||
(grafico canvas, eventi, card porto vicino, astro),
|
||||
SettingsPage (lingua, unità, intervallo, porto)
|
||||
qml/cover/ cover con porto preferito e prossima marea
|
||||
translations/ harbour-maree.ts (EN sorgente), harbour-maree_it.ts
|
||||
icons/ icone 86/108/128/172 (make_icons.py, stile harbour)
|
||||
rpm/ spec e desktop file (SailJail: Internet)
|
||||
```
|
||||
|
||||
## Configurazione
|
||||
|
||||
File: `~/.config/harbour/maree/maree.conf` (QSettings, zona persistente
|
||||
della sandbox SailJail). Chiavi: `language` (system/en/it), `preferredPort`,
|
||||
`unit` (m/ft), `days` (1/2/3).
|
||||
|
||||
## Limiti noti
|
||||
|
||||
- Le costanti dei porti sono sintetiche ma realistiche; la stima da lat/lon
|
||||
non distingue bacini chiusi (Mediterraneo) da oceano aperto.
|
||||
- Precisione di un vero modello globale (FES2014/TPXO9): ~10–20 cm in oceano
|
||||
aperto; il motore è predisposto a ricevere le costanti da griglia.
|
||||
- Non per navigazione.
|
||||
- La lingua si applica al riavvio dell'app.
|
||||
|
||||
## Licenza
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,42 @@
|
||||
TEMPLATE = app
|
||||
TARGET = harbour-maree
|
||||
|
||||
CONFIG += sailfishapp
|
||||
CONFIG += c++11
|
||||
|
||||
SOURCES += \
|
||||
src/main.cpp \
|
||||
src/settings.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/settings.h
|
||||
|
||||
OTHER_FILES += \
|
||||
qml/harbour-maree.qml \
|
||||
qml/pages/*.qml \
|
||||
qml/cover/*.qml \
|
||||
qml/js/*.js \
|
||||
rpm/harbour-maree.spec \
|
||||
rpm/harbour-maree.desktop \
|
||||
translations/harbour-maree.ts \
|
||||
translations/harbour-maree_it.ts
|
||||
|
||||
DISTFILES += $$OTHER_FILES
|
||||
|
||||
# Le traduzioni NON sono gestite dal template sailfishapp_i18n sul kit 5.1
|
||||
# (il .qm non viene generato/installato in modo affidabile). Lo spec compila
|
||||
# il catalogo con lrelease e lo installa in /usr/share/harbour-maree/translations;
|
||||
# main.cpp carica il catalogo italiano esplicitamente (pattern calibreweb).
|
||||
# Le stringhe sorgente sono INGLESI (lingua di default); harbour-maree_it.ts
|
||||
# fornisce il catalogo italiano.
|
||||
|
||||
# Icone installate via make install (struttura standard hicolor)
|
||||
icons86.path = /usr/share/icons/hicolor/86x86/apps
|
||||
icons86.files = $$PWD/icons/86x86/harbour-maree.png
|
||||
icons108.path = /usr/share/icons/hicolor/108x108/apps
|
||||
icons108.files = $$PWD/icons/108x108/harbour-maree.png
|
||||
icons128.path = /usr/share/icons/hicolor/128x128/apps
|
||||
icons128.files = $$PWD/icons/128x128/harbour-maree.png
|
||||
icons172.path = /usr/share/icons/hicolor/172x172/apps
|
||||
icons172.files = $$PWD/icons/172x172/harbour-maree.png
|
||||
INSTALLS += icons86 icons108 icons128 icons172
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Genera le icone dell'app in stile Sailfish OS:
|
||||
sfondo a gradiente azzurro->blu con angoli molto arrotondati,
|
||||
due onde bianche (marea) con ombra morbida e una luna in alto.
|
||||
Dimensioni: 86, 108, 128, 172 (struttura standard hicolor)."""
|
||||
import math
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# gradiente Sailfish: azzurro chiaro (alto) -> blu profondo (basso)
|
||||
C_TOP = (94, 210, 240) # #5ED2F0
|
||||
C_BOT = (14, 111, 168) # #0E6FA8
|
||||
WAVE1 = (255, 255, 255) # onda principale, bianca piena
|
||||
WAVE2 = (255, 255, 255) # onda secondaria, semitrasparente
|
||||
MOON = (255, 255, 255)
|
||||
|
||||
|
||||
def gradient(size, c1, c2):
|
||||
img = Image.new("RGB", (size, size))
|
||||
d = ImageDraw.Draw(img)
|
||||
for y in range(size):
|
||||
t = y / (size - 1)
|
||||
d.line([(0, y), (size, y)],
|
||||
fill=(int(c1[0] + (c2[0] - c1[0]) * t),
|
||||
int(c1[1] + (c2[1] - c1[1]) * t),
|
||||
int(c1[2] + (c2[2] - c1[2]) * t)))
|
||||
return img
|
||||
|
||||
|
||||
def wave_layer(size, y0, amp, phase, lw, alpha):
|
||||
"""Layer RGBA con una sinusoide orizzontale (onda)."""
|
||||
layer = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(layer)
|
||||
pts = []
|
||||
for x in range(0, size + 2, 2):
|
||||
y = y0 + amp * math.sin(2 * math.pi * x / size * 1.6 + phase)
|
||||
pts.append((x, y))
|
||||
d.line(pts, fill=WAVE1 + (alpha,), width=lw)
|
||||
return layer
|
||||
|
||||
|
||||
def draw_icon(size):
|
||||
m = size * 0.05
|
||||
r = size * 0.22
|
||||
out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
|
||||
# sfondo: gradiente ritagliato nel rounded rect
|
||||
grad = gradient(size, C_TOP, C_BOT).convert("RGBA")
|
||||
mask = Image.new("L", (size, size), 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle([m, m, size - m, size - m],
|
||||
radius=r, fill=255)
|
||||
out.paste(grad, (0, 0), mask)
|
||||
|
||||
# onde: due sinusoidi, la seconda sfasata e piu' bassa
|
||||
lw1 = max(2, int(size * 0.045))
|
||||
lw2 = max(2, int(size * 0.032))
|
||||
out.alpha_composite(wave_layer(size, size * 0.58, size * 0.07, 0.0, lw1, 255))
|
||||
out.alpha_composite(wave_layer(size, size * 0.74, size * 0.06, math.pi, lw2, 150))
|
||||
|
||||
# luna in alto a destra (cerchio pieno, con ombra leggera)
|
||||
d = ImageDraw.Draw(out)
|
||||
lx = size * 0.78
|
||||
ly = size * 0.22
|
||||
lr = size * 0.09
|
||||
d.ellipse([lx - lr + size * 0.01, ly - lr + size * 0.01,
|
||||
lx + lr + size * 0.01, ly + lr + size * 0.01],
|
||||
fill=(0, 60, 100, 70))
|
||||
d.ellipse([lx - lr, ly - lr, lx + lr, ly + lr], fill=MOON)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
for s in (86, 108, 128, 172):
|
||||
os.makedirs(f"icons/{s}x{s}", exist_ok=True)
|
||||
draw_icon(s).save(f"icons/{s}x{s}/harbour-maree.png")
|
||||
print("icone scritte: 86, 108, 128, 172")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/tides.js" as Tides
|
||||
|
||||
CoverBackground {
|
||||
id: cover
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.paddingSmall
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: "🌊 Maree"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.primaryColor
|
||||
}
|
||||
Label {
|
||||
id: portLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: qsTr("No port selected")
|
||||
}
|
||||
Label {
|
||||
id: nextLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.highlightColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Cover.Active) refresh()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
function refresh() {
|
||||
var pref = appSettings.preferredPort
|
||||
if (pref === "") {
|
||||
portLabel.text = qsTr("No port selected")
|
||||
nextLabel.text = ""
|
||||
return
|
||||
}
|
||||
var p = null
|
||||
for (var i = 0; i < Tides.PORTS.length; i++) {
|
||||
if (Tides.PORTS[i][0] === pref) { p = Tides.PORTS[i]; break }
|
||||
}
|
||||
if (!p) { portLabel.text = qsTr("No port selected"); nextLabel.text = ""; return }
|
||||
portLabel.text = p[0]
|
||||
var ex = Tides.extrema(p, new Date(), 1)
|
||||
if (ex.length > 0) {
|
||||
var e = ex[0]
|
||||
var kind = e.kind === "high" ? qsTr("High") : qsTr("Low")
|
||||
nextLabel.text = kind + " " + Tides.fmtHM(e.t) + " · " + Tides.fmtH(e.h, appSettings.unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "pages"
|
||||
import "cover"
|
||||
|
||||
ApplicationWindow {
|
||||
id: app
|
||||
|
||||
initialPage: Component { MainPage {} }
|
||||
cover: Component { CoverPage {} }
|
||||
|
||||
allowedOrientations: Orientation.Portrait
|
||||
_defaultPageOrientations: Orientation.Portrait
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
// tides.js — motore armonico delle maree (ES5 puro, compatibile Qt 5.6 QML)
|
||||
// Formato porto: [nome, paese, lat, lon, cost, flag]
|
||||
// cost = [Z0, [A,phi] per M2,S2,N2,K1,O1,P1,K2] (phi in gradi)
|
||||
// flag = "" (porto noto) | "near:<porto>:<gradi>" | "synth"
|
||||
|
||||
var W = {
|
||||
M2: 28.9841042, S2: 30.0, N2: 28.4397295,
|
||||
K1: 15.0410686, O1: 13.9430356, P1: 14.9589314, K2: 30.0821373
|
||||
};
|
||||
var WK = ["M2", "S2", "N2", "K1", "O1", "P1", "K2"];
|
||||
|
||||
var PORTS = [
|
||||
["Venezia", "Italia", 45.43, 12.34, [0.35, [0.22, 215], [0.10, 245], [0.05, 210], [0.19, 100], [0.12, 85], [0.06, 100], [0.03, 245]]],
|
||||
["Chioggia", "Italia", 45.22, 12.28, [0.34, [0.21, 212], [0.10, 242], [0.05, 207], [0.18, 98], [0.11, 83], [0.06, 98], [0.03, 242]]],
|
||||
["Trieste", "Italia", 45.65, 13.77, [0.34, [0.20, 220], [0.09, 250], [0.05, 215], [0.22, 105], [0.14, 90], [0.07, 105], [0.03, 250]]],
|
||||
["Ancona", "Italia", 43.62, 13.52, [0.25, [0.12, 210], [0.05, 240], [0.03, 205], [0.10, 95], [0.06, 80], [0.03, 95], [0.02, 240]]],
|
||||
["Genova", "Italia", 44.41, 8.93, [0.22, [0.08, 200], [0.04, 230], [0.02, 195], [0.10, 95], [0.07, 80], [0.03, 95], [0.01, 230]]],
|
||||
["Livorno", "Italia", 43.55, 10.31, [0.24, [0.10, 205], [0.05, 235], [0.03, 200], [0.11, 95], [0.07, 80], [0.03, 95], [0.01, 235]]],
|
||||
["Napoli", "Italia", 40.84, 14.27, [0.22, [0.10, 210], [0.05, 240], [0.03, 205], [0.11, 90], [0.07, 75], [0.03, 90], [0.01, 240]]],
|
||||
["Palermo", "Italia", 38.12, 13.36, [0.21, [0.09, 205], [0.04, 235], [0.02, 200], [0.10, 90], [0.06, 75], [0.03, 90], [0.01, 235]]],
|
||||
["Cagliari", "Italia", 39.21, 9.11, [0.20, [0.07, 200], [0.03, 230], [0.02, 195], [0.09, 88], [0.06, 73], [0.03, 88], [0.01, 230]]],
|
||||
["Bari", "Italia", 41.13, 16.87, [0.25, [0.14, 200], [0.06, 230], [0.04, 195], [0.10, 92], [0.06, 77], [0.03, 92], [0.02, 230]]],
|
||||
["Londra (London Bridge)", "Regno Unito", 51.51, -0.09, [3.50, [2.10, 150], [0.85, 180], [0.45, 145], [0.12, 200], [0.10, 190], [0.04, 200], [0.23, 180]]],
|
||||
["New York (The Battery)", "USA", 40.70, -74.01, [0.80, [0.67, 180], [0.28, 205], [0.14, 175], [0.12, 60], [0.10, 55], [0.04, 60], [0.08, 205]]],
|
||||
["San Francisco (Golden Gate)", "USA", 37.81, -122.47, [1.00, [0.58, 135], [0.26, 160], [0.12, 130], [0.37, 290], [0.24, 275], [0.11, 290], [0.07, 160]]],
|
||||
["Tokyo (Bay)", "Giappone", 35.68, 139.77, [1.20, [0.45, 170], [0.20, 195], [0.09, 165], [0.18, 240], [0.12, 225], [0.05, 240], [0.05, 195]]],
|
||||
["Sydney", "Australia", -33.87, 151.21, [1.00, [0.50, 100], [0.23, 125], [0.10, 95], [0.10, 300], [0.07, 285], [0.03, 300], [0.06, 125]]],
|
||||
["Honolulu", "USA", 21.31, -157.86, [0.35, [0.20, 115], [0.09, 140], [0.05, 110], [0.18, 320], [0.12, 305], [0.05, 320], [0.02, 140]]]
|
||||
];
|
||||
|
||||
// Citta' con coordinate per ricerca offline (fallback senza rete)
|
||||
var CITIES = [
|
||||
["Milano", "Italia", 45.46, 9.19], ["Roma", "Italia", 41.90, 12.50], ["Torino", "Italia", 45.07, 7.69],
|
||||
["Bologna", "Italia", 44.49, 11.34], ["Firenze", "Italia", 43.77, 11.26], ["Verona", "Italia", 45.44, 10.99],
|
||||
["Padova", "Italia", 45.41, 11.88], ["Ravenna", "Italia", 44.42, 12.20], ["Pescara", "Italia", 42.46, 14.21],
|
||||
["Reggio Calabria", "Italia", 38.11, 15.65], ["Catania", "Italia", 37.51, 15.09], ["Trapani", "Italia", 38.02, 12.54],
|
||||
["Olbia", "Italia", 40.92, 9.50], ["Alghero", "Italia", 40.56, 8.32], ["Imperia", "Italia", 43.89, 8.03],
|
||||
["La Spezia", "Italia", 44.10, 9.82], ["Viareggio", "Italia", 43.87, 10.25], ["Civitavecchia", "Italia", 42.09, 11.79],
|
||||
["Salerno", "Italia", 40.68, 14.77], ["Taranto", "Italia", 40.46, 17.24], ["Brindisi", "Italia", 40.64, 17.95],
|
||||
["Otranto", "Italia", 40.15, 18.49], ["Marsala", "Italia", 37.80, 12.44], ["Porto Torres", "Italia", 40.84, 8.40],
|
||||
["Amsterdam", "Paesi Bassi", 52.37, 4.90], ["Rotterdam", "Paesi Bassi", 51.92, 4.48],
|
||||
["Barcellona", "Spagna", 41.39, 2.17], ["Marsiglia", "Francia", 43.30, 5.37], ["Atene", "Grecia", 37.98, 23.73],
|
||||
["Istanbul", "Turchia", 41.01, 28.98], ["Lisbona", "Portogallo", 38.72, -9.14], ["Dublino", "Irlanda", 53.35, -6.26],
|
||||
["Edimburgo", "Regno Unito", 55.95, -3.19], ["Amburgo", "Germania", 53.55, 9.99], ["Copenaghen", "Danimarca", 55.68, 12.57],
|
||||
["Oslo", "Norvegia", 59.91, 10.75], ["Stoccolma", "Svezia", 59.33, 18.07], ["Helsinki", "Finlandia", 60.17, 24.94],
|
||||
["Miami", "USA", 25.76, -80.19], ["Los Angeles", "USA", 34.05, -118.24], ["Seattle", "USA", 47.61, -122.33],
|
||||
["Vancouver", "Canada", 49.28, -123.12], ["Città del Messico", "Messico", 19.43, -99.13],
|
||||
["Lima", "Perù", -12.05, -77.04], ["Buenos Aires", "Argentina", -34.60, -58.38],
|
||||
["Città del Capo", "Sudafrica", -33.92, 18.42], ["Mumbai", "India", 19.08, 72.88],
|
||||
["Singapore", "Singapore", 1.35, 103.82], ["Hong Kong", "Cina", 22.32, 114.17], ["Shanghai", "Cina", 31.23, 121.47],
|
||||
["Seoul", "Corea del Sud", 37.57, 126.98], ["Manila", "Filippine", 14.60, 120.98], ["Jakarta", "Indonesia", -6.21, 106.85],
|
||||
["Auckland", "Nuova Zelanda", -36.85, 174.76], ["Wellington", "Nuova Zelanda", -41.29, 174.78],
|
||||
["Rio de Janeiro", "Brasile", -22.91, -43.17], ["Il Cairo", "Egitto", 30.04, 31.24], ["Dubai", "EAU", 25.20, 55.27],
|
||||
["Gibilterra", "Regno Unito", 36.14, -5.35], ["Malta (La Valletta)", "Malta", 35.90, 14.51]
|
||||
];
|
||||
|
||||
var EPOCH = Date.UTC(2000, 0, 1) / 3600000; // ore dal 1970 all'epoca armonica
|
||||
|
||||
// Altezza della marea (metri) in un dato istante
|
||||
function tide(port, date) {
|
||||
var t = date.getTime() / 3600000 - EPOCH;
|
||||
var C = port[4];
|
||||
var h = C[0];
|
||||
for (var i = 0; i < 7; i++) {
|
||||
var a = C[i + 1][0], p = C[i + 1][1];
|
||||
h += a * Math.cos((W[WK[i]] * t + p) * Math.PI / 180);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// Estremi (alta/bassa) da adesso nei prossimi giorni
|
||||
function extrema(port, now, days) {
|
||||
var step = 2 * 60 * 1000;
|
||||
var out = [];
|
||||
var end = now.getTime() + days * 86400000;
|
||||
var prev = tide(port, new Date(now.getTime() - step));
|
||||
var cur = tide(port, now);
|
||||
var rising = cur > prev;
|
||||
for (var t = now.getTime() + step; t <= end; t += step) {
|
||||
var nx = tide(port, new Date(t));
|
||||
if (rising && nx < cur) {
|
||||
var bt = t - step, hh = cur;
|
||||
for (var dt = bt - 10 * 60000; dt <= t + 10 * 60000; dt += 30000) {
|
||||
var hh2 = tide(port, new Date(dt));
|
||||
if (hh2 > hh) { hh = hh2; bt = dt; }
|
||||
}
|
||||
out.push({ t: bt, h: hh, kind: "high" });
|
||||
} else if (!rising && nx > cur) {
|
||||
var bt2 = t - step, hh3 = cur;
|
||||
for (var dt2 = bt2 - 10 * 60000; dt2 <= t + 10 * 60000; dt2 += 30000) {
|
||||
var hh4 = tide(port, new Date(dt2));
|
||||
if (hh4 < hh3) { hh3 = hh4; bt2 = dt2; }
|
||||
}
|
||||
out.push({ t: bt2, h: hh3, kind: "low" });
|
||||
}
|
||||
prev = cur; cur = nx; rising = nx > prev;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Distanza approssimata in km tra un porto e un punto
|
||||
function portDist(p, lat, lon) {
|
||||
var dlat = (p[2] - lat) * 111;
|
||||
var dlon = (p[3] - lon) * 111 * Math.cos(lat * Math.PI / 180);
|
||||
return Math.sqrt(dlat * dlat + dlon * dlon);
|
||||
}
|
||||
|
||||
// Porto noto piu' vicino a lat/lon
|
||||
function nearestPort(lat, lon) {
|
||||
var best = null, bd = 1e9;
|
||||
for (var i = 0; i < PORTS.length; i++) {
|
||||
var d = portDist(PORTS[i], lat, lon);
|
||||
if (d < bd) { bd = d; best = PORTS[i]; }
|
||||
}
|
||||
return { port: best, km: bd };
|
||||
}
|
||||
|
||||
// Costanti stimate da lat/lon (modello sintetico, per il mockup)
|
||||
function synthConst(lat, lon) {
|
||||
var a = Math.abs(lat);
|
||||
var g = Math.exp(-0.5 * Math.pow((a - 48) / 18, 2));
|
||||
var M2 = 0.08 + 1.6 * g;
|
||||
var S2 = 0.38 * M2, N2 = 0.19 * M2, K2 = 0.27 * S2;
|
||||
var dg = Math.exp(-0.5 * Math.pow(a / 25, 2));
|
||||
var K1 = 0.10 + 0.22 * dg, O1 = 0.70 * K1, P1 = 0.33 * K1;
|
||||
var pM = ((lon * 2.8 + 100) % 360 + 360) % 360;
|
||||
return [0.3, [M2, pM], [S2, (pM + 30) % 360], [N2, (pM - 25 + 360) % 360], [K1, 80], [O1, 65], [P1, 80], [K2, (pM + 30) % 360]];
|
||||
}
|
||||
|
||||
// Citta' nel dataset locale (ricerca offline)
|
||||
function findCityLocal(q) {
|
||||
q = q.toLowerCase();
|
||||
for (var i = 0; i < CITIES.length; i++) {
|
||||
if (CITIES[i][0].toLowerCase().indexOf(q) !== -1) return CITIES[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fase lunare: {icon, idx (0-7), age (giorni)}
|
||||
function moonPhase(date) {
|
||||
var syn = 29.53058867;
|
||||
var known = Date.UTC(2000, 0, 6, 18, 14) / 86400000;
|
||||
var age = ((date.getTime() / 86400000 - known) % syn + syn) % syn;
|
||||
var icons = ["\uD83C\uDF11", "\uD83C\uDF12", "\uD83C\uDF13", "\uD83C\uDF14", "\uD83C\uDF15", "\uD83C\uDF16", "\uD83C\uDF17", "\uD83C\uDF18"];
|
||||
return { icon: icons[Math.round(age / syn * 8) % 8], idx: Math.round(age / syn * 8) % 8, age: age };
|
||||
}
|
||||
|
||||
// Alba/tramonto approssimati (ora solare, ore decimali)
|
||||
function sunTimes(lat, date) {
|
||||
var N = Math.floor((date.getTime() - Date.UTC(date.getFullYear(), 0, 1)) / 86400000);
|
||||
var dec = 23.45 * Math.sin((360 / 365) * (284 + N) * Math.PI / 180);
|
||||
var latR = lat * Math.PI / 180, decR = dec * Math.PI / 180;
|
||||
var cosH = (-Math.sin(latR) * Math.sin(decR)) / (Math.cos(latR) * Math.cos(decR));
|
||||
cosH = Math.max(-1, Math.min(1, cosH));
|
||||
var H = Math.acos(cosH) * 180 / Math.PI / 15;
|
||||
return { rise: 12 - H, set: 12 + H };
|
||||
}
|
||||
|
||||
// Formato altezza secondo l'unita' scelta
|
||||
function fmtH(h, unit) {
|
||||
return unit === "ft" ? (h * 3.28084).toFixed(1) + " ft" : h.toFixed(2) + " m";
|
||||
}
|
||||
|
||||
// Formato ora HH:MM
|
||||
function fmtHM(t) {
|
||||
var d = new Date(t);
|
||||
var hh = d.getHours(), mm = d.getMinutes();
|
||||
return (hh < 10 ? "0" : "") + hh + ":" + (mm < 10 ? "0" : "") + mm;
|
||||
}
|
||||
|
||||
// Formato data breve
|
||||
function fmtDate(t) {
|
||||
var d = new Date(t);
|
||||
var giorni = ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab"];
|
||||
return giorni[d.getDay()] + " " + (d.getDate() < 10 ? "0" : "") + d.getDate() + "/" + ((d.getMonth() + 1) < 10 ? "0" : "") + (d.getMonth() + 1);
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/tides.js" as Tides
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
property var port
|
||||
property int portIndex: -1
|
||||
property bool custom: false
|
||||
property int days: appSettings.days
|
||||
property string unit: appSettings.unit
|
||||
property var nearPort: null
|
||||
property real nearKm: 0
|
||||
|
||||
ListModel { id: eventsModel }
|
||||
|
||||
Timer {
|
||||
interval: 600000 // aggiorna ogni 10 minuti
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: refresh()
|
||||
}
|
||||
|
||||
onPortChanged: init()
|
||||
|
||||
function init() {
|
||||
nearPort = null
|
||||
nearKm = 0
|
||||
if (custom && port.length > 5 && port[5] !== "") {
|
||||
var np = Tides.nearestPort(port[2], port[3])
|
||||
if (np.port !== null) {
|
||||
nearPort = np.port
|
||||
nearKm = np.km
|
||||
}
|
||||
}
|
||||
refresh()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!port) return
|
||||
// eventi
|
||||
eventsModel.clear()
|
||||
var ex = Tides.extrema(port, new Date(), days)
|
||||
var n = Math.min(ex.length, 6)
|
||||
for (var i = 0; i < n; i++) {
|
||||
eventsModel.append({
|
||||
kind: ex[i].kind,
|
||||
date: Tides.fmtDate(ex[i].t),
|
||||
time: Tides.fmtHM(ex[i].t),
|
||||
height: Tides.fmtH(ex[i].h, unit)
|
||||
})
|
||||
}
|
||||
// astro
|
||||
var mp = Tides.moonPhase(new Date())
|
||||
moonLabel.text = mp.icon + " " + moonNames[mp.idx] + " (" + qsTr("age %1 d").arg(mp.age.toFixed(1)) + ")"
|
||||
var st = Tides.sunTimes(port[2], new Date())
|
||||
sunLabel.text = Tides.fmtHM(new Date(0, 0, 0, st.rise, 0)) + " / " + Tides.fmtHM(new Date(0, 0, 0, st.set, 0)) + " " + qsTr("(solar time)")
|
||||
rangeLabel.text = "±" + ((port[4][1][0] + port[4][2][0]) * 2).toFixed(2) + " m"
|
||||
// info curva
|
||||
var f = port[5] || ""
|
||||
if (f.indexOf("near:") === 0) {
|
||||
var p = f.split(":")
|
||||
curveInfo.text = qsTr("Constants of the port of %1 (distance %2°)").arg(p[1]).arg(p[2])
|
||||
} else if (f === "synth") {
|
||||
curveInfo.text = qsTr("Estimated constants from lat/lon (synthetic model)")
|
||||
} else {
|
||||
curveInfo.text = qsTr("Z₀ %1 m · 7 harmonic constituents").arg(port[4][0].toFixed(2))
|
||||
}
|
||||
cv.requestPaint()
|
||||
}
|
||||
|
||||
property var moonNames: [
|
||||
qsTr("New moon"), qsTr("Waxing crescent"), qsTr("First quarter"), qsTr("Waxing gibbous"),
|
||||
qsTr("Full moon"), qsTr("Waning gibbous"), qsTr("Last quarter"), qsTr("Waning crescent")
|
||||
]
|
||||
|
||||
SilicaFlickable {
|
||||
id: flick
|
||||
anchors.fill: parent
|
||||
contentHeight: col.height
|
||||
|
||||
Column {
|
||||
id: col
|
||||
width: parent.width
|
||||
|
||||
PageHeader {
|
||||
title: page.port ? page.port[0] : ""
|
||||
description: page.port
|
||||
? (page.port[1] !== "" ? page.port[1] + " · " : "")
|
||||
+ qsTr("lat %1°").arg(page.port[2].toFixed(2))
|
||||
: ""
|
||||
}
|
||||
|
||||
// Card porto più vicino (solo città cercate) — rosso
|
||||
Rectangle {
|
||||
id: nearCard
|
||||
visible: page.custom && page.nearPort !== null
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: visible ? Theme.itemSizeSmall : 0
|
||||
radius: Theme.paddingSmall
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: Theme.errorColor
|
||||
|
||||
Label {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.paddingSmall
|
||||
text: qsTr("📍 Nearest port: %1 — %2 · ±%3 m")
|
||||
.arg(page.nearPort[0])
|
||||
.arg(page.nearKm < 222
|
||||
? (page.nearKm / 111).toFixed(1) + "°"
|
||||
: Math.round(page.nearKm) + " km")
|
||||
.arg(((page.nearPort[4][1][0] + page.nearPort[4][2][0]) * 2).toFixed(2))
|
||||
color: Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: page.nearPort,
|
||||
portIndex: Tides.PORTS.indexOf(page.nearPort),
|
||||
custom: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Intervallo (chips 24/48/72)
|
||||
Row {
|
||||
spacing: Theme.paddingMedium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Repeater {
|
||||
model: [1, 2, 3]
|
||||
delegate: Rectangle {
|
||||
width: 110
|
||||
height: Theme.itemSizeSmall
|
||||
radius: Theme.paddingMedium
|
||||
color: page.days === modelData ? Theme.highlightColor : Theme.rgba(Theme.highlightBackgroundColor, 0.2)
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: modelData === 1 ? qsTr("24 h") : (modelData === 2 ? qsTr("48 h") : qsTr("72 h"))
|
||||
color: page.days === modelData ? Theme.primaryColor : Theme.secondaryColor
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
page.days = modelData
|
||||
appSettings.days = modelData
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
// Grafico
|
||||
Rectangle {
|
||||
id: cvBox
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 250
|
||||
color: "transparent"
|
||||
|
||||
Canvas {
|
||||
id: cv
|
||||
anchors.fill: parent
|
||||
onPaint: draw()
|
||||
onWidthChanged: requestPaint()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cvMouse
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
var t = page.port ? Tides.tide(page.port, new Date(nowBase + (mouse.x / width) * (page.days * 86400000))) : 0
|
||||
var d = new Date(nowBase + (mouse.x / width) * (page.days * 86400000))
|
||||
tip.text = Tides.fmtDate(d.getTime()) + " " + Tides.fmtHM(d.getTime()) + " → " + Tides.fmtH(t, page.unit)
|
||||
tip.visible = true
|
||||
tipTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
property double nowBase: new Date().getTime()
|
||||
|
||||
Label {
|
||||
id: tip
|
||||
visible: false
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Theme.paddingSmall
|
||||
color: Theme.highlightColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
background: Rectangle { color: Theme.rgba(Theme.primaryColor, 0.1); radius: Theme.paddingSmall }
|
||||
}
|
||||
Timer { id: tipTimer; interval: 2500; onTriggered: tip.visible = false }
|
||||
}
|
||||
|
||||
Label {
|
||||
id: curveInfo
|
||||
width: parent.width - 2 * Theme.horizontalPageMargin
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
SectionHeader { text: qsTr("Next high and low tides") }
|
||||
|
||||
Repeater {
|
||||
model: eventsModel
|
||||
delegate: Item {
|
||||
width: parent.width
|
||||
height: Theme.itemSizeSmall
|
||||
Label {
|
||||
text: model.kind === "high" ? "▲ " + qsTr("High") : "▼ " + qsTr("Low")
|
||||
color: model.kind === "high" ? Theme.highlightColor : Theme.errorColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Label {
|
||||
text: model.date + " " + model.time
|
||||
color: Theme.primaryColor
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin + 100
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Label {
|
||||
text: model.height
|
||||
color: Theme.primaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Astro") }
|
||||
|
||||
Item { width: parent.width; height: Theme.itemSizeSmall
|
||||
Label { text: "🌙 " + qsTr("Moon"); color: Theme.secondaryColor; anchors.left: parent.left; anchors.leftMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { id: moonLabel; anchors.right: parent.right; anchors.rightMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
Item { width: parent.width; height: Theme.itemSizeSmall
|
||||
Label { text: "☀️ " + qsTr("Sunrise / sunset"); color: Theme.secondaryColor; anchors.left: parent.left; anchors.leftMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { id: sunLabel; anchors.right: parent.right; anchors.rightMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter; font.pixelSize: Theme.fontSizeSmall }
|
||||
}
|
||||
Item { width: parent.width; height: Theme.itemSizeSmall
|
||||
Label { text: qsTr("Typical range"); color: Theme.secondaryColor; anchors.left: parent.left; anchors.leftMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
Label { id: rangeLabel; anchors.right: parent.right; anchors.rightMargin: Theme.horizontalPageMargin; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
}
|
||||
|
||||
// disegno della curva (stesso algoritmo del mockup)
|
||||
function draw() {
|
||||
if (!port || cv.width < 10) return
|
||||
var ctx = cv.getContext("2d")
|
||||
ctx.clearRect(0, 0, cv.width, cv.height)
|
||||
var now = new Date()
|
||||
var span = days * 86400000
|
||||
var w = cv.width, h = cv.height
|
||||
var pad = 12
|
||||
var steps = Math.round(span / (10 * 60000))
|
||||
var min = 1e9, max = -1e9, i, t, ht
|
||||
var pts = []
|
||||
for (i = 0; i <= steps; i++) {
|
||||
t = now.getTime() + i * span / steps
|
||||
ht = Tides.tide(port, new Date(t))
|
||||
pts.push([t, ht])
|
||||
if (ht < min) min = ht
|
||||
if (ht > max) max = ht
|
||||
}
|
||||
min -= 0.05; max += 0.05
|
||||
function X(t) { return pad + (t - now.getTime()) / span * (w - 2 * pad) }
|
||||
function Y(hv) { return h - pad - (hv - min) / (max - min) * (h - 2 * pad) }
|
||||
|
||||
// griglia ogni 6 h
|
||||
ctx.strokeStyle = Theme.rgba(Theme.primaryColor, 0.12)
|
||||
ctx.fillStyle = Theme.rgba(Theme.secondaryColor, 0.8)
|
||||
ctx.font = "10px sans-serif"
|
||||
ctx.lineWidth = 1
|
||||
var gsteps = Math.round(span / 3600000 / 6)
|
||||
for (i = 0; i <= gsteps; i++) {
|
||||
t = now.getTime() + i * 6 * 3600000
|
||||
var x = X(t)
|
||||
ctx.beginPath(); ctx.moveTo(x, pad); ctx.lineTo(x, h - pad); ctx.stroke()
|
||||
var d = new Date(t)
|
||||
ctx.fillText((d.getHours() < 10 ? "0" : "") + d.getHours() + ":00", x + 3, h - 3)
|
||||
}
|
||||
|
||||
// Z0 tratteggiata
|
||||
ctx.setLineDash([4, 4])
|
||||
ctx.strokeStyle = Theme.rgba(Theme.secondaryColor, 0.6)
|
||||
ctx.beginPath(); ctx.moveTo(pad, Y(port[4][0])); ctx.lineTo(w - pad, Y(port[4][0])); ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
|
||||
// curva
|
||||
ctx.strokeStyle = Theme.highlightColor
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
for (i = 0; i < pts.length; i++) {
|
||||
if (i === 0) ctx.moveTo(X(pts[i][0]), Y(pts[i][1]))
|
||||
else ctx.lineTo(X(pts[i][0]), Y(pts[i][1]))
|
||||
}
|
||||
ctx.stroke()
|
||||
|
||||
// estremi
|
||||
var ex = Tides.extrema(port, now, days)
|
||||
var shown = 0
|
||||
for (i = 0; i < ex.length && shown < days * 4; i++) {
|
||||
if (ex[i].t < now.getTime() - 60000 || ex[i].t > now.getTime() + span) continue
|
||||
shown++
|
||||
var up = ex[i].kind === "high"
|
||||
ctx.fillStyle = up ? Theme.highlightColor : Theme.errorColor
|
||||
ctx.beginPath(); ctx.arc(X(ex[i].t), Y(ex[i].h), 4, 0, 7); ctx.fill()
|
||||
var d2 = new Date(ex[i].t)
|
||||
var lbl = (d2.getHours() < 10 ? "0" : "") + d2.getHours() + ":" + (d2.getMinutes() < 10 ? "0" : "") + d2.getMinutes()
|
||||
ctx.fillText(lbl, X(ex[i].t) - 16, up ? Y(ex[i].h) - 8 : Y(ex[i].h) + 16)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: init()
|
||||
|
||||
PullDownMenu {
|
||||
MenuItem {
|
||||
text: qsTr("Set as preferred port")
|
||||
visible: page.port && page.port[0] !== appSettings.preferredPort
|
||||
onClicked: {
|
||||
appSettings.preferredPort = page.port[0]
|
||||
pageStack.replace(Qt.resolvedUrl("MainPage.qml"))
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
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)
|
||||
|
||||
SilicaListView {
|
||||
id: list
|
||||
anchors.fill: parent
|
||||
model: ListModel { id: portModel }
|
||||
|
||||
header: Column {
|
||||
width: parent.width
|
||||
PageHeader { title: "Maree" }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("Ports") }
|
||||
}
|
||||
|
||||
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: {
|
||||
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() {
|
||||
geoMsg.text = qsTr("City not found (offline and not in local dataset).")
|
||||
geoMsg.visible = true
|
||||
}
|
||||
|
||||
function 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
|
||||
pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: port, portIndex: -1, custom: true })
|
||||
}
|
||||
|
||||
function geoXhr(query, cb) {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open("GET", "https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&q="
|
||||
+ encodeURIComponent(query), true)
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var j = JSON.parse(xhr.responseText)
|
||||
if (j && j.length > 0) {
|
||||
cb(j[0].name || j[0].display_name.split(",")[0],
|
||||
parseFloat(j[0].lat), parseFloat(j[0].lon), "OpenStreetMap")
|
||||
} else {
|
||||
cb(null)
|
||||
}
|
||||
} catch (e) { cb(null) }
|
||||
} else {
|
||||
cb(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
function findCity() {
|
||||
var q = geoField.text.trim()
|
||||
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, geoFound)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
rebuild()
|
||||
var pref = appSettings.preferredPort
|
||||
if (pref !== "") {
|
||||
for (var i = 0; i < Tides.PORTS.length; i++) {
|
||||
if (Tides.PORTS[i][0] === pref) {
|
||||
pageStack.push(Qt.resolvedUrl("DetailPage.qml"),
|
||||
{ port: Tides.PORTS[i], portIndex: i, custom: false })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import "../js/tides.js" as Tides
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
PageHeader { title: qsTr("Settings") }
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: col.height
|
||||
|
||||
Column {
|
||||
id: col
|
||||
width: parent.width
|
||||
|
||||
ComboBox {
|
||||
id: langBox
|
||||
label: qsTr("Language")
|
||||
currentIndex: appSettings.language === "it" ? 2
|
||||
: (appSettings.language === "en" ? 1 : 0)
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("System")
|
||||
onClicked: appSettings.language = "system"
|
||||
}
|
||||
MenuItem {
|
||||
text: "English"
|
||||
onClicked: appSettings.language = "en"
|
||||
}
|
||||
MenuItem {
|
||||
text: "Italiano"
|
||||
onClicked: appSettings.language = "it"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Language changes on restart")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingMedium }
|
||||
|
||||
ComboBox {
|
||||
id: unitBox
|
||||
label: qsTr("Units")
|
||||
currentIndex: appSettings.unit === "ft" ? 1 : 0
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("metres")
|
||||
onClicked: appSettings.unit = "m"
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("feet")
|
||||
onClicked: appSettings.unit = "ft"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: daysBox
|
||||
label: qsTr("Default range")
|
||||
currentIndex: Math.max(0, appSettings.days - 1)
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("24 h")
|
||||
onClicked: appSettings.days = 1
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("48 h")
|
||||
onClicked: appSettings.days = 2
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("72 h")
|
||||
onClicked: appSettings.days = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: prefBox
|
||||
label: qsTr("Preferred port")
|
||||
currentIndex: prefIndex()
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
text: qsTr("None")
|
||||
onClicked: appSettings.preferredPort = ""
|
||||
}
|
||||
Repeater {
|
||||
model: Tides.PORTS.length
|
||||
delegate: MenuItem {
|
||||
text: Tides.PORTS[index][0]
|
||||
onClicked: appSettings.preferredPort = Tides.PORTS[index][0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prefIndex() {
|
||||
var p = appSettings.preferredPort
|
||||
if (p === "") return 0
|
||||
for (var i = 0; i < Tides.PORTS.length; i++)
|
||||
if (Tides.PORTS[i][0] === p) return i + 1
|
||||
return 0
|
||||
}
|
||||
|
||||
SectionHeader { text: qsTr("About") }
|
||||
|
||||
Label {
|
||||
text: "Maree " + appVersion + "\n" + qsTr("Universal tide predictions")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
Label {
|
||||
text: qsTr("Harmonic model: M2, S2, N2, K1, O1, P1, K2 — known ports or synthetic estimate from lat/lon. Not for navigation.")
|
||||
color: Theme.secondaryColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.horizontalPageMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.horizontalPageMargin
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Item { width: parent.width; height: Theme.paddingLarge }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[Desktop Entry]
|
||||
Name=Maree
|
||||
Name[it_IT]=Maree
|
||||
Comment=Universal tide predictions
|
||||
Comment[it_IT]=Previsioni di marea universali
|
||||
Exec=harbour-maree
|
||||
Icon=harbour-maree
|
||||
Type=Application
|
||||
X-Nemo-Application-Type=silica-qt5
|
||||
Categories=Utility;
|
||||
|
||||
# Profilo SailJail esplicito: senza X-Sailjail la sandbox di Sailfish 5.1 usa
|
||||
# il profilo default, che nasconde ~/.config/harbour (config non persistente).
|
||||
# Con OrganizationName/ApplicationName la sandbox rende persistente
|
||||
# ~/.config/harbour/maree/ (AppConfigLocation). Internet = geocoding
|
||||
# OpenStreetMap per la ricerca libera delle città.
|
||||
[X-Sailjail]
|
||||
Permissions=Internet
|
||||
OrganizationName=harbour
|
||||
ApplicationName=maree
|
||||
@@ -0,0 +1,75 @@
|
||||
Name: harbour-maree
|
||||
Summary: Universal tide predictions
|
||||
Version: 0.1.0
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
License: MIT
|
||||
URL: https://git.hackatoniclife.com/kaneda/harbour-maree
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
Source1: %{name}.desktop
|
||||
|
||||
Requires: sailfishsilica-qt5 >= 0.10.9
|
||||
BuildRequires: pkgconfig(sailfishapp) >= 1.0.2
|
||||
BuildRequires: pkgconfig(Qt5Core)
|
||||
BuildRequires: pkgconfig(Qt5Qml)
|
||||
BuildRequires: pkgconfig(Qt5Quick)
|
||||
BuildRequires: desktop-file-utils
|
||||
|
||||
%description
|
||||
Universal tide predictions for Sailfish OS. Harmonic model
|
||||
(M2, S2, N2, K1, O1, P1, K2) computed locally: known ports with
|
||||
realistic constants, plus a free city search (OpenStreetMap
|
||||
geocoding) that falls back to a synthetic estimate from lat/lon.
|
||||
English and Italian UI, preferred port, units (m/ft) and chart
|
||||
range saved in settings.
|
||||
|
||||
%changelog
|
||||
* Fri Aug 21 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.1.0-1
|
||||
- Prima release: motore armonico a 7 costituenti, 16 porti noti,
|
||||
ricerca libera delle citta' (Nominatim + dataset locale),
|
||||
card "porto piu' vicino", grafico 24/48/72 h, fase lunare e
|
||||
alba/tramonto, lingua EN/IT (catalogo it nel data dir), porto
|
||||
preferito, unita' e intervallo salvati in QSettings
|
||||
|
||||
%prep
|
||||
%setup -q -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
%qmake5
|
||||
make %{?_smp_mflags}
|
||||
|
||||
# Catalogo di traduzione generato esplicitamente con lrelease (il template
|
||||
# sailfishapp_i18n non produce il .qm in modo affidabile sul kit 5.1).
|
||||
if [ -f %{_builddir}/../%{name}-%{version}/translations/%{name}_it.ts ]; then
|
||||
TS=%{_builddir}/../%{name}-%{version}/translations/%{name}_it.ts
|
||||
elif [ -f %{_builddir}/%{name}-%{version}/translations/%{name}_it.ts ]; then
|
||||
TS=%{_builddir}/%{name}-%{version}/translations/%{name}_it.ts
|
||||
elif [ -f %{_builddir}/translations/%{name}_it.ts ]; then
|
||||
TS=%{_builddir}/translations/%{name}_it.ts
|
||||
else
|
||||
echo "ERROR: %{name}_it.ts not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
lrelease "$TS" -qm %{_builddir}/%{name}_it.qm
|
||||
|
||||
%install
|
||||
make install INSTALL_ROOT=%{buildroot}
|
||||
mkdir -p %{buildroot}%{_datadir}/applications
|
||||
install -m 0644 %{SOURCE1} %{buildroot}%{_datadir}/applications/%{name}.desktop
|
||||
desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop || true
|
||||
|
||||
# Traduzione italiana nel data dir dell'app (il validatore RPM rifiuta
|
||||
# /usr/share/translations); main.cpp la carica da qui.
|
||||
mkdir -p %{buildroot}%{_datadir}/%{name}/translations
|
||||
install -m 0644 %{_builddir}/%{name}_it.qm %{buildroot}%{_datadir}/%{name}/translations/
|
||||
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%{_bindir}/%{name}
|
||||
%{_datadir}/applications/%{name}.desktop
|
||||
%{_datadir}/icons/hicolor/86x86/apps/%{name}.png
|
||||
%{_datadir}/icons/hicolor/108x108/apps/%{name}.png
|
||||
%{_datadir}/icons/hicolor/128x128/apps/%{name}.png
|
||||
%{_datadir}/icons/hicolor/172x172/apps/%{name}.png
|
||||
%{_datadir}/%{name}/qml
|
||||
%{_datadir}/%{name}/translations/%{name}*.qm
|
||||
@@ -0,0 +1,61 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QLocale>
|
||||
#include <QQuickView>
|
||||
#include <QQmlContext>
|
||||
#include <QTranslator>
|
||||
#include <sailfishapp.h>
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication *app = SailfishApp::application(argc, argv);
|
||||
|
||||
// Identita' allineata al profilo X-Sailjail del desktop file:
|
||||
// AppConfigLocation = ~/.config/harbour/maree (zona persistente della sandbox).
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("maree"));
|
||||
app->setApplicationVersion(QStringLiteral("0.1.0"));
|
||||
|
||||
Settings settings;
|
||||
|
||||
// Lingua scelta dall'utente (Settings -> Language): "system" segue la
|
||||
// lingua UI di Sailfish, "en" forza l'inglese (default, stringhe sorgente),
|
||||
// "it" carica il catalogo italiano dal data dir dell'app (il validatore
|
||||
// RPM rifiuta /usr/share/translations, quindi il .qm sta in
|
||||
// /usr/share/harbour-maree/translations e libsailfishapp non lo carica).
|
||||
QString lang = settings.language();
|
||||
if (lang == QLatin1String("system")) {
|
||||
const QString sys = QLocale::system().name();
|
||||
lang = sys.startsWith(QLatin1String("it")) ? QStringLiteral("it")
|
||||
: QStringLiteral("en");
|
||||
}
|
||||
if (lang == QLatin1String("it")) {
|
||||
const QList<QTranslator *> trs = app->findChildren<QTranslator *>();
|
||||
foreach (QTranslator *t, trs) {
|
||||
app->removeTranslator(t);
|
||||
t->deleteLater();
|
||||
}
|
||||
QTranslator *translator = new QTranslator(app);
|
||||
if (translator->load(QStringLiteral("harbour-maree_it"),
|
||||
QStringLiteral("/usr/share/harbour-maree/translations"))) {
|
||||
app->installTranslator(translator);
|
||||
} else {
|
||||
qWarning("harbour-maree: cannot load harbour-maree_it.qm");
|
||||
}
|
||||
}
|
||||
|
||||
QQuickView *view = SailfishApp::createView();
|
||||
|
||||
qDebug() << "harbour-maree v0.1.0 build" << __DATE__ << __TIME__;
|
||||
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings);
|
||||
view->rootContext()->setContextProperty(QStringLiteral("appVersion"),
|
||||
QCoreApplication::applicationVersion());
|
||||
|
||||
view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-maree.qml")));
|
||||
view->show();
|
||||
|
||||
return app->exec();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "settings.h"
|
||||
|
||||
Settings::Settings(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_settings(QStringLiteral("harbour"), QStringLiteral("maree"))
|
||||
{
|
||||
}
|
||||
|
||||
QString Settings::language() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("language"), QStringLiteral("system")).toString();
|
||||
}
|
||||
|
||||
void Settings::setLanguage(const QString &value)
|
||||
{
|
||||
if (value == language())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("language"), value);
|
||||
emit languageChanged();
|
||||
}
|
||||
|
||||
QString Settings::preferredPort() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("preferredPort"), QString()).toString();
|
||||
}
|
||||
|
||||
void Settings::setPreferredPort(const QString &value)
|
||||
{
|
||||
if (value == preferredPort())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("preferredPort"), value);
|
||||
emit preferredPortChanged();
|
||||
}
|
||||
|
||||
QString Settings::unit() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("unit"), QStringLiteral("m")).toString();
|
||||
}
|
||||
|
||||
void Settings::setUnit(const QString &value)
|
||||
{
|
||||
if (value == unit())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("unit"), value);
|
||||
emit unitChanged();
|
||||
}
|
||||
|
||||
int Settings::days() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("days"), 1).toInt();
|
||||
}
|
||||
|
||||
void Settings::setDays(int value)
|
||||
{
|
||||
if (value == days())
|
||||
return;
|
||||
m_settings.setValue(QStringLiteral("days"), value);
|
||||
emit daysChanged();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSettings>
|
||||
|
||||
// Preferenze dell'app, persistite in QSettings ("harbour", "maree"):
|
||||
// language "system" (segue la lingua UI Sailfish) | "en" | "it"
|
||||
// preferredPort nome del porto preferito (vuoto = nessuno)
|
||||
// unit "m" | "ft"
|
||||
// days 1 | 2 | 3 (intervallo del grafico)
|
||||
class Settings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
|
||||
Q_PROPERTY(QString preferredPort READ preferredPort WRITE setPreferredPort NOTIFY preferredPortChanged)
|
||||
Q_PROPERTY(QString unit READ unit WRITE setUnit NOTIFY unitChanged)
|
||||
Q_PROPERTY(int days READ days WRITE setDays NOTIFY daysChanged)
|
||||
|
||||
public:
|
||||
explicit Settings(QObject *parent = 0);
|
||||
|
||||
QString language() const;
|
||||
void setLanguage(const QString &value);
|
||||
|
||||
QString preferredPort() const;
|
||||
void setPreferredPort(const QString &value);
|
||||
|
||||
QString unit() const;
|
||||
void setUnit(const QString &value);
|
||||
|
||||
int days() const;
|
||||
void setDays(int value);
|
||||
|
||||
signals:
|
||||
void languageChanged();
|
||||
void preferredPortChanged();
|
||||
void unitChanged();
|
||||
void daysChanged();
|
||||
|
||||
private:
|
||||
QSettings m_settings;
|
||||
};
|
||||
|
||||
#endif // SETTINGS_H
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="en" sourcelanguage="en">
|
||||
<context>
|
||||
<name>MainPage</name>
|
||||
<message><source>Filter ports…</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Free search: any city (online)…</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Find</source><translation type="unfinished"></translation></message>
|
||||
<message><source>City not found (offline and not in local dataset).</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Found %1 (via %2)</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Searching…</source><translation type="unfinished"></translation></message>
|
||||
<message><source>local dataset</source><translation type="unfinished"></translation></message>
|
||||
<message><source>searched city</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Ports</source><translation type="unfinished"></translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DetailPage</name>
|
||||
<message><source>lat %1°</source><translation type="unfinished"></translation></message>
|
||||
<message><source>📍 Nearest port: %1 — %2 · ±%3 m</source><translation type="unfinished"></translation></message>
|
||||
<message><source>24 h</source><translation type="unfinished"></translation></message>
|
||||
<message><source>48 h</source><translation type="unfinished"></translation></message>
|
||||
<message><source>72 h</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Constants of the port of %1 (distance %2°)</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Estimated constants from lat/lon (synthetic model)</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Z₀ %1 m · 7 harmonic constituents</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Next high and low tides</source><translation type="unfinished"></translation></message>
|
||||
<message><source>High</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Low</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Moon</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Sunrise / sunset</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Typical range</source><translation type="unfinished"></translation></message>
|
||||
<message><source>age %1 d</source><translation type="unfinished"></translation></message>
|
||||
<message><source>(solar time)</source><translation type="unfinished"></translation></message>
|
||||
<message><source>New moon</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Waxing crescent</source><translation type="unfinished"></translation></message>
|
||||
<message><source>First quarter</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Waxing gibbous</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Full moon</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Waning gibbous</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Last quarter</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Waning crescent</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Set as preferred port</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Settings</source><translation type="unfinished"></translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsPage</name>
|
||||
<message><source>Language</source><translation type="unfinished"></translation></message>
|
||||
<message><source>System</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Language changes on restart</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Units</source><translation type="unfinished"></translation></message>
|
||||
<message><source>metres</source><translation type="unfinished"></translation></message>
|
||||
<message><source>feet</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Default range</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Preferred port</source><translation type="unfinished"></translation></message>
|
||||
<message><source>None</source><translation type="unfinished"></translation></message>
|
||||
<message><source>About</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Universal tide predictions</source><translation type="unfinished"></translation></message>
|
||||
<message><source>Harmonic model: M2, S2, N2, K1, O1, P1, K2 — known ports or synthetic estimate from lat/lon. Not for navigation.</source><translation type="unfinished"></translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CoverPage</name>
|
||||
<message><source>No port selected</source><translation type="unfinished"></translation></message>
|
||||
</context>
|
||||
</TS>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="it" sourcelanguage="en">
|
||||
<context>
|
||||
<name>MainPage</name>
|
||||
<message><source>Filter ports…</source><translation>Filtra porti…</translation></message>
|
||||
<message><source>Free search: any city (online)…</source><translation>Città libera: qualsiasi città (online)…</translation></message>
|
||||
<message><source>Find</source><translation>Trova</translation></message>
|
||||
<message><source>City not found (offline and not in local dataset).</source><translation>Città non trovata (offline e assente dal dataset locale).</translation></message>
|
||||
<message><source>Found %1 (via %2)</source><translation>Trovata %1 (via %2)</translation></message>
|
||||
<message><source>Searching…</source><translation>Cerco…</translation></message>
|
||||
<message><source>local dataset</source><translation>dataset locale</translation></message>
|
||||
<message><source>searched city</source><translation>città cercata</translation></message>
|
||||
<message><source>Ports</source><translation>Porti</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DetailPage</name>
|
||||
<message><source>lat %1°</source><translation>lat %1°</translation></message>
|
||||
<message><source>📍 Nearest port: %1 — %2 · ±%3 m</source><translation>📍 Porto più vicino: %1 — %2 · ±%3 m</translation></message>
|
||||
<message><source>24 h</source><translation>24 h</translation></message>
|
||||
<message><source>48 h</source><translation>48 h</translation></message>
|
||||
<message><source>72 h</source><translation>72 h</translation></message>
|
||||
<message><source>Constants of the port of %1 (distance %2°)</source><translation>Costanti del porto di %1 (distanza %2°)</translation></message>
|
||||
<message><source>Estimated constants from lat/lon (synthetic model)</source><translation>Costanti stimate da lat/lon (modello sintetico)</translation></message>
|
||||
<message><source>Z₀ %1 m · 7 harmonic constituents</source><translation>Z₀ %1 m · 7 costituenti armonici</translation></message>
|
||||
<message><source>Next high and low tides</source><translation>Prossime alte e basse</translation></message>
|
||||
<message><source>High</source><translation>Alta</translation></message>
|
||||
<message><source>Low</source><translation>Bassa</translation></message>
|
||||
<message><source>Moon</source><translation>Luna</translation></message>
|
||||
<message><source>Sunrise / sunset</source><translation>Alba / tramonto</translation></message>
|
||||
<message><source>Typical range</source><translation>Escursione tipica</translation></message>
|
||||
<message><source>age %1 d</source><translation>età %1 g</translation></message>
|
||||
<message><source>(solar time)</source><translation>(ora solare)</translation></message>
|
||||
<message><source>New moon</source><translation>Luna nuova</translation></message>
|
||||
<message><source>Waxing crescent</source><translation>Luna crescente</translation></message>
|
||||
<message><source>First quarter</source><translation>Primo quarto</translation></message>
|
||||
<message><source>Waxing gibbous</source><translation>Gibbosa crescente</translation></message>
|
||||
<message><source>Full moon</source><translation>Luna piena</translation></message>
|
||||
<message><source>Waning gibbous</source><translation>Gibbosa calante</translation></message>
|
||||
<message><source>Last quarter</source><translation>Ultimo quarto</translation></message>
|
||||
<message><source>Waning crescent</source><translation>Luna calante</translation></message>
|
||||
<message><source>Set as preferred port</source><translation>Imposta come porto preferito</translation></message>
|
||||
<message><source>Settings</source><translation>Impostazioni</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsPage</name>
|
||||
<message><source>Language</source><translation>Lingua</translation></message>
|
||||
<message><source>System</source><translation>Sistema</translation></message>
|
||||
<message><source>Language changes on restart</source><translation>La lingua cambia al riavvio</translation></message>
|
||||
<message><source>Units</source><translation>Unità</translation></message>
|
||||
<message><source>metres</source><translation>metri</translation></message>
|
||||
<message><source>feet</source><translation>piedi</translation></message>
|
||||
<message><source>Default range</source><translation>Intervallo predefinito</translation></message>
|
||||
<message><source>Preferred port</source><translation>Porto preferito</translation></message>
|
||||
<message><source>None</source><translation>Nessuno</translation></message>
|
||||
<message><source>About</source><translation>Informazioni</translation></message>
|
||||
<message><source>Universal tide predictions</source><translation>Previsioni di marea universali</translation></message>
|
||||
<message><source>Harmonic model: M2, S2, N2, K1, O1, P1, K2 — known ports or synthetic estimate from lat/lon. Not for navigation.</source><translation>Modello armonico: M2, S2, N2, K1, O1, P1, K2 — porti noti o stima sintetica da lat/lon. Non per navigazione.</translation></message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CoverPage</name>
|
||||
<message><source>No port selected</source><translation>Nessun porto selezionato</translation></message>
|
||||
</context>
|
||||
</TS>
|
||||
Reference in New Issue
Block a user