mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
feat(core,fuse,gui): Proton theme, photos on the map and an opt-in street map
- The Proton accent preference becomes a whole Proton theme that also holds against a GTK theme imported from gtk.css; folders get Proton's purple folder icon while it is on. - Places map markers show one of each town's photos with a count, and zoomed in on a town every photo sits where it was taken (Request::PlaceLocations). Photos taken on one spot are listed in a popover. "Show on map" in the photo viewer goes to the Places map. - The built-in map draws country borders, sea and land in their own colours. - An opt-in street map from OpenFreeMap (vector tiles drawn with cairo, cached on disk up to 256 MB), with the OpenStreetMap credit on the map and in About. - Translations for all catalogs; changelog folded into 2.3.0.
This commit is contained in:
1 parent
4fd14eb745
commit
fde9c2d271
37 files changed
+5836
-2204
No files matched your search
Generated
+27
@@ -3389,6 +3389,31 @@ dependencies = [
|
|||||||
"system-deps 9.0.0",
|
"system-deps 9.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pangocairo"
|
||||||
|
version = "0.22.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd199a0fef965b3ebda91a929a1ab5e7729538882c68e8de38e1904d2e094757"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-rs",
|
||||||
|
"glib",
|
||||||
|
"pango",
|
||||||
|
"pangocairo-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pangocairo-sys"
|
||||||
|
version = "0.22.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a649a616d67ae68539bc85f00fb45d36fba742ed20fc20e6712a15d8ee52bd3f"
|
||||||
|
dependencies = [
|
||||||
|
"cairo-sys-rs",
|
||||||
|
"glib-sys",
|
||||||
|
"libc",
|
||||||
|
"pango-sys",
|
||||||
|
"system-deps 9.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
@@ -3517,7 +3542,9 @@ dependencies = [
|
|||||||
"kamadak-exif",
|
"kamadak-exif",
|
||||||
"ksni",
|
"ksni",
|
||||||
"libadwaita",
|
"libadwaita",
|
||||||
|
"pangocairo",
|
||||||
"pdfs-core",
|
"pdfs-core",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ pdfs-app # the desktop app
|
|||||||
- **File Version History**: Every revision Proton Drive still holds for a file, from the browser's details pane (**Versions**) or `pdfs versions list|restore|save|rm`. Restoring is server-side — no re-upload — and an old version can be written out to a local file without touching the live one.
|
- **File Version History**: Every revision Proton Drive still holds for a file, from the browser's details pane (**Versions**) or `pdfs versions list|restore|save|rm`. Restoring is server-side — no re-upload — and an old version can be written out to a local file without touching the live one.
|
||||||
- **Photo Favourites**: Star a photo in the lightbox, filter the gallery to favourites, or use `pdfs favorite <uid>` / `pdfs photos --favorites`.
|
- **Photo Favourites**: Star a photo in the lightbox, filter the gallery to favourites, or use `pdfs favorite <uid>` / `pdfs photos --favorites`.
|
||||||
- **Photo Albums**: Browse your albums — including the ones shared with you — from the Albums view of the Photos page, or with `pdfs albums` / `pdfs album <uid>`. Album contents open in the same gallery as the timeline.
|
- **Photo Albums**: Browse your albums — including the ones shared with you — from the Albums view of the Photos page, or with `pdfs albums` / `pdfs album <uid>`. Album contents open in the same gallery as the timeline.
|
||||||
- **Places**: The Places view of the Photos page groups photos by the town they were taken in, using the location stored with each photo, as cards or on a world map. Towns are looked up offline in [GeoNames](https://www.geonames.org/) data (CC BY 4.0), and the map is drawn offline from [Natural Earth](https://www.naturalearthdata.com/) land outlines (public domain), so no map tiles are fetched; install `iso-codes` for country names in your language.
|
- **Places**: The Places view of the Photos page groups photos by the town they were taken in, using the location stored with each photo, as cards or on a world map. Towns are looked up offline in [GeoNames](https://www.geonames.org/) data (CC BY 4.0), and the map is drawn offline from [Natural Earth](https://www.naturalearthdata.com/) land outlines and country borders (public domain). Each town is marked with one of its photos, and zoomed in, every photo sits where it was taken. A street map from [OpenFreeMap](https://openfreemap.org/) (© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors) can be turned on under Appearance; it is off by default because the tile server learns which parts of the world you look at. Install `iso-codes` for country names in your language.
|
||||||
- **Human Verification (CAPTCHA) Recovery**: Detects sign-in gates (VPN/new IP challenges) and launches an embedded `WebKitWebView` dialog to safely complete the challenge, transparently retrying authentication with the earned token.
|
- **Human Verification (CAPTCHA) Recovery**: Detects sign-in gates (VPN/new IP challenges) and launches an embedded `WebKitWebView` dialog to safely complete the challenge, transparently retrying authentication with the earned token.
|
||||||
- **Selective Sync (`.pdfsignore`)**: Keep build trees, dependency directories, and editor leftovers out of synced folders using gitignore-style rules.
|
- **Selective Sync (`.pdfsignore`)**: Keep build trees, dependency directories, and editor leftovers out of synced folders using gitignore-style rules.
|
||||||
- **Data-Safe Offline Writes**: Durable scratch/staging files and a transactional pending queue preserve acknowledged writes across network failures and restarts.
|
- **Data-Safe Offline Writes**: Durable scratch/staging files and a transactional pending queue preserve acknowledged writes across network failures and restarts.
|
||||||
|
|||||||
@@ -128,11 +128,12 @@ pub struct AppConfig {
|
|||||||
/// field.
|
/// field.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prompt: Option<PromptConfig>,
|
pub prompt: Option<PromptConfig>,
|
||||||
/// Paint the desktop app in Proton purple instead of the system accent
|
/// Paint the desktop app in Proton's colours instead of the system theme
|
||||||
/// colour. `None` means "follow the system". Defaulted for configs
|
/// and accent colour. `None` means "follow the system". Defaulted for
|
||||||
/// predating the field.
|
/// configs predating the field; also read from `proton_accent`, its name
|
||||||
#[serde(default)]
|
/// while it only changed the accent.
|
||||||
pub proton_accent: Option<bool>,
|
#[serde(default, alias = "proton_accent")]
|
||||||
|
pub proton_theme: Option<bool>,
|
||||||
/// How the desktop app's My Files page lays out and orders a folder.
|
/// How the desktop app's My Files page lays out and orders a folder.
|
||||||
/// Defaulted for configs predating the field.
|
/// Defaulted for configs predating the field.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -147,6 +148,12 @@ pub struct AppConfig {
|
|||||||
/// Defaulted for configs predating the field.
|
/// Defaulted for configs predating the field.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
|
/// Draw the Places map from OpenFreeMap's street map instead of the
|
||||||
|
/// built-in outline. Off by default: the tiles come from a third party,
|
||||||
|
/// which then learns which parts of the world are looked at. Defaulted for
|
||||||
|
/// configs predating the field.
|
||||||
|
#[serde(default)]
|
||||||
|
pub online_map: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The My Files page's view choices, remembered between runs.
|
/// The My Files page's view choices, remembered between runs.
|
||||||
@@ -216,10 +223,11 @@ impl Default for AppConfig {
|
|||||||
conflict_sweep: None,
|
conflict_sweep: None,
|
||||||
open_with: None,
|
open_with: None,
|
||||||
prompt: None,
|
prompt: None,
|
||||||
proton_accent: None,
|
proton_theme: None,
|
||||||
files_view: FilesView::default(),
|
files_view: FilesView::default(),
|
||||||
tray_hidden: false,
|
tray_hidden: false,
|
||||||
language: None,
|
language: None,
|
||||||
|
online_map: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,7 +574,7 @@ mod tests {
|
|||||||
conflict_sweep: Some(SweepMode::Enforce),
|
conflict_sweep: Some(SweepMode::Enforce),
|
||||||
open_with: None,
|
open_with: None,
|
||||||
prompt: None,
|
prompt: None,
|
||||||
proton_accent: Some(true),
|
proton_theme: Some(true),
|
||||||
files_view: FilesView {
|
files_view: FilesView {
|
||||||
list: true,
|
list: true,
|
||||||
sort: FileSort::Modified,
|
sort: FileSort::Modified,
|
||||||
@@ -575,13 +583,15 @@ mod tests {
|
|||||||
},
|
},
|
||||||
tray_hidden: true,
|
tray_hidden: true,
|
||||||
language: Some("de".to_string()),
|
language: Some("de".to_string()),
|
||||||
|
online_map: true,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&config).unwrap();
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
let decoded: AppConfig = serde_json::from_str(&json).unwrap();
|
let decoded: AppConfig = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0");
|
assert_eq!(decoded.app_version, "external-drive-test-client@1.0.0");
|
||||||
assert_eq!(decoded.user_agent, "test-agent/1.0");
|
assert_eq!(decoded.user_agent, "test-agent/1.0");
|
||||||
assert_eq!(decoded.proton_accent, Some(true));
|
assert_eq!(decoded.proton_theme, Some(true));
|
||||||
assert!(decoded.tray_hidden);
|
assert!(decoded.tray_hidden);
|
||||||
|
assert!(decoded.online_map);
|
||||||
assert_eq!(decoded.language.as_deref(), Some("de"));
|
assert_eq!(decoded.language.as_deref(), Some("de"));
|
||||||
assert_eq!(decoded.upload_limit, Some(500_000));
|
assert_eq!(decoded.upload_limit, Some(500_000));
|
||||||
assert_eq!(decoded.download_limit, None);
|
assert_eq!(decoded.download_limit, None);
|
||||||
@@ -598,6 +608,14 @@ mod tests {
|
|||||||
assert!(partial.folders_first);
|
assert!(partial.folders_first);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_old_proton_accent_setting_turns_on_the_proton_theme() {
|
||||||
|
let decoded: AppConfig =
|
||||||
|
serde_json::from_str(r#"{"app_version":"a","user_agent":"b","proton_accent":true}"#)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(decoded.proton_theme, Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_default_app_config() {
|
fn test_default_app_config() {
|
||||||
let default_config = AppConfig::default();
|
let default_config = AppConfig::default();
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ pub enum Request {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
},
|
},
|
||||||
|
/// Every photo taken in one town with where exactly it was taken, for the
|
||||||
|
/// map's close-up. Replies with [`Response::PhotoLocations`].
|
||||||
|
PlaceLocations { id: u32 },
|
||||||
/// List the account's photo albums, newest activity first, including the
|
/// List the account's photo albums, newest activity first, including the
|
||||||
/// albums other people share with us. Metadata only — an album's cover
|
/// albums other people share with us. Metadata only — an album's cover
|
||||||
/// thumbnail is fetched with [`Request::PhotoThumbs`] like any other photo.
|
/// thumbnail is fetched with [`Request::PhotoThumbs`] like any other photo.
|
||||||
@@ -1468,6 +1471,15 @@ pub struct PlaceInfo {
|
|||||||
pub cover: PhotoItem,
|
pub cover: PhotoItem,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One photo and where it was taken, in decimal degrees. Part of
|
||||||
|
/// [`Response::PhotoLocations`].
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct PhotoLocation {
|
||||||
|
pub photo: PhotoItem,
|
||||||
|
pub latitude: f64,
|
||||||
|
pub longitude: f64,
|
||||||
|
}
|
||||||
|
|
||||||
/// How many of the timeline's photos a running refresh has read the metadata
|
/// How many of the timeline's photos a running refresh has read the metadata
|
||||||
/// (and so the location) of, out of how many. Part of [`Response::Places`].
|
/// (and so the location) of, out of how many. Part of [`Response::Places`].
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -1845,6 +1857,9 @@ pub enum Response {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
mapping: Option<MappingProgress>,
|
mapping: Option<MappingProgress>,
|
||||||
},
|
},
|
||||||
|
/// The photos of one town, each with its location (reply to
|
||||||
|
/// [`Request::PlaceLocations`]).
|
||||||
|
PhotoLocations { items: Vec<PhotoLocation> },
|
||||||
/// Thumbnails for a [`Request::PhotoThumbs`] batch.
|
/// Thumbnails for a [`Request::PhotoThumbs`] batch.
|
||||||
Thumbs { items: Vec<PhotoThumb> },
|
Thumbs { items: Vec<PhotoThumb> },
|
||||||
/// Per-node outcome of a [`Request::TrashNodes`] batch. Reported per uid
|
/// Per-node outcome of a [`Request::TrashNodes`] batch. Reported per uid
|
||||||
@@ -2549,6 +2564,7 @@ mod tests {
|
|||||||
offset: 60,
|
offset: 60,
|
||||||
limit: 30,
|
limit: 30,
|
||||||
},
|
},
|
||||||
|
Request::PlaceLocations { id: 2950159 },
|
||||||
] {
|
] {
|
||||||
let line = serde_json::to_string(&req).unwrap();
|
let line = serde_json::to_string(&req).unwrap();
|
||||||
let back: Request = serde_json::from_str(&line).unwrap();
|
let back: Request = serde_json::from_str(&line).unwrap();
|
||||||
|
|||||||
@@ -697,6 +697,29 @@ impl Db {
|
|||||||
Ok(rows)
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every photo taken in a town, one per group as in [`Self::photos_at_place`],
|
||||||
|
/// with where exactly it was taken. A group's own location is that of the
|
||||||
|
/// photo standing for it.
|
||||||
|
pub fn photo_locations_at_place(&self, id: u32) -> Result<Vec<(StoredPhoto, (f64, f64))>> {
|
||||||
|
let photos = self.photos_at_place(id, 0, usize::MAX >> 1)?;
|
||||||
|
let located: HashMap<String, (f64, f64)> = {
|
||||||
|
let conn = self.read();
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT uid, latitude, longitude FROM photos \
|
||||||
|
WHERE place_id = ?1 AND latitude IS NOT NULL AND longitude IS NOT NULL",
|
||||||
|
)?;
|
||||||
|
stmt.query_map(params![id], |r| Ok((r.get(0)?, (r.get(1)?, r.get(2)?))))?
|
||||||
|
.collect::<rusqlite::Result<_>>()?
|
||||||
|
};
|
||||||
|
Ok(photos
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|photo| {
|
||||||
|
let at = *located.get(&photo.uid)?;
|
||||||
|
Some((photo, at))
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets of photos with the same content hash, each set ordered with the
|
/// Sets of photos with the same content hash, each set ordered with the
|
||||||
/// copy worth keeping first, and the sets newest first.
|
/// copy worth keeping first, and the sets newest first.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -339,6 +339,30 @@ fn places_group_photos_by_the_nearest_town() {
|
|||||||
assert_eq!(berlin, ["b2", "b1-jpeg"]);
|
assert_eq!(berlin, ["b2", "b1-jpeg"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A town's photos come with where each was taken, one per RAW+JPEG shot.
|
||||||
|
#[test]
|
||||||
|
fn place_photos_carry_their_own_location() {
|
||||||
|
let db = Db::open_in_memory().unwrap();
|
||||||
|
let kreuzberg = (52.497, 13.411);
|
||||||
|
db.photos_replace(&[
|
||||||
|
located("b2", 500, "IMG_5.JPG", BERLIN),
|
||||||
|
located("b1-jpeg", 300, "IMG_3.JPG", kreuzberg),
|
||||||
|
located("b1-raw", 300, "IMG_3.CR2", kreuzberg),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let id = db.photo_places().unwrap()[0].id;
|
||||||
|
let located: Vec<(String, (f64, f64))> = db
|
||||||
|
.photo_locations_at_place(id)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(photo, at)| (photo.uid, at))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
located,
|
||||||
|
[("b2".to_owned(), BERLIN), ("b1-jpeg".to_owned(), kreuzberg)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A refresh that skips an already-resolved photo keeps where it was taken; one
|
/// A refresh that skips an already-resolved photo keeps where it was taken; one
|
||||||
/// that reads the node again takes what the node now says.
|
/// that reads the node again takes what the node now says.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -482,6 +482,10 @@ fn handle_control_conn(core: &Core, username: &str, mountpoint: &Path, stream: U
|
|||||||
Err(e) => CtlResponse::error(e),
|
Err(e) => CtlResponse::error(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(CtlRequest::PlaceLocations { id }) => match core.place_locations(id) {
|
||||||
|
Ok(items) => CtlResponse::PhotoLocations { items },
|
||||||
|
Err(e) => CtlResponse::error(e),
|
||||||
|
},
|
||||||
Ok(CtlRequest::PhotosOnThisDay { limit }) => {
|
Ok(CtlRequest::PhotosOnThisDay { limit }) => {
|
||||||
match core.photos_on_this_day(clamp_limit(limit)) {
|
match core.photos_on_this_day(clamp_limit(limit)) {
|
||||||
Ok(items) => CtlResponse::Photos {
|
Ok(items) => CtlResponse::Photos {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use pdfs_core::control::{
|
use pdfs_core::control::{
|
||||||
FileThumbRequest, MappingProgress, PhotoItem, PhotoKind, PhotoThumb, PlaceInfo,
|
FileThumbRequest, MappingProgress, PhotoItem, PhotoKind, PhotoLocation, PhotoThumb, PlaceInfo,
|
||||||
ThumbnailBuildStatus, is_raw_image_name, is_thumbnail_image_name,
|
ThumbnailBuildStatus, is_raw_image_name, is_thumbnail_image_name,
|
||||||
};
|
};
|
||||||
use pdfs_core::db::{self, StoredPhoto};
|
use pdfs_core::db::{self, StoredPhoto};
|
||||||
@@ -787,6 +787,22 @@ impl Core {
|
|||||||
Ok(photos.into_iter().map(|p| self.photo_item(p)).collect())
|
Ok(photos.into_iter().map(|p| self.photo_item(p)).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every photo taken in town `id` with its own location, as wire items.
|
||||||
|
pub(crate) fn place_locations(&self, id: u32) -> CoreResult<Vec<PhotoLocation>> {
|
||||||
|
let photos = self
|
||||||
|
.db
|
||||||
|
.photo_locations_at_place(id)
|
||||||
|
.map_err(CoreError::from)?;
|
||||||
|
Ok(photos
|
||||||
|
.into_iter()
|
||||||
|
.map(|(photo, (latitude, longitude))| PhotoLocation {
|
||||||
|
photo: self.photo_item(photo),
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// Photos taken on today's date in earlier years, as wire items. Served
|
/// Photos taken on today's date in earlier years, as wire items. Served
|
||||||
/// from the stored timeline only: the strip is a nicety, not worth a wait
|
/// from the stored timeline only: the strip is a nicety, not worth a wait
|
||||||
/// on the network, and the timeline's own load keeps that store fresh.
|
/// on the network, and the timeline's own load keeps that store fresh.
|
||||||
|
|||||||
@@ -35,5 +35,9 @@ adw = { package = "libadwaita", version = "0.9", features = ["v1_5"] }
|
|||||||
async-channel = "2"
|
async-channel = "2"
|
||||||
exif = { version = "0.6", package = "kamadak-exif" }
|
exif = { version = "0.6", package = "kamadak-exif" }
|
||||||
webkit6 = "0.6"
|
webkit6 = "0.6"
|
||||||
|
# Town and street names on the Places map, with font fallback for every script.
|
||||||
|
pangocairo = "0.22"
|
||||||
|
# Map tiles for the opt-in online map. The same build proton-sdk already pulls in.
|
||||||
|
reqwest = { version = "0.13", default-features = false, features = ["rustls", "webpki-roots"] }
|
||||||
# The system libintl (glibc) rather than a bundled gettext build.
|
# The system libintl (glibc) rather than a bundled gettext build.
|
||||||
gettext-rs = { version = "0.7", features = ["gettext-system"] }
|
gettext-rs = { version = "0.7", features = ["gettext-system"] }
|
||||||
@@ -26,5 +26,6 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=resources/pdfs.gresource.xml");
|
println!("cargo:rerun-if-changed=resources/pdfs.gresource.xml");
|
||||||
println!("cargo:rerun-if-changed=resources/icons");
|
println!("cargo:rerun-if-changed=resources/icons");
|
||||||
println!("cargo:rerun-if-changed=resources/style.css");
|
println!("cargo:rerun-if-changed=resources/style.css");
|
||||||
|
println!("cargo:rerun-if-changed=resources/proton-theme.css");
|
||||||
println!("cargo:rerun-if-changed=../../packaging/io.narl.proton-drive-linux.svg");
|
println!("cargo:rerun-if-changed=../../packaging/io.narl.proton-drive-linux.svg");
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
47.98,8.00 48.94,9.45 48.94,11.26
|
||||||
|
47.98,8.00 46.42,6.50 44.94,4.91 43.99,4.95 43.58,4.85 43.13,4.64 42.86,4.32 42.02,4.14 41.88,3.98
|
||||||
|
34.25,31.21 34.53,31.53 34.48,31.58
|
||||||
|
35.55,32.40 35.19,32.53 35.07,32.46 34.96,32.16 34.95,31.84 35.20,31.75 34.95,31.60 34.87,31.40 34.91,31.35 35.10,31.37 35.45,31.48
|
||||||
|
35.11,33.08 35.41,33.08 35.49,33.12 35.53,33.25 35.60,33.24
|
||||||
|
35.79,32.73 35.89,32.94 35.79,33.29 35.84,33.42
|
||||||
|
34.00,35.07 33.87,35.09 33.48,35.00 33.38,35.16 33.19,35.17 32.92,35.09 32.71,35.17
|
||||||
|
77.80,35.50 77.89,35.45 78.04,35.48 78.01,35.25 78.33,34.61 78.67,34.52 78.97,34.30 78.97,34.23 78.73,34.01 78.80,33.50 78.92,33.39
|
||||||
|
77.05,35.11 77.00,34.99 76.59,34.74 76.04,34.67 75.71,34.50 75.32,34.58
|
||||||
|
91.63,27.76 91.98,27.73 92.10,27.81 92.55,27.88 92.69,27.99 92.64,28.06 92.70,28.15 93.17,28.36 93.36,28.65 93.90,28.74 93.97,28.86 94.27,28.97 94.29,29.14 94.62,29.31 94.77,29.18 95.39,29.04 95.52,29.21 95.71,29.24 95.77,29.34 96.13,29.38 96.23,29.25 96.36,29.25 96.18,29.12 96.20,28.96 96.44,29.05 96.58,28.76 96.28,28.41 96.39,28.37 96.60,28.46 96.78,28.37 97.08,28.37 97.32,28.22
|
||||||
|
128.37,38.62 128.17,38.36 128.04,38.31 127.09,38.28 126.67,37.92 126.63,37.78
|
||||||
|
-8.68,27.66 -8.82,27.66 -8.75,27.19 -8.79,27.12 -9.41,27.09 -9.82,26.85 -10.03,26.91 -10.25,26.86 -10.76,27.02 -11.39,26.88 -11.32,26.74 -11.34,26.63 -11.51,26.47 -11.72,26.10 -12.06,25.99 -12.43,24.83 -12.99,24.47 -13.31,23.98 -13.89,23.69 -14.10,23.10 -14.22,22.31 -14.63,21.86 -14.61,21.75 -14.75,21.50 -14.97,21.44 -16.04,21.50 -17.00,21.42
|
||||||
|
35.92,4.62 35.76,4.81 35.80,5.16 35.75,5.34 35.47,5.42 35.33,5.36 35.27,5.49
|
||||||
|
-124.76,48.49 -123.86,48.26 -123.39,48.25 -123.14,48.40 -123.24,48.64 -123.01,48.79 -123.31,48.99
|
||||||
|
91.63,27.76 91.64,27.92 91.27,28.08 91.02,27.97 90.33,28.12
|
||||||
|
23.61,51.52 23.61,51.61 23.71,51.64 23.98,51.59 24.36,51.87 25.79,51.92 27.14,51.75 27.30,51.60 27.69,51.57 27.70,51.48 27.86,51.59 28.18,51.61 28.60,51.54 28.73,51.43 29.10,51.63 29.35,51.38 30.16,51.48 30.33,51.33 30.54,51.27 30.63,51.36 30.53,51.60 30.76,51.90 31.08,52.08 31.76,52.10
|
||||||
|
35.84,33.42 35.60,33.24
|
||||||
|
35.84,33.42 36.03,33.59 35.94,33.67 35.99,33.75 36.09,33.83 36.37,33.84 36.28,33.93 36.58,34.22 36.50,34.43 36.33,34.50 36.43,34.61 36.38,34.66 35.98,34.63
|
||||||
|
34.68,-12.01 34.76,-12.03 34.68,-12.12 34.68,-12.01
|
||||||
|
80.21,42.19 80.26,42.24 80.16,42.63 80.20,42.73 80.54,42.87 80.38,42.97 80.39,43.04 80.62,43.13 80.75,43.10 80.79,43.16 80.67,43.31 80.70,43.43 80.65,43.56 80.36,44.10 80.36,44.55 80.38,44.66 80.48,44.71 80.26,44.81 80.00,44.80 79.87,44.88 80.06,45.01 80.85,45.13 81.69,45.35 81.79,45.23 81.94,45.16 82.27,45.22 82.52,45.13 82.62,45.29 82.61,45.42 82.33,45.52 82.32,45.59 82.97,46.97 83.03,47.19 84.02,46.97 84.67,46.97 84.79,46.83 85.23,47.04 85.48,47.06 85.66,47.25 85.53,47.92 85.75,48.39 86.55,48.53 86.72,48.70 86.73,48.94 86.81,49.05 87.05,49.11 87.32,49.09
|
||||||
|
23.48,53.94 24.19,53.95 24.32,53.89 24.77,53.97 24.87,54.15 25.05,54.13 25.46,54.29 25.51,54.16 25.75,54.16 25.75,54.26 25.55,54.33 25.72,54.56 25.72,54.72 25.86,54.92 26.18,55.00 26.29,55.14 26.60,55.13 26.78,55.27 26.46,55.34 26.59,55.67
|
||||||
|
-53.37,-33.74 -53.53,-33.66 -53.53,-33.17 -53.13,-32.74 -53.16,-32.68 -53.60,-32.40 -53.76,-32.06 -54.22,-31.86 -54.59,-31.49 -54.90,-31.39 -55.04,-31.28 -55.17,-31.28 -55.56,-30.88 -55.63,-30.86 -55.81,-31.03
|
||||||
|
116.68,49.82 116.03,48.78 115.82,48.58 115.79,48.25 115.53,48.13 115.62,47.87 115.90,47.69 116.23,47.86 116.76,47.87 117.07,47.81 117.35,47.65 117.77,47.99 118.15,48.03 118.50,47.98 118.76,47.76 119.08,47.65 119.16,47.53 119.71,47.15 119.90,46.86 119.87,46.67 119.71,46.61 119.33,46.61 118.84,46.76 118.72,46.69 118.31,46.72 118.07,46.67 117.74,46.52 117.44,46.59 117.33,46.36 116.86,46.39 116.56,46.29 116.21,45.89 116.20,45.74 115.68,45.46 114.56,45.39 114.42,45.20 114.08,44.97 113.59,44.75 112.71,44.88 112.41,45.06 111.90,45.06 111.62,44.83 111.40,44.37 111.60,44.11 111.84,43.93 111.93,43.71 111.55,43.50 111.01,43.34 110.40,42.77 109.34,42.44 108.17,42.45 106.77,42.29 105.20,41.74 104.98,41.60 104.50,41.66 104.50,41.88 103.71,41.75 103.07,42.01 102.16,42.16 101.97,42.22 101.66,42.50 101.50,42.54 99.98,42.68 99.47,42.57 97.21,42.79 96.39,42.72 96.30,42.93 95.86,43.28 95.53,43.95 95.33,44.04 95.35,44.28 95.05,44.26 94.71,44.35 94.20,44.65 93.96,44.67 93.52,44.94 92.79,45.04 91.58,45.08 91.05,45.22 90.88,45.20 90.66,45.53 90.71,45.73 91.00,46.04 90.91,46.27 91.03,46.57 90.87,46.95 90.72,47.00 90.50,47.29 90.33,47.66 90.10,47.75 90.03,47.88 89.78,47.83 89.48,48.03 89.05,48.00 88.58,48.22 88.52,48.38 87.98,48.56 88.06,48.71 87.74,48.88 87.87,49.00 87.81,49.16
|
||||||
|
13.81,48.77 13.55,48.96 13.40,48.98 12.92,49.33 12.68,49.41 12.39,49.74 12.51,49.90 12.21,50.10 12.09,50.30 12.28,50.18 12.55,50.39 12.94,50.41 13.02,50.49 13.44,50.60 13.56,50.70 14.37,50.90 14.26,51.00 14.32,51.04 14.55,50.99 14.66,50.83 14.81,50.86
|
||||||
|
27.35,57.53 26.97,57.61 26.46,57.54 25.99,57.84 25.28,58.05 25.26,58.00 25.11,58.06 24.32,57.87
|
||||||
|
27.35,57.53 27.51,57.51 27.54,57.43 27.83,57.29 27.64,56.85 27.85,56.85 28.10,56.55 28.20,56.26 28.15,56.14
|
||||||
|
21.05,56.07 21.65,56.31 22.08,56.41 22.88,56.40 23.04,56.32 23.20,56.37 24.12,56.26 24.47,56.28 24.84,56.41 25.07,56.20 25.66,56.10 26.28,55.75 26.59,55.67
|
||||||
|
20.62,69.04 20.12,69.02 20.35,68.85 20.24,68.67 19.97,68.54 20.24,68.48 19.97,68.36 18.30,68.56 18.16,68.53 18.18,68.20 17.92,67.96 17.32,68.10 16.78,67.90 16.57,67.62 16.19,67.51 16.13,67.43 16.43,67.16 16.40,67.05 15.42,66.49 15.48,66.31 15.04,66.17 14.54,66.13 14.63,65.79 14.48,65.30 13.65,64.58 14.08,64.46 14.14,64.17 13.96,64.01 13.20,64.08 12.79,64.00 12.18,63.60 12.21,63.49 12.00,63.29 12.22,63.00 12.11,62.92 12.11,62.59 12.30,62.29 12.16,61.72 12.88,61.35 12.71,61.06 12.29,61.00 12.59,60.45 12.49,60.11 12.17,59.91 11.93,59.86 11.83,59.70 11.68,59.59 11.80,59.29 11.64,58.93 11.47,58.91 11.34,59.09 11.24,59.10 11.14,59.09 11.06,58.96 10.91,58.95
|
||||||
|
20.62,69.04 21.07,69.04 21.13,69.08 21.07,69.21 21.59,69.27 22.30,68.86 22.41,68.72 23.32,68.65 23.71,68.71 23.85,68.81 24.94,68.59 25.09,68.64 25.25,68.82 25.58,68.89 25.75,68.99 25.77,69.28 26.07,69.69 26.53,69.92 27.13,69.91 27.75,70.06 27.89,70.06 28.41,69.82 29.14,69.67 29.33,69.47 28.85,69.18 28.83,69.12 28.97,69.02
|
||||||
|
104.43,10.41 104.56,10.52 104.85,10.53 105.05,10.70 105.05,10.91 105.31,10.85 105.41,10.95 105.76,10.99 105.85,10.86 106.16,10.79 106.16,11.04 105.86,11.29 105.85,11.64 106.01,11.76 106.40,11.69 106.41,11.95 106.70,11.98 106.76,12.05 106.93,12.08 107.21,12.30 107.33,12.32 107.39,12.26 107.51,12.36 107.56,12.54 107.48,13.03 107.61,13.44 107.33,14.13 107.36,14.37 107.49,14.55 107.52,14.71
|
||||||
|
-69.51,-17.50 -69.31,-17.94 -69.09,-18.05 -69.15,-18.14 -68.97,-18.97 -68.46,-19.43 -68.70,-19.72 -68.56,-19.97 -68.60,-20.04 -68.76,-20.09 -68.69,-20.31 -68.76,-20.42 -68.48,-20.63 -68.56,-20.72 -68.56,-20.90 -68.44,-20.95 -68.20,-21.30 -68.19,-21.62 -68.08,-21.98 -67.99,-22.06 -67.88,-22.49 -67.88,-22.82 -67.58,-22.89 -67.19,-22.82
|
||||||
|
-69.51,-17.51 -69.85,-17.70 -69.80,-17.99 -69.93,-18.21 -70.18,-18.33 -70.42,-18.35
|
||||||
|
6.34,49.45 6.49,49.80 6.20,49.92 6.11,50.03 6.12,50.12
|
||||||
|
2.52,51.10 2.60,50.88 2.76,50.75 2.84,50.71 3.11,50.78 3.23,50.66 3.27,50.53 3.60,50.48 3.69,50.31 3.95,50.34 4.17,50.25 4.15,49.97 4.55,49.96 4.82,50.15 4.86,50.14 4.79,49.96 4.87,49.79 5.28,49.68 5.51,49.51 5.79,49.54
|
||||||
|
6.12,50.12 5.98,50.17 5.74,49.92 5.72,49.81 5.88,49.64 5.79,49.54
|
||||||
|
46.43,41.89 45.95,42.04 45.64,42.21 45.71,42.50 45.34,42.53 45.16,42.68 44.87,42.76 44.77,42.62 44.64,42.73 44.51,42.75 43.83,42.57 43.74,42.62 43.78,42.75 43.09,42.99 42.99,43.09 42.76,43.17 42.57,43.16 42.42,43.22 41.58,43.22 40.65,43.53 40.15,43.57 39.98,43.42
|
||||||
|
20.96,40.85 20.93,40.90 20.71,40.93 20.49,41.27 20.45,41.52 20.52,41.57 20.57,41.87
|
||||||
|
20.57,41.87 20.58,42.01 20.49,42.22 20.24,42.34 20.06,42.55
|
||||||
|
41.51,41.52 41.82,41.43 41.93,41.50 42.47,41.44 42.61,41.58 42.75,41.58 43.15,41.31 43.21,41.20 43.40,41.18 43.44,41.11
|
||||||
|
101.14,21.57 101.20,21.52 101.25,21.20 101.54,21.23 101.70,21.15 101.80,21.21 101.72,21.31 101.74,21.83 101.60,21.99 101.52,22.25 101.74,22.50 101.84,22.39 102.02,22.44 102.13,22.38
|
||||||
|
73.63,39.45 73.82,39.49 73.91,39.58 73.84,39.80 73.99,40.04 74.41,40.14 74.68,40.31 74.83,40.33 74.84,40.48 75.11,40.45 75.56,40.63 75.68,40.31 75.87,40.30 76.26,40.43 76.32,40.35 76.48,40.45 76.66,40.78 76.91,41.02 77.58,40.99 78.12,41.08 78.44,41.42 79.29,41.78 79.77,41.90 79.84,42.00 80.22,42.03 80.21,42.19
|
||||||
|
43.44,41.11 44.23,41.21 44.84,41.21 44.81,41.26 45.00,41.29
|
||||||
|
80.21,42.19 79.92,42.41 79.43,42.48 79.13,42.78 76.99,42.97 75.84,42.94 75.64,42.81 75.37,42.84 74.82,42.98 74.21,43.24 73.95,43.20 73.56,43.00 73.42,42.59 73.49,42.41 72.28,42.76 71.76,42.82 71.26,42.73 70.89,42.34 70.95,42.25
|
||||||
|
-56.48,1.94 -56.84,1.88 -57.12,2.01 -57.32,1.96 -57.55,1.73 -57.98,1.65 -58.03,1.52 -58.34,1.59 -58.40,1.48 -58.51,1.44 -58.51,1.28 -58.68,1.28 -58.82,1.20 -59.23,1.38 -59.54,1.70 -59.67,1.75 -59.67,1.84 -59.76,1.90 -59.76,2.27 -59.89,2.36 -59.99,2.69 -59.97,2.99 -59.83,3.35 -59.85,3.59 -59.68,3.70 -59.55,3.93 -59.74,4.23 -59.70,4.38 -59.83,4.48 -60.15,4.53 -60.03,4.74 -59.99,5.08 -60.14,5.24 -60.74,5.20
|
||||||
|
8.39,55.10 8.59,54.92 9.25,54.81 9.74,54.83
|
||||||
|
9.52,30.23 9.90,30.39 10.22,30.78 10.26,30.94 10.11,31.46 10.27,31.68 10.48,31.74 10.61,31.93 10.83,32.08 11.51,32.41 11.53,32.52 11.45,32.64 11.50,33.18
|
||||||
|
45.00,41.29 45.19,41.15 45.07,41.08 45.42,40.99 45.59,40.85 45.38,40.67 45.45,40.53 45.57,40.42 45.96,40.23 45.89,40.02 45.58,39.98 46.20,39.59 46.48,39.56 46.48,39.48 46.37,39.40 46.58,39.22 46.40,39.19 46.49,39.07 46.49,38.91
|
||||||
|
44.77,39.70 45.03,39.77 45.17,39.57 45.25,39.60 45.46,39.49 45.78,39.55 45.77,39.38 45.98,39.24 45.95,39.18 46.11,38.88
|
||||||
|
20.24,46.11 20.51,46.17 20.61,46.13 20.76,46.25 21.12,46.28 21.26,46.41 21.32,46.61 21.50,46.70 21.66,47.04 22.00,47.40 22.00,47.51 22.29,47.73 22.61,47.77 22.88,47.95
|
||||||
|
70.96,40.24 71.30,40.29 71.63,40.22 71.69,40.15 71.96,40.26 72.13,40.44 72.36,40.40 72.40,40.58 72.60,40.53 73.14,40.81 72.66,40.87 72.36,41.04 72.19,41.03 72.16,41.17 71.88,41.20 71.86,41.31 71.66,41.54 71.60,41.50 71.59,41.33 71.42,41.34 71.39,41.12 70.86,41.22 70.69,41.45 70.47,41.41 70.20,41.51 70.18,41.57 70.63,41.88 70.80,41.92 70.86,42.03 71.23,42.16 71.04,42.28 70.95,42.25
|
||||||
|
18.83,49.51 18.94,49.50 18.97,49.40 19.15,49.40 19.44,49.60 19.63,49.41 19.77,49.37 19.76,49.20 20.06,49.18 20.16,49.32 20.36,49.39 20.62,49.39 20.87,49.31 21.08,49.42 21.64,49.41 21.89,49.34 22.02,49.21 22.54,49.07
|
||||||
|
-7.22,55.09 -7.38,55.03 -7.55,54.77 -7.91,54.70 -7.75,54.59 -8.12,54.48 -8.12,54.41 -7.85,54.22 -7.61,54.14 -7.32,54.13 -7.16,54.24 -7.20,54.30 -7.01,54.41 -6.80,54.21 -6.65,54.16 -6.65,54.06 -6.22,54.09
|
||||||
|
18.83,49.51 18.81,49.61 18.59,49.76 18.56,49.88 18.03,50.04 17.87,49.97 17.63,50.12 17.59,50.16 17.74,50.23 17.70,50.31 17.42,50.25 16.88,50.43 16.99,50.24 16.64,50.10 16.21,50.42 16.42,50.57 16.28,50.66 16.01,50.61 15.73,50.74 15.35,50.81 15.26,50.96 14.99,51.01 14.98,50.89 14.81,50.86
|
||||||
|
70.96,40.24 70.95,40.19 70.64,40.08 70.62,40.00 70.52,39.95 70.45,40.05 69.97,40.20 69.53,40.10 69.48,39.92 69.31,39.97 69.28,39.92 69.23,39.76 69.30,39.52 70.50,39.59 70.80,39.39 71.00,39.41 71.07,39.49 71.47,39.60 71.50,39.48 71.73,39.42 71.73,39.31 71.78,39.28 72.04,39.35 72.23,39.21 72.36,39.34 72.56,39.38 73.11,39.36 73.63,39.45
|
||||||
|
20.00,39.71 20.25,39.68 20.31,39.80 20.38,39.80 20.31,39.98 20.66,40.12 20.81,40.45 20.95,40.49 21.03,40.62 20.96,40.85
|
||||||
|
33.20,-14.01 33.15,-13.94 32.99,-14.02 32.87,-13.82 32.77,-13.76 32.80,-13.69 32.67,-13.59 32.85,-13.46 32.97,-13.22 33.00,-12.90 32.95,-12.80 33.02,-12.63 33.40,-12.49 33.51,-12.35 33.34,-12.31 33.25,-12.11 33.30,-11.69 33.23,-11.42 33.38,-11.16 33.26,-10.89 33.46,-10.78 33.66,-10.55 33.53,-10.23 33.31,-10.04 33.35,-9.86 33.20,-9.63 33.00,-9.62 32.92,-9.41
|
||||||
|
22.13,48.41 22.14,48.57 22.30,48.69 22.54,49.07
|
||||||
|
-11.51,6.91 -11.27,7.23 -10.65,7.76 -10.57,8.07 -10.36,8.19 -10.28,8.49
|
||||||
|
24.15,8.67 24.22,8.61 24.21,8.37 24.29,8.29 24.85,8.14 25.20,7.81 25.25,7.72 25.18,7.56 25.28,7.43 25.89,7.06 26.36,6.64 26.31,6.46 26.51,6.07 27.14,5.72 27.33,5.19
|
||||||
|
42.92,11.00 43.25,11.50
|
||||||
|
42.92,11.00 42.56,11.08 41.96,10.94 41.80,10.98 41.79,11.69 42.38,12.47
|
||||||
|
29.40,-4.45 29.32,-4.90 29.61,-5.72 29.48,-6.03 29.54,-6.31 29.71,-6.62 30.21,-7.04 30.75,-8.19
|
||||||
|
42.38,12.47 42.48,12.51 42.70,12.38 42.87,12.62 43.12,12.71
|
||||||
|
13.81,48.77 14.05,48.60 14.69,48.60 14.79,48.75 14.92,48.77 14.97,48.98 15.07,49.00 15.83,48.86 16.06,48.75 16.37,48.74 16.54,48.80 16.88,48.70 16.95,48.60
|
||||||
|
42.36,37.11 42.77,37.37 42.94,37.32 43.09,37.37 43.68,37.23 44.11,37.30 44.19,37.25 44.20,37.05 44.28,36.98 44.61,37.18 44.77,37.14
|
||||||
|
16.09,46.86 16.25,46.97 16.45,47.01 16.49,47.12 16.42,47.22 16.44,47.40 16.62,47.45 16.68,47.54 16.64,47.61 16.42,47.67 16.59,47.75 16.79,47.68 17.07,47.71 17.03,47.84 17.15,48.01
|
||||||
|
7.02,45.93 7.13,45.88 7.54,45.98 7.79,45.92 7.99,46.02 8.13,46.16 8.10,46.27 8.42,46.45 8.46,46.25 8.64,46.11 8.82,46.08 8.78,46.00 8.95,45.83 9.02,45.85 9.00,46.01 9.25,46.29 9.26,46.48 9.43,46.48 9.53,46.31 9.94,46.36 10.04,46.24 10.13,46.24 10.04,46.48 10.09,46.60 10.43,46.55 10.40,46.67 10.45,46.86
|
||||||
|
10.45,46.86 10.35,46.98 10.13,46.85 9.88,46.94 9.85,47.01 9.58,47.06
|
||||||
|
44.77,37.14 44.79,37.29 44.57,37.44 44.55,37.66 44.59,37.71 44.21,37.91 44.45,38.33 44.30,38.39 44.27,38.84 44.17,38.93 44.18,39.14 44.08,39.22 44.02,39.38 44.39,39.42 44.46,39.67 44.59,39.77 44.82,39.65
|
||||||
|
23.48,53.94 23.45,54.14 23.37,54.20 22.89,54.39 22.77,54.36
|
||||||
|
30.75,-8.19 30.89,-8.47 31.03,-8.60 31.35,-8.61 31.53,-8.71 31.67,-8.91 31.92,-8.94 31.94,-9.05 32.43,-9.16 32.92,-9.41
|
||||||
|
7.20,53.28 7.19,53.00 7.03,52.65 6.71,52.62 6.75,52.46 6.97,52.44 7.04,52.38 6.98,52.21 6.72,52.08 6.80,51.97 6.52,51.85 6.36,51.82 6.17,51.88 5.95,51.80 6.20,51.45 6.08,51.22 6.13,51.15 5.86,51.03 6.05,50.90 5.99,50.75
|
||||||
|
9.58,47.06 9.61,47.11 9.53,47.27
|
||||||
|
-8.49,7.56 -8.43,7.60 -8.23,7.56 -8.01,8.08 -8.05,8.17 -8.26,8.25 -8.24,8.46 -8.05,8.50 -7.82,8.47 -7.74,8.38 -7.68,8.41 -7.72,8.64 -7.95,8.79 -7.94,8.98 -7.78,9.08 -7.92,9.19 -7.90,9.42 -8.03,9.40 -8.14,9.50 -8.16,9.97 -7.99,10.16
|
||||||
|
7.49,43.77 7.48,43.86 7.68,44.08 7.64,44.16 7.32,44.14 6.90,44.34 6.84,44.51 7.03,44.72 6.99,44.83 6.74,44.92 6.63,45.07 6.69,45.14 6.84,45.14 7.08,45.24 7.15,45.38 6.79,45.74 6.80,45.81 7.02,45.93
|
||||||
|
28.97,69.02 28.41,68.90 28.77,68.84 28.48,68.54 28.56,68.35 28.69,68.19 29.34,68.06 29.99,67.67 29.94,67.55 29.24,67.10 29.07,66.89 29.90,66.09 30.09,65.79 30.10,65.68 29.72,65.62 29.82,65.57 29.73,65.47 29.71,65.34 29.61,65.25 29.81,65.20 29.83,65.15 29.62,65.04 29.60,64.97 29.78,64.80 30.11,64.73 30.12,64.64 29.99,64.52 30.11,64.37 30.49,64.24 30.53,64.08 30.21,63.80 29.99,63.74 31.18,63.21 31.53,62.89 31.19,62.48 29.25,61.29 28.41,60.90 27.73,60.46
|
||||||
|
30.87,69.78 30.92,69.61 30.86,69.54 30.62,69.53 30.18,69.64 30.19,69.54 30.09,69.43 29.39,69.30 29.21,69.10 28.97,69.02
|
||||||
|
55.19,22.70 55.20,23.03 55.53,23.82 55.47,23.94 55.99,24.06 55.93,24.22 55.76,24.24 55.80,24.87 55.92,24.97 56.00,24.95 55.97,24.86 56.06,24.74 56.39,24.98
|
||||||
|
48.57,41.84 48.39,41.60 48.06,41.46 47.79,41.20 47.32,41.28 47.14,41.52 46.75,41.81 46.57,41.80 46.54,41.87 46.43,41.89
|
||||||
|
-54.62,2.33 -54.72,2.44 -54.88,2.45 -54.98,2.60 -55.39,2.44 -55.73,2.41 -55.96,2.52 -56.14,2.26 -55.92,2.04 -55.93,1.89 -56.02,1.84 -56.48,1.94
|
||||||
|
46.43,41.89 46.20,41.74 46.18,41.66 46.31,41.51 46.67,41.29 46.53,41.09 46.43,41.08 46.17,41.20 45.92,41.19 45.73,41.26 45.72,41.34 45.28,41.45 45.00,41.29
|
||||||
|
-11.39,12.40 -11.45,12.56 -11.39,12.94 -11.63,13.37 -11.76,13.39 -11.83,13.32 -12.05,13.63 -11.96,13.88 -12.02,13.97 -12.02,14.21 -12.23,14.46 -12.19,14.65 -12.28,14.81
|
||||||
|
-54.62,2.33 -54.13,2.12 -53.77,2.35 -53.51,2.25 -53.33,2.34 -53.23,2.20 -52.90,2.21 -52.58,2.53 -52.33,3.18 -51.99,3.70 -51.65,4.06
|
||||||
|
2.71,6.37 2.77,6.71 2.72,6.98 2.79,7.48 2.69,7.87 2.77,9.05 3.04,9.08 3.15,9.32 3.14,9.45 3.33,9.67 3.35,9.81 3.56,9.91 3.65,10.16 3.58,10.29 3.65,10.41 3.77,10.42 3.83,10.61 3.72,11.08 3.49,11.40 3.60,11.70
|
||||||
|
13.07,-4.63 12.80,-4.43 12.38,-4.62 12.31,-4.77 12.20,-4.78 12.02,-5.00
|
||||||
|
16.52,46.50 16.32,46.53 16.24,46.48 16.23,46.37 16.07,46.37 15.64,46.20 15.59,46.14 15.67,46.05 15.65,45.86 15.28,45.73 15.35,45.66 15.28,45.58 15.34,45.47 14.79,45.48 14.57,45.66 14.37,45.48 13.99,45.51 13.88,45.43 13.58,45.52
|
||||||
|
18.91,45.93 18.67,45.91 18.53,45.80 18.36,45.75 17.81,45.79 17.31,46.00 16.87,46.34 16.52,46.50
|
||||||
|
20.24,46.11 19.61,46.17 19.28,46.00 19.09,46.02 18.91,45.93
|
||||||
|
16.52,46.50 16.28,46.86 16.09,46.86
|
||||||
|
51.27,24.61 50.97,24.57 50.80,24.79
|
||||||
|
25.26,-17.79 25.24,-17.97 25.94,-18.94 25.95,-19.08 26.17,-19.54 26.68,-19.89 27.18,-20.10 27.26,-20.23 27.28,-20.48 27.68,-20.50 27.67,-21.06 28.01,-21.55 29.03,-21.80 29.04,-22.02 29.36,-22.19
|
||||||
|
77.80,35.50 77.45,35.48 76.77,35.66
|
||||||
|
28.01,41.97 27.53,41.92 27.24,42.09 27.01,42.06 26.58,41.95 26.51,41.83 26.36,41.80 26.32,41.72
|
||||||
|
102.93,11.71 102.74,12.09 102.76,12.43 102.50,12.67 102.46,13.02 102.33,13.29 102.34,13.56 102.55,13.59 102.54,13.66 102.91,14.14 103.20,14.33 103.60,14.42 103.98,14.36 104.78,14.43 104.97,14.37 105.00,14.25 105.07,14.23 105.18,14.35
|
||||||
|
88.89,27.32 88.74,27.18 88.86,26.96 89.15,26.82 89.33,26.85 89.55,26.80 89.61,26.72 90.12,26.75 90.35,26.89 90.74,26.77 91.29,26.79 91.43,26.87 91.67,26.80 92.05,26.87 92.07,26.98 91.99,27.10 92.08,27.29 91.99,27.45 91.74,27.44 91.59,27.56 91.63,27.76
|
||||||
|
104.07,1.28 104.06,1.40 103.82,1.46 103.70,1.44 103.60,1.21
|
||||||
|
100.12,6.44 100.18,6.67 100.26,6.68 100.35,6.55 100.75,6.46 100.87,6.25 101.05,6.24 101.08,5.96 100.98,5.77 101.11,5.64 101.26,5.79 101.56,5.91 101.68,5.78 101.87,5.83 102.10,6.24
|
||||||
|
5.99,50.75 6.24,50.60 6.17,50.55 6.20,50.50 6.34,50.45 6.36,50.32 6.18,50.23 6.12,50.12
|
||||||
|
-71.78,19.72 -71.71,19.49 -71.75,19.29 -71.65,19.16 -71.81,18.99 -71.74,18.73 -71.87,18.61 -72.00,18.60 -71.76,18.34 -71.77,18.04
|
||||||
|
14.06,13.08 13.61,13.70
|
||||||
|
46.53,29.10 46.91,29.54 47.15,30.00 47.33,30.08 47.64,30.10 47.98,29.98
|
||||||
|
88.11,27.87 88.14,27.95 88.58,28.09 88.80,28.01 88.85,27.87 88.75,27.52 88.89,27.32
|
||||||
|
-90.10,13.74 -90.05,13.90 -89.55,14.24 -89.57,14.39 -89.36,14.42
|
||||||
|
124.04,-9.34 124.09,-9.42 124.28,-9.43 124.41,-9.31 124.44,-9.19
|
||||||
|
124.92,-8.94 124.94,-9.05 125.10,-9.00 125.15,-9.04 125.10,-9.19 124.96,-9.21 125.07,-9.51
|
||||||
|
115.14,4.90 115.27,4.63 115.29,4.35 115.11,4.39 115.03,4.90
|
||||||
|
109.63,2.03 109.54,1.90 109.64,1.78 109.65,1.61 110.51,0.86 111.10,1.05 111.81,1.01 111.92,1.11 112.08,1.14 112.19,1.44 112.48,1.56 112.94,1.57 113.01,1.43 113.62,1.24 113.90,1.43 114.39,1.50 114.51,1.45 114.70,1.85 114.83,1.98 114.76,2.16 114.79,2.25 115.18,2.52 115.08,2.63 115.12,2.89 115.25,3.03 115.45,3.03 115.57,3.45 115.57,3.94 115.68,4.19 115.84,4.33 116.02,4.29 116.24,4.36 116.41,4.31 116.51,4.37 117.10,4.34 117.54,4.17 117.88,4.19
|
||||||
|
10.45,46.86 10.99,46.78 11.24,46.98 11.78,46.99 12.17,47.08 12.15,46.94 12.39,46.70 13.70,46.52
|
||||||
|
13.70,46.52 13.38,46.26 13.63,46.18 13.48,46.01 13.61,45.96 13.58,45.81 13.87,45.61 13.72,45.59
|
||||||
|
29.01,-2.72 29.10,-2.60 29.30,-2.67 29.39,-2.81 29.70,-2.79 29.87,-2.72 29.93,-2.34 30.12,-2.42 30.41,-2.31 30.55,-2.40
|
||||||
|
92.57,21.98 92.63,21.31 92.57,21.26 92.33,21.44 92.18,21.29 92.32,20.79
|
||||||
|
1.43,42.60 1.29,42.71 0.70,42.85 0.63,42.69 -0.04,42.69 -0.30,42.83 -0.59,42.80 -0.76,42.94 -1.18,43.02 -1.30,43.10 -1.46,43.05 -1.41,43.24 -1.71,43.31 -1.79,43.41
|
||||||
|
1.43,42.60 1.50,42.64 1.71,42.60 1.71,42.50
|
||||||
|
1.71,42.50 2.03,42.35 2.20,42.42 2.65,42.34 2.89,42.46 3.21,42.43
|
||||||
|
1.43,42.60 1.45,42.44 1.71,42.50
|
||||||
|
74.89,37.23 75.12,37.39 74.89,37.60 74.94,37.77 74.78,38.19 74.81,38.46 74.28,38.66 74.13,38.66 74.03,38.54 73.80,38.61 73.70,38.85 73.81,38.97 73.61,39.23 73.63,39.45
|
||||||
|
74.89,37.23 74.73,37.29 74.37,37.16 74.54,37.02
|
||||||
|
20.96,40.85 21.40,40.91 21.58,40.87 21.99,41.13 22.60,41.14 22.72,41.18 22.78,41.33 22.92,41.34
|
||||||
|
21.56,42.25 21.39,42.22 21.29,42.10 21.06,42.17 20.78,42.07 20.72,41.87 20.57,41.87
|
||||||
|
22.34,42.31 22.24,42.36 21.56,42.25
|
||||||
|
22.34,42.31 22.52,42.44 22.44,42.63 22.47,42.84 22.71,42.88 22.98,43.19 22.55,43.45 22.37,43.78 22.42,44.01 22.60,44.08 22.63,44.19 22.71,44.24
|
||||||
|
22.92,41.34 23.00,41.74 22.84,41.99 22.58,42.10 22.34,42.31
|
||||||
|
18.44,42.56 18.52,42.43
|
||||||
|
18.44,42.56 17.80,42.90 17.67,42.90
|
||||||
|
78.92,33.39 79.12,33.23
|
||||||
|
20.34,42.83 20.47,42.86 20.48,42.95 20.62,43.03 20.62,43.20 20.80,43.26 20.84,43.17 21.06,43.09 21.40,42.83 21.39,42.75 21.75,42.67 21.61,42.39 21.52,42.33 21.56,42.25
|
||||||
|
20.34,42.83 20.05,42.76 20.06,42.55
|
||||||
|
19.19,43.53 19.22,43.45 19.61,43.17 20.27,42.94 20.34,42.83
|
||||||
|
-89.36,14.42 -89.12,14.37 -88.71,14.03 -88.51,13.98 -88.48,13.85 -88.15,13.99 -87.99,13.88 -87.80,13.89 -87.72,13.81 -87.78,13.52 -87.74,13.45 -87.81,13.40
|
||||||
|
-87.34,12.98 -87.01,13.01 -86.92,13.22 -86.73,13.28 -86.76,13.75 -86.33,13.77 -86.04,14.05 -85.98,13.97 -85.73,13.86 -85.68,13.98 -85.18,14.34 -85.16,14.53 -85.04,14.61 -84.99,14.75 -84.86,14.81 -84.65,14.66 -84.45,14.64 -84.24,14.75 -84.15,14.72 -83.64,14.88 -83.54,14.98 -83.16,14.99
|
||||||
|
-75.28,-0.11 -75.78,0.09 -76.07,0.35 -76.27,0.44 -76.39,0.40 -76.43,0.26 -76.49,0.24 -76.83,0.25 -77.40,0.39 -77.47,0.64 -77.65,0.72 -77.70,0.84 -77.83,0.83 -78.18,0.97 -78.86,1.46
|
||||||
|
-57.60,3.37 -57.65,3.52 -57.83,3.68 -58.03,4.00 -58.05,4.17 -57.85,4.67 -57.92,4.82 -57.88,4.88 -57.71,4.99 -57.33,5.02 -57.21,5.20 -57.32,5.34 -57.19,5.55
|
||||||
|
-54.08,3.33 -53.99,3.59 -54.35,4.05 -54.48,4.84 -54.45,5.01 -54.16,5.36
|
||||||
|
-58.41,-33.94 -58.48,-33.65 -58.38,-33.14 -58.24,-33.11 -58.13,-32.98 -58.20,-32.47
|
||||||
|
-57.61,-30.19 -57.87,-30.59 -57.81,-30.86 -57.90,-30.98 -57.89,-31.20 -58.05,-31.49 -57.99,-31.58 -58.01,-31.68 -58.19,-31.92 -58.12,-32.25 -58.20,-32.47
|
||||||
|
-54.62,-25.58 -54.44,-25.62 -54.15,-25.52 -53.89,-25.67 -53.67,-26.29 -53.75,-26.75 -53.72,-26.88 -53.84,-27.12 -54.16,-27.25 -54.33,-27.42 -54.83,-27.55 -54.91,-27.71 -55.10,-27.87 -55.73,-28.20 -55.69,-28.38 -55.89,-28.37 -56.94,-29.59 -57.22,-29.78 -57.41,-30.03 -57.61,-30.19
|
||||||
|
20.62,69.04 20.90,68.98 20.92,68.91 22.00,68.52 22.85,68.37 23.10,68.26 23.18,68.14 23.32,68.13 23.64,67.95 23.50,67.88 23.54,67.61 23.45,67.46 23.73,67.42 23.77,67.33 23.63,67.23 23.64,67.13 23.99,66.81 23.67,66.38 23.72,66.22 23.91,66.15 24.16,65.81 24.19,65.61 24.12,65.51
|
||||||
|
4.23,51.39 4.17,51.31 3.90,51.21 3.58,51.29 3.43,51.25 3.35,51.38
|
||||||
|
5.99,50.75 5.75,50.76 5.64,50.84 5.75,50.95 5.80,51.15 5.48,51.29 5.21,51.28 5.03,51.47 4.85,51.40 4.76,51.49 4.63,51.42 4.50,51.47 4.38,51.43 4.37,51.36 4.23,51.39
|
||||||
|
6.34,49.45 6.53,49.39 6.74,49.16 6.89,49.21 7.04,49.11 7.45,49.15 7.61,49.06 8.13,48.97 8.14,48.89 7.84,48.64 7.62,48.16 7.53,47.67 7.62,47.59
|
||||||
|
9.52,47.52 9.18,47.67 8.88,47.66 8.57,47.78 8.40,47.69 8.57,47.65 8.45,47.60 7.62,47.59
|
||||||
|
9.53,47.27 9.63,47.47 9.52,47.52
|
||||||
|
9.58,47.06 9.49,47.06 9.53,47.27
|
||||||
|
7.02,45.93 6.77,46.17 6.82,46.28 6.76,46.42 6.43,46.43 6.23,46.33 6.27,46.25 6.20,46.19 6.01,46.14 5.97,46.21 6.10,46.28 6.12,46.38 6.07,46.46 6.16,46.61 6.41,46.76 6.46,46.95 6.67,47.03 7.00,47.32 6.90,47.39 6.97,47.45 7.14,47.49 7.34,47.43 7.62,47.59
|
||||||
|
-7.41,37.18 -7.50,37.59 -7.44,37.73 -7.19,38.01 -7.02,38.04 -6.96,38.19 -7.11,38.18 -7.34,38.46 -7.28,38.71 -7.05,38.91 -7.00,39.06 -7.17,39.14 -7.34,39.47 -7.54,39.66 -7.12,39.68 -6.98,39.80 -6.90,40.02 -7.03,40.17 -6.81,40.34 -6.84,40.78 -6.93,41.01 -6.21,41.53 -6.31,41.64 -6.54,41.67 -6.62,41.94 -7.15,41.98 -7.21,41.90 -7.40,41.83 -7.92,41.88 -8.15,41.81 -8.22,41.90 -8.13,42.02 -8.27,42.14 -8.78,41.94
|
||||||
|
13.81,48.77 13.79,48.59 13.72,48.54 13.49,48.58 13.37,48.36 12.76,48.11 12.95,47.89 12.90,47.72 13.05,47.66 13.01,47.48 12.81,47.54 12.77,47.64 12.69,47.67 12.48,47.64 12.21,47.72 12.19,47.62 11.72,47.58 11.04,47.39 10.87,47.52 10.44,47.55 10.37,47.37 10.18,47.28 10.20,47.36 10.07,47.39 9.97,47.51 9.75,47.58 9.52,47.52
|
||||||
|
16.09,46.86 15.98,46.80 15.96,46.68 15.77,46.71 15.44,46.63 14.89,46.61 14.55,46.40 13.70,46.52
|
||||||
|
18.44,42.56 18.55,42.64 18.47,42.78 18.46,43.00 18.62,43.03 18.67,43.23 18.85,43.35 19.03,43.29 18.95,43.53 19.19,43.53
|
||||||
|
19.19,43.53 19.30,43.59 19.45,43.56 19.50,43.64 19.25,43.97 19.58,44.01 19.12,44.36 19.35,44.88 18.84,44.88 18.66,45.08 18.00,45.14 17.81,45.08 17.65,45.16 17.50,45.12 17.13,45.17 16.92,45.28 16.79,45.20 16.53,45.22 16.29,45.01 16.03,45.19 15.79,45.18 15.74,44.77 16.10,44.52 16.30,44.12 17.08,43.52 17.27,43.45 17.29,43.31 17.65,43.01 17.59,42.94
|
||||||
|
19.01,44.87 19.10,44.97 19.06,45.14 19.14,45.20 19.40,45.21 19.00,45.40 19.06,45.52 18.92,45.60 18.95,45.66 18.84,45.84 18.91,45.93
|
||||||
|
16.95,48.60 17.14,48.84 17.76,48.89 18.09,49.07 18.16,49.26 18.60,49.49 18.83,49.51
|
||||||
|
17.15,48.01 16.87,48.39 16.95,48.60
|
||||||
|
17.15,48.01 17.32,47.99 17.76,47.77 18.72,47.79 18.79,48.00 19.47,48.11 19.63,48.22 19.95,48.15 20.33,48.30 20.49,48.53 21.45,48.55 21.72,48.35 22.13,48.41
|
||||||
|
14.81,50.86 15.02,51.25 14.91,51.46 14.72,51.52 14.74,51.63 14.60,51.83 14.75,52.08 14.55,52.36 14.62,52.53 14.13,52.88 14.41,53.22 14.21,53.95
|
||||||
|
22.88,47.95 22.77,48.11 22.58,48.13 22.35,48.26 22.25,48.41 22.13,48.41
|
||||||
|
28.59,43.74 28.22,43.77 28.05,43.82 27.88,43.99 27.74,43.96 27.43,44.02 27.09,44.17 26.22,44.01 25.82,43.77 25.50,43.67 23.22,43.87 22.92,43.83 22.87,43.95 23.03,44.08 22.71,44.24
|
||||||
|
20.24,46.11 20.71,45.74 20.77,45.75 20.79,45.47 21.49,45.15 21.35,45.01 21.53,44.90 21.36,44.83
|
||||||
|
22.92,41.34 23.64,41.39 24.01,41.46 24.06,41.53 24.49,41.56 24.77,41.36 24.99,41.36 25.25,41.24 25.92,41.31 26.14,41.39 26.07,41.67 26.11,41.73 26.41,41.70 26.58,41.60 26.62,41.40 26.33,41.24 26.33,40.95 26.04,40.73
|
||||||
|
20.06,42.55 19.79,42.48 19.73,42.63 19.65,42.63 19.28,42.17 19.36,42.07 19.34,41.87
|
||||||
|
26.62,48.26 26.79,48.26 26.98,48.16 27.61,47.34 28.07,46.98 28.24,46.64 28.24,46.45 28.10,45.97 28.16,45.65 28.07,45.60 28.21,45.45
|
||||||
|
22.88,47.95 23.20,48.08 23.41,47.99 24.18,47.91 24.58,47.93 24.89,47.72 24.98,47.72 25.46,47.91 26.16,47.99 26.31,48.20 26.62,48.26
|
||||||
|
22.54,49.07 22.81,49.02 22.85,49.06 22.71,49.17 22.73,49.30 22.65,49.54 22.71,49.61 23.71,50.38 23.97,50.41 24.09,50.53 23.98,50.79 24.10,50.87 23.66,51.31 23.61,51.52
|
||||||
|
20.90,55.29 21.39,55.28 22.07,55.06 22.57,55.06 22.82,54.87 22.68,54.56 22.77,54.36
|
||||||
|
31.76,52.10 31.58,52.31 31.62,52.55 31.52,52.70 31.56,52.76 31.26,53.02 31.42,53.20 31.67,53.20 31.85,53.11 32.14,53.09 32.70,53.34 32.71,53.42 32.47,53.55 32.45,53.69 32.20,53.78 31.75,53.81 31.83,54.03 31.40,54.20 31.18,54.45 31.07,54.49 31.15,54.63 30.80,54.78 30.83,54.91 30.98,55.05 30.81,55.31 30.90,55.40 30.91,55.57 30.23,55.85 29.94,55.85 29.48,55.68 29.35,55.78 29.38,55.94 29.03,56.02 28.79,55.94 28.56,56.09 28.28,56.06 28.15,56.14
|
||||||
|
31.76,52.10 32.12,52.05 32.28,52.11 32.44,52.31 32.81,52.25 33.15,52.34 33.74,52.34 33.92,52.25 34.11,51.98 34.40,51.78 34.38,51.72 34.12,51.68 34.27,51.34 34.21,51.26 34.71,51.17 35.06,51.20 35.16,51.06 35.31,51.04 35.44,50.73 35.41,50.54 35.59,50.37 35.67,50.35 35.89,50.44 36.12,50.41 36.31,50.28 36.50,50.28 36.62,50.21 36.76,50.29 37.42,50.41 37.70,50.11 38.05,49.92 38.26,50.05 38.92,49.82 39.17,49.86 39.30,49.74 39.46,49.73 39.78,49.57 40.08,49.58 40.11,49.25 39.89,49.06 39.69,49.01 39.75,48.91 40.00,48.82 39.79,48.81 39.64,48.59 39.84,48.54 39.89,48.36 39.85,48.30 39.96,48.27 39.78,47.89 39.66,47.84 38.90,47.86 38.64,47.67 38.37,47.61 38.29,47.56 38.20,47.32 38.28,47.28 38.20,47.18 38.21,47.09
|
||||||
|
43.44,41.11 43.63,40.93 43.72,40.72 43.57,40.48 43.71,40.17 43.67,40.13 44.01,40.01 44.29,40.04 44.78,39.68
|
||||||
|
87.82,49.16 88.12,49.26 88.19,49.45 88.83,49.45 88.90,49.54 89.01,49.47 89.11,49.50 89.24,49.63 89.40,49.61 89.65,49.72 89.64,49.90 89.98,49.98 90.05,50.09 90.66,50.22 91.02,50.42 91.42,50.47 91.80,50.69 92.19,50.70 92.35,50.86 92.63,50.69 92.78,50.78 92.94,50.78 93.10,50.60 94.25,50.56 94.35,50.22 94.61,50.02 94.93,50.04 95.11,49.94 95.52,49.91 95.79,50.01 95.94,49.96 96.07,50.00 96.32,49.90 96.99,49.88 97.21,49.73 97.36,49.74 97.59,49.91 98.00,50.01 98.25,50.30 98.28,50.53 98.03,50.64 97.84,51.05 98.04,51.45 98.22,51.51 98.35,51.72 98.64,51.80 98.89,52.12 99.72,51.87 99.92,51.76 100.47,51.73 101.38,51.45 102.11,51.35 102.22,50.83 102.32,50.72 102.29,50.59 102.68,50.39 103.16,50.29 103.30,50.20 103.63,50.14 104.08,50.15 104.47,50.31 105.38,50.47 106.22,50.30 106.71,50.31 107.23,49.99 107.92,49.95 107.97,49.65 108.61,49.32 109.24,49.33 110.71,49.14 111.34,49.36 112.08,49.42 112.49,49.53 112.81,49.52 113.06,49.62 113.16,49.80 113.57,50.01 114.30,50.27 114.74,50.23 115.43,49.90 115.72,49.88 116.22,50.01 116.55,49.92 116.68,49.82
|
||||||
|
116.68,49.82 117.25,49.62 117.87,49.51 118.45,49.84 119.26,50.07 119.35,50.28 119.16,50.41 119.45,50.70 120.07,51.60 120.51,51.85 120.75,52.10 120.67,52.30 120.70,52.49 120.66,52.57 120.17,52.60 120.07,52.63 120.04,52.72 120.70,53.17 120.99,53.28 122.34,53.49 123.61,53.55 124.81,53.13 125.08,53.20 125.65,53.04 125.73,52.89 125.87,52.87 126.05,52.74 126.02,52.61 126.34,52.36 126.51,51.93 126.70,51.70 126.71,51.57 126.81,51.51 126.92,51.10 127.31,50.71 127.34,50.35 127.59,50.21 127.49,49.98 127.55,49.80 127.71,49.67 128.00,49.57 128.70,49.60 128.82,49.46 129.07,49.37 129.50,49.39 130.20,48.89 130.55,48.86 130.62,48.77 130.55,48.60 130.80,48.34 130.71,48.13 130.73,48.02 130.92,47.84 130.96,47.71 132.48,47.71 132.71,47.95 133.14,48.11 133.47,48.10 133.84,48.27 134.29,48.37 134.67,48.25 134.67,48.15 134.57,48.02 134.75,47.72 134.54,47.49 134.29,47.41 134.17,47.30 134.20,47.13 133.87,46.50 133.86,46.25 133.70,46.14 133.65,45.96 133.51,45.88 133.44,45.60 133.19,45.49 133.11,45.13 132.94,45.03 131.98,45.24 131.85,45.33 131.45,44.98 130.98,44.84 131.26,44.07 131.17,43.70 131.18,43.51 131.26,43.38 131.07,42.90 130.58,42.81 130.42,42.73 130.58,42.62 130.53,42.54
|
||||||
|
130.53,42.54 130.25,42.74 130.24,42.89 129.90,43.00 129.70,42.45 129.52,42.38 129.31,42.41 129.20,42.22 128.92,42.04 128.05,41.99 128.03,41.90 128.29,41.61 128.15,41.39 128.01,41.45 127.69,41.44 127.18,41.53 126.95,41.77 126.72,41.72 126.60,41.64 126.49,41.36 125.99,40.90 125.73,40.87 125.42,40.66 124.89,40.46 124.39,40.10 124.36,40.00
|
||||||
|
130.53,42.54 130.69,42.30
|
||||||
|
70.96,40.24 70.65,40.20 70.37,40.38 70.38,40.45 70.75,40.72 70.40,41.04 70.29,40.89 69.71,40.66 69.41,40.80 69.36,40.77 69.31,40.63 69.21,40.57 69.30,40.33 69.22,40.29 69.27,40.20 68.95,40.22 68.63,40.17 68.97,40.09 68.80,40.05 68.87,39.91 68.64,39.84 68.59,39.63 68.46,39.54 67.72,39.62 67.43,39.47 67.36,39.22 67.65,39.13 67.69,38.99 68.04,38.98 68.13,38.93 68.05,38.67 68.09,38.47 68.35,38.21 68.29,38.03 67.81,37.49 67.76,37.17
|
||||||
|
35.89,35.92 36.15,35.83 36.20,35.94 36.35,36.00 36.38,36.17 36.64,36.23 36.54,36.46 36.66,36.80 36.94,36.76 37.07,36.65 37.44,36.64 38.19,36.90 38.44,36.86 38.77,36.69 39.36,36.68 40.02,36.83 40.71,37.10 41.52,37.09 42.06,37.21 42.20,37.30 42.31,37.23 42.36,37.11
|
||||||
|
35.45,31.48 35.40,31.23 35.44,31.13 35.14,30.42 35.14,30.14 34.97,29.56
|
||||||
|
35.55,32.40 35.56,31.77 35.45,31.48
|
||||||
|
35.61,32.68 35.55,32.40
|
||||||
|
88.11,27.87 87.14,27.84 86.61,28.10 86.52,27.96 86.41,27.93 86.14,28.11 86.08,28.08 86.06,27.93 85.99,27.91 85.68,28.28 85.12,28.32 85.09,28.37 85.16,28.59 84.80,28.56 84.23,28.91 84.10,29.22 83.94,29.28 83.58,29.18 83.16,29.61 82.85,29.68 82.22,30.06 82.04,30.33 81.64,30.39 81.42,30.34 81.18,30.04 81.11,30.04 81.01,30.16
|
||||||
|
97.32,28.22 97.34,27.94 96.88,27.59 96.90,27.44 97.10,27.12 96.95,27.13 96.73,27.33 96.19,27.26 95.46,26.76 95.13,26.60 95.05,26.35 95.13,26.04 95.02,25.91 94.95,25.70 94.58,25.32 94.55,25.22 94.71,25.05 94.29,24.32 94.13,23.88 93.68,24.01 93.49,23.97 93.33,24.06 93.41,23.68 93.39,23.34 93.35,23.08 93.31,23.03 93.16,23.03 93.08,22.72 93.15,22.23 93.04,22.18 92.96,22.00 92.85,22.01 92.69,22.13 92.57,21.98
|
||||||
|
100.12,20.32 99.89,20.42 99.72,20.33 99.46,20.36 99.49,20.15 99.34,20.08 99.07,20.10 98.92,19.77 98.37,19.69 98.02,19.75 97.82,19.46 97.80,19.13 97.71,18.93 97.75,18.59 97.52,18.50 97.37,18.52 97.45,18.36 97.63,18.29 97.74,17.94 97.71,17.80 98.44,16.98 98.48,16.73 98.66,16.33 98.84,16.42 98.89,16.35 98.82,16.18 98.59,16.05 98.56,15.37 98.19,15.20 98.20,14.98 98.25,14.81 98.57,14.36 98.93,14.05 99.14,13.72 99.18,13.23 99.12,13.03 99.22,12.74 99.41,12.55 99.61,11.78 99.19,11.11 98.76,10.66 98.70,10.19
|
||||||
|
103.09,18.14 103.09,18.14
|
||||||
|
103.09,18.14 103.26,18.28 103.29,18.41 103.95,18.32 104.43,17.70 104.74,17.46 104.82,17.30 104.74,16.88 104.82,16.47 105.05,16.16 105.41,15.99 105.40,15.83 105.64,15.66 105.49,15.26 105.55,14.93 105.50,14.59 105.42,14.47 105.18,14.35
|
||||||
|
100.12,20.32 100.14,20.25 100.32,20.39 100.52,20.18 100.54,20.09 100.40,19.76 100.51,19.55 100.74,19.51 100.97,19.61 101.21,19.55 101.20,19.33 101.29,18.98 101.05,18.44 101.14,18.14 100.91,17.58 100.96,17.54 101.17,17.50 101.69,17.89 101.77,18.03 101.95,18.08 102.10,18.21 102.55,17.97 102.66,17.82 102.81,17.95 103.05,18.03 103.09,18.14
|
||||||
|
101.14,21.57 101.08,21.47 100.70,21.25 100.54,20.99 100.52,20.92 100.62,20.86 100.25,20.73 100.12,20.32
|
||||||
|
97.32,28.22 97.54,28.51 97.60,28.52 98.06,28.19 98.30,27.55 98.45,27.66 98.65,27.57 98.74,26.79 98.69,26.19 98.56,26.07 98.66,25.86 98.47,25.79 98.33,25.59 98.14,25.57 98.01,25.29 97.82,25.25 97.71,25.03 97.74,24.87 97.58,24.77 97.53,24.49 97.67,24.38 97.71,24.23 97.56,23.91 97.69,23.90 98.21,24.11 98.58,24.07 98.84,24.12 98.68,23.91 98.83,23.62 98.80,23.52 98.88,23.38 98.86,23.19 99.42,23.07 99.51,22.96 99.39,22.83 99.34,22.50 99.21,22.28 99.19,22.13 99.92,22.03 99.94,21.76 100.10,21.66 100.15,21.48 100.60,21.47 101.08,21.76 101.13,21.74 101.14,21.57
|
||||||
|
102.13,22.38 102.58,21.90 102.66,21.68 102.74,21.68 102.82,21.81 102.95,21.68 102.85,21.27 103.10,20.89 103.64,20.70 104.10,20.95 104.58,20.65 104.37,20.44 104.62,20.37 104.70,20.21 104.85,20.20 104.93,20.02 104.59,19.62 104.03,19.68 104.06,19.48 103.89,19.30 104.72,18.80 105.15,18.65 105.11,18.41 105.33,18.19 105.46,18.15 105.69,17.74 106.50,16.95 106.55,16.65 106.66,16.49 106.74,16.45 106.85,16.52 106.93,16.35 107.40,16.04 107.39,15.95 107.17,15.80 107.65,15.26 107.48,14.98 107.52,14.71
|
||||||
|
27.40,5.11 27.07,5.20 26.82,5.06 25.53,5.31 25.40,5.26 25.25,5.02 25.07,4.97 24.77,4.93 24.32,4.99 23.42,4.66 23.12,4.74 22.86,4.72 22.71,4.59 22.42,4.13 21.69,4.28 21.54,4.24 20.56,4.46 20.23,4.83 19.81,5.09 19.50,5.13 19.07,4.89 18.83,4.52 18.59,4.35 18.61,3.48
|
||||||
|
30.55,-2.40 30.42,-2.64 30.47,-2.69 30.43,-2.87 30.78,-2.98 30.79,-3.27 30.63,-3.35 30.63,-3.42 30.43,-3.59 30.15,-4.09 29.95,-4.31 29.72,-4.46 29.40,-4.45
|
||||||
|
29.01,-2.72 29.02,-2.80 29.22,-3.05 29.21,-3.83 29.40,-4.45
|
||||||
|
29.58,-1.39 29.35,-1.52 29.20,-1.72 29.13,-1.86 29.13,-2.20 28.88,-2.40 28.89,-2.64 29.01,-2.72
|
||||||
|
35.27,5.49 34.98,5.86 34.71,6.66 34.06,7.23 33.90,7.51 33.67,7.67 33.23,7.76 33.00,7.90 33.28,8.44 33.95,8.44 34.07,8.55 34.08,9.46
|
||||||
|
22.86,10.92 22.49,11.00 22.04,10.82 21.77,10.64 21.71,10.57 21.68,10.29 21.53,10.21 21.40,10.00 21.26,9.97 20.77,9.41 20.34,9.13 20.07,9.13 19.67,9.02 19.15,9.02 18.96,8.94 18.89,8.84 19.11,8.66 18.56,8.05 17.65,7.98 16.78,7.55 16.55,7.87 16.40,7.77 16.38,7.68 15.96,7.51 15.48,7.52
|
||||||
|
13.29,2.16 13.22,2.26 11.56,2.30 11.35,2.30 11.33,2.17
|
||||||
|
11.13,-3.92 11.23,-3.69 11.50,-3.52 11.69,-3.68 11.88,-3.67 11.83,-3.53 11.93,-3.32 11.72,-3.18 11.76,-2.98 11.64,-2.86 11.54,-2.84 11.61,-2.34 11.73,-2.39 11.95,-2.34 12.06,-2.41 12.45,-2.33 12.43,-1.93 12.59,-1.83 12.79,-1.93 12.99,-2.31 13.46,-2.40 13.73,-2.14 13.88,-2.33 13.89,-2.47 13.99,-2.49 14.20,-2.35 14.16,-2.22 14.25,-2.00 14.38,-1.89 14.47,-0.57 14.21,-0.43 14.07,-0.27 13.86,-0.20 13.95,0.35 14.09,0.54 14.32,0.62 14.43,0.90 14.18,1.37 13.85,1.42 13.22,1.25 13.25,1.37 13.17,1.79 13.29,2.16
|
||||||
|
15.48,7.52 15.55,7.79 15.44,7.85 15.12,8.56 14.33,9.20 13.98,9.69 14.24,9.98 14.84,9.94 15.65,10.01 15.28,10.36 15.13,10.65 15.03,11.11 15.12,11.54 15.08,11.85 14.88,12.27 14.85,12.50 14.76,12.66 14.54,12.82 14.46,13.02 14.06,13.08
|
||||||
|
14.06,13.08 14.20,12.38 14.52,12.30 14.62,12.15 14.58,11.53 14.20,11.27 13.98,11.21 13.70,10.87 13.54,10.61 13.41,10.17 13.27,10.04 13.20,9.56 12.93,9.43 12.78,8.82 12.58,8.62 12.40,8.60 12.23,8.28 12.02,7.59 11.77,7.27 11.86,7.12 11.58,6.89 11.48,6.60 11.24,6.45 11.11,6.46 11.01,6.74 10.61,7.06 10.48,6.89 10.21,6.89 10.14,7.00 9.78,6.76 9.66,6.53 9.00,5.92 8.80,5.20 8.56,4.76
|
||||||
|
3.60,11.70 3.66,11.76 3.61,11.89 3.65,12.53 3.95,12.78 4.04,12.93 4.15,13.46 4.66,13.73 5.24,13.76 5.49,13.87 6.30,13.66 6.59,13.41 6.87,13.04 7.01,13.00 7.83,13.34 8.10,13.29 8.75,12.91 9.62,12.81 10.05,13.21 10.48,13.33 11.41,13.35 11.99,13.19 12.12,13.09 12.46,13.09 12.65,13.33 12.87,13.45 13.32,13.67 13.61,13.70
|
||||||
|
1.19,6.09 0.98,6.32 0.74,6.45 0.53,6.85 0.63,7.35 0.51,7.44 0.50,7.55 0.61,7.73 0.58,8.15 0.69,8.35 0.37,8.76 0.49,8.85 0.47,9.12 0.53,9.40 0.41,9.49 0.26,9.43 0.23,9.46 0.34,9.60 0.26,9.64 0.32,9.69 0.38,10.29 -0.09,10.67 0.01,11.02 -0.07,11.12
|
||||||
|
-2.70,9.48 -2.67,9.28 -2.75,9.05 -2.60,8.80 -2.51,8.21 -2.61,8.15 -2.61,8.05 -2.79,7.93 -2.99,7.20 -3.24,6.81 -3.24,6.65 -3.00,5.71 -2.96,5.64 -2.79,5.60 -2.76,5.36 -2.82,5.15 -3.11,5.09
|
||||||
|
-5.52,10.43 -5.46,10.77 -5.49,11.04 -5.35,11.13 -5.25,11.38 -5.29,11.83 -5.11,11.97 -4.70,12.08 -4.43,12.34 -4.42,12.58 -4.48,12.67 -4.23,12.79 -4.33,13.12 -4.15,13.31 -3.95,13.40 -3.53,13.18 -3.30,13.28 -3.25,13.66 -2.95,13.65 -2.87,13.95 -2.59,14.23 -2.46,14.27 -2.11,14.17 -1.97,14.46 -1.70,14.51 -1.05,14.82 -0.76,15.05 -0.54,15.08 -0.41,15.01 -0.24,15.06 0.22,14.91
|
||||||
|
-2.70,9.48 -2.82,9.43 -2.99,9.69 -3.22,9.90 -3.79,9.92 -4.27,9.74 -4.33,9.65 -4.63,9.71 -4.97,9.93 -5.10,10.24 -5.38,10.31 -5.52,10.43
|
||||||
|
-8.49,7.56 -8.30,6.98 -8.33,6.80 -8.60,6.51 -8.29,6.32 -7.89,6.23 -7.80,5.98 -7.45,5.84 -7.40,5.55 -7.43,5.32 -7.57,5.08 -7.59,4.82 -7.54,4.35
|
||||||
|
-8.49,7.56 -8.66,7.69 -8.89,7.26 -9.12,7.22 -9.26,7.38 -9.46,7.42 -9.37,7.70 -9.52,8.35 -9.68,8.48 -9.74,8.45 -9.78,8.54 -10.06,8.43 -10.15,8.52 -10.28,8.49
|
||||||
|
-5.52,10.43 -5.84,10.39 -6.03,10.19 -6.20,10.23 -6.26,10.72 -6.43,10.67 -6.42,10.56 -6.65,10.66 -6.69,10.35 -6.95,10.34 -7.02,10.14 -7.36,10.26 -7.50,10.44 -7.66,10.43 -7.81,10.24 -7.99,10.16
|
||||||
|
-15.04,10.94 -14.68,11.51 -14.27,11.66 -13.73,11.74 -13.74,12.01 -13.95,12.18 -13.71,12.31 -13.67,12.48 -13.73,12.67
|
||||||
|
-11.39,12.40 -11.50,12.20 -11.31,12.02 -10.93,12.21 -10.71,11.90 -10.47,12.14 -10.27,12.21 -9.75,12.03 -9.66,12.14 -9.36,12.26 -9.40,12.46 -9.22,12.48 -9.04,12.40 -8.82,11.92 -8.82,11.67 -8.40,11.37 -8.67,11.01 -8.47,11.05 -8.34,10.99 -8.27,10.49 -8.01,10.32 -7.99,10.16
|
||||||
|
107.52,14.71 107.41,14.56 107.29,14.59 106.94,14.33 106.78,14.34 106.50,14.58 106.35,14.45 106.23,14.48 106.17,14.37 105.98,14.34 106.12,14.05 106.07,13.92 105.90,13.92 105.74,14.08 105.53,14.16 105.35,14.11 105.18,14.35
|
||||||
|
114.06,4.60 114.22,4.48 114.32,4.26 114.42,4.26 114.57,4.05 114.65,4.04 114.78,4.17 114.84,4.39 114.75,4.72 115.03,4.90
|
||||||
|
-88.23,15.73 -88.96,15.15 -89.14,15.07 -89.22,14.87 -89.17,14.61 -89.36,14.42
|
||||||
|
-77.37,8.66 -77.48,8.50 -77.20,7.97 -77.35,7.84 -77.35,7.71 -77.54,7.57 -77.59,7.54 -77.76,7.70 -77.74,7.54 -77.90,7.23
|
||||||
|
-60.74,5.20 -61.39,5.94 -61.13,6.21 -61.20,6.59 -61.15,6.69 -60.87,6.79 -60.72,6.77 -60.35,7.00 -60.35,7.15 -60.58,7.16 -60.63,7.21 -60.61,7.32 -60.72,7.54 -60.51,7.81 -60.38,7.83 -60.03,8.05 -59.96,8.19 -59.85,8.25 -59.83,8.31 -60.02,8.55
|
||||||
|
-60.74,5.20 -60.67,5.16 -60.60,4.95 -61.00,4.54 -61.28,4.52 -61.55,4.29 -61.82,4.20 -62.15,4.10 -62.41,4.16 -62.71,4.02 -62.76,3.67 -62.86,3.59 -62.97,3.59 -63.34,3.94 -63.53,3.89 -64.02,3.93 -64.19,4.13 -64.58,4.14 -64.79,4.28 -64.82,4.23 -64.67,4.01 -64.22,3.59 -64.22,3.20 -64.01,2.67 -64.05,2.50 -63.39,2.41 -63.43,2.16 -64.01,1.93 -64.11,1.62 -64.21,1.53 -64.30,1.46 -64.49,1.45 -64.73,1.25 -65.10,1.11 -65.47,0.69 -65.56,0.69 -65.52,0.84 -65.68,0.98 -66.06,0.79 -66.35,0.77 -66.88,1.22
|
||||||
|
52.49,41.78 53.06,42.15 54.12,42.34 54.85,41.97 55.43,41.30 55.98,41.32
|
||||||
|
55.98,41.32 57.02,41.26 57.12,41.35 57.02,41.45 56.96,41.86 57.29,42.12 57.81,42.19 58.03,42.49 58.28,42.43 58.40,42.29 58.47,42.30 58.15,42.63 58.26,42.69 58.48,42.66 58.59,42.78 58.88,42.56 59.16,42.51 59.35,42.32 59.86,42.30 59.99,42.21 59.94,41.97 60.20,41.80 60.08,41.76 60.14,41.59 60.09,41.40 60.45,41.22 60.87,41.25 61.24,41.19 61.50,41.28 61.90,41.09 62.10,40.68 62.38,40.33 62.48,39.98 63.51,39.38 63.76,39.16 64.16,38.95 64.31,38.98 65.61,38.24 65.97,38.24 66.57,38.01 66.63,37.93 66.53,37.79 66.52,37.35
|
||||||
|
49.23,46.34 48.54,46.61 48.50,46.70 48.56,46.76 48.88,46.71 48.96,46.77 48.55,47.32 48.17,47.71 47.48,47.80 47.29,47.74 47.09,47.95 47.12,48.13 47.06,48.23 46.66,48.41 46.61,48.57 46.70,48.81 47.03,49.15 46.80,49.37 46.89,49.70 46.99,49.85 47.25,50.00 47.33,50.27 47.50,50.40 47.71,50.38 48.33,49.86 48.43,49.83 48.76,49.93 48.84,50.01 48.63,50.61 48.81,50.60 49.32,50.85 49.50,51.08 49.82,51.13 50.31,51.32 50.79,51.73 51.16,51.65 51.34,51.48 51.61,51.48 52.22,51.71 52.57,51.48 53.34,51.48 54.14,51.04 54.42,50.78 54.47,50.58 54.56,50.54 54.65,50.66 54.55,50.95 54.64,51.01 55.36,50.67 55.69,50.58 56.05,50.71 56.14,50.84 56.49,51.02 56.62,50.98 57.01,51.07 57.44,50.89 57.65,50.93 57.84,51.09 58.36,51.06 58.88,50.69 59.45,50.62 59.52,50.58 59.52,50.49 59.81,50.58 59.96,50.80 60.06,50.85 60.42,50.68 60.94,50.70 61.39,50.86 61.59,51.23 61.55,51.32 61.36,51.44 60.46,51.65 60.39,51.77 60.03,51.93 60.43,52.13 60.67,52.15 60.99,52.34 60.77,52.68 61.05,52.97 61.97,52.94 62.08,53.01 62.01,53.11 61.66,53.23 61.20,53.29 61.16,53.34 61.23,53.45 61.53,53.52 61.47,53.58 61.25,53.55 60.98,53.62 61.11,53.75 61.14,53.96 61.23,54.02 61.33,54.05 61.93,53.95 63.85,54.24 64.46,54.38 64.93,54.40 65.09,54.34 65.24,54.52 65.48,54.62 65.71,54.62 65.91,54.69 66.22,54.67 68.16,54.98 68.24,55.05 68.21,55.16 68.52,55.20 68.98,55.39 69.49,55.36 70.18,55.16 70.49,55.28 70.74,55.31 70.91,55.13 71.19,54.60 71.15,54.36 71.05,54.26 71.09,54.21 71.34,54.16 72.00,54.21 72.19,54.33 72.39,54.12 72.45,53.94 72.59,54.00 72.56,54.09 72.62,54.13 72.91,54.11 73.23,53.96 73.62,54.07 73.71,54.04 73.68,53.93 73.40,53.81 73.31,53.71 73.29,53.60 73.41,53.45 73.86,53.62 74.21,53.58 74.35,53.49 74.45,53.65 74.83,53.83 75.22,53.89 75.38,53.97 75.44,54.09 76.84,54.44 76.65,54.15 76.42,54.15 76.48,54.02 77.86,53.27 79.47,51.49 79.99,50.77 80.07,50.76 80.09,50.84 80.22,50.91 80.42,50.95 80.45,51.18 80.74,51.29 80.88,51.28 80.97,51.19 81.13,51.19 81.07,50.97 81.39,50.96 81.47,50.74 82.49,50.73 82.76,50.89 83.36,50.99 83.95,50.77 84.18,50.52 84.32,50.24 84.99,50.06 85.00,49.89 85.23,49.62 86.18,49.50 86.68,49.78 86.73,49.70 86.63,49.56 86.81,49.49 87.00,49.29 87.23,49.22 87.32,49.09
|
||||||
|
34.20,31.32 34.90,29.48
|
||||||
|
33.98,4.22 34.38,4.62
|
||||||
|
41.88,3.98 41.14,3.96 40.77,4.27 39.84,3.85 39.54,3.47 39.23,3.48 38.09,3.65 36.91,4.41 36.08,4.45 35.92,4.62
|
||||||
|
23.38,-17.64 21.42,-18.00 21.11,-17.96 20.75,-18.02 20.39,-17.89 18.96,-17.80 18.72,-17.70 18.40,-17.40 14.02,-17.41 13.48,-17.04 13.10,-16.97 12.55,-17.21 12.01,-17.17 11.74,-17.25
|
||||||
|
25.26,-17.79 25.64,-17.82 26.00,-17.97 26.14,-17.91 26.33,-17.93 26.78,-18.04 27.02,-17.96 27.93,-16.90 28.76,-16.53 28.83,-16.42 28.91,-15.99 29.49,-15.70 30.40,-15.64
|
||||||
|
23.38,-17.64 24.27,-17.48 24.73,-17.52 25.00,-17.57 25.26,-17.79
|
||||||
|
31.29,-22.40 32.43,-21.30 32.35,-21.14 32.48,-20.95 32.49,-20.66 32.67,-20.52 32.99,-19.98 33.01,-19.87 32.78,-19.39 32.85,-19.10 32.70,-18.94 32.72,-18.83 32.88,-18.73 32.99,-18.36 32.97,-17.25 32.88,-16.88 32.95,-16.71 32.24,-16.45 31.94,-16.43 31.69,-16.21 31.49,-16.18 31.24,-16.02 30.44,-16.00 30.40,-15.64
|
||||||
|
12.28,-6.00 12.42,-6.05 13.18,-5.86 16.43,-5.90 16.70,-6.16 16.74,-6.62 16.92,-6.93 16.98,-7.26 17.58,-8.10 18.01,-8.11 18.56,-7.94 18.94,-8.00 19.34,-7.97 19.53,-7.14 19.66,-7.04 19.88,-6.99 20.59,-6.92 20.54,-7.18 20.61,-7.28 21.78,-7.31 21.84,-7.42 21.80,-8.11 21.90,-8.34 21.91,-8.69 21.81,-9.47 22.20,-10.04 22.27,-10.26 22.31,-10.69 22.18,-10.89 22.23,-11.12 22.31,-11.20 22.56,-11.06 23.08,-11.09 23.46,-10.97 23.83,-11.01 23.97,-10.87
|
||||||
|
-16.56,13.59 -15.51,13.59 -15.43,13.73 -15.11,13.81 -14.94,13.79 -14.41,13.50 -13.98,13.54 -13.83,13.41 -13.85,13.34 -14.25,13.24 -15.15,13.56 -15.29,13.40 -15.81,13.33 -15.83,13.16 -16.65,13.15 -16.76,13.06
|
||||||
|
0.22,14.91 0.16,14.50 0.38,14.25 0.35,14.14 0.43,13.97 0.62,13.70 0.90,13.61 1.20,13.36 0.99,13.36 0.99,13.04 1.56,12.64 1.79,12.61 1.96,12.71 2.10,12.70 2.23,12.47 2.07,12.38 2.07,12.31 2.39,11.90
|
||||||
|
18.61,3.48 18.47,3.62 18.16,3.50 17.49,3.69 16.61,3.51 16.48,3.17 16.47,2.83 16.18,2.27
|
||||||
|
16.18,2.26 16.08,2.11 16.14,1.72 16.06,1.68 15.74,1.92 15.34,1.94 15.16,2.04 14.90,2.01 14.88,2.08 14.76,2.08 14.58,2.20 14.48,2.15 13.29,2.16
|
||||||
|
15.48,7.52 15.21,7.21 14.74,6.28 14.51,6.16 14.43,6.04 14.62,5.87 14.56,5.28 14.64,5.18 14.73,4.60 15.06,4.28 15.14,4.04 15.03,4.02 15.13,3.83 15.77,3.13 15.90,3.10 16.06,2.91 16.18,2.26
|
||||||
|
33.98,4.22 33.49,3.76 33.15,3.77 33.00,3.88 32.34,3.71 32.14,3.52 31.94,3.61 31.80,3.80 31.55,3.68 31.15,3.79 30.93,3.63 30.84,3.49
|
||||||
|
13.07,-4.63 12.83,-4.74 12.45,-5.07 12.52,-5.15 12.50,-5.70 12.21,-5.76
|
||||||
|
31.95,-25.96 31.99,-24.46 31.80,-23.89 31.55,-23.48 31.29,-22.40
|
||||||
|
33.20,-14.01 30.23,-14.99 30.40,-15.64
|
||||||
|
34.96,-11.58 34.62,-11.62 34.36,-12.16 34.56,-13.36 34.66,-13.49 34.91,-13.55 35.25,-13.90 35.85,-14.67 35.89,-14.89 35.81,-15.27 35.82,-15.68 35.76,-16.06 35.36,-16.16 35.17,-16.56 35.28,-16.81 35.27,-17.12 35.06,-17.08 35.08,-16.83 34.93,-16.76 34.42,-16.25 34.40,-16.08 34.25,-15.89 34.54,-15.30 34.51,-14.60 34.33,-14.41 33.64,-14.57 33.20,-14.01
|
||||||
|
29.58,-1.39 29.56,-0.98 29.72,0.10 29.93,0.50 29.94,0.82 30.18,0.97 30.32,1.19 30.48,1.24 31.25,2.04 31.27,2.15 31.18,2.27 30.96,2.40 30.73,2.46 30.85,2.89 30.75,3.04 30.91,3.41 30.84,3.49
|
||||||
|
36.52,14.26 36.43,15.13 36.52,15.25 36.91,16.30 36.89,16.62 36.98,16.80 37.01,17.06 37.41,17.06 37.55,17.32 37.78,17.46 38.25,17.58 38.61,18.01
|
||||||
|
42.38,12.47 42.13,12.77 41.95,12.88 41.77,13.18 41.36,13.50 40.82,14.11 40.14,14.46 39.90,14.44 39.53,14.54 39.20,14.48 39.02,14.63 38.81,14.48 38.43,14.43 38.22,14.65 37.88,14.85 37.57,14.15 37.26,14.45 37.02,14.27 36.81,14.32 36.52,14.26
|
||||||
|
27.35,57.53 27.40,57.67 27.54,57.80 27.78,57.87 27.67,57.93 27.50,58.22 27.53,58.44 27.43,58.79 27.76,59.05 27.90,59.28 28.15,59.37 28.01,59.48
|
||||||
|
74.31,32.81 74.66,32.76 74.69,32.49 74.99,32.46 75.30,32.32
|
||||||
|
-84.35,10.98 -84.70,11.05 -84.91,10.95 -85.58,11.19 -85.74,11.06
|
||||||
|
-82.88,8.07 -83.03,8.34 -82.86,8.45 -82.86,8.64 -82.92,8.74 -82.73,8.92 -82.94,9.06 -82.94,9.45 -82.80,9.59 -82.64,9.51 -82.56,9.58
|
||||||
|
-66.88,1.22 -67.21,2.39 -67.62,2.79 -67.86,2.79 -67.83,2.89 -67.31,3.42 -67.66,3.86 -67.86,4.51 -67.82,5.27 -67.64,5.56 -67.58,5.83 -67.47,5.93 -67.48,6.18 -67.86,6.29 -68.47,6.16 -68.94,6.20 -69.27,6.10 -69.43,6.12 -70.13,6.95 -70.74,7.09 -71.13,6.99 -72.01,7.03 -72.21,7.37 -72.39,7.42 -72.47,7.52 -72.45,7.97 -72.36,8.09 -72.39,8.29 -72.67,8.63 -72.80,9.11 -72.96,9.14 -73.06,9.26 -73.37,9.19 -73.01,9.79 -72.87,10.49 -72.69,10.84 -72.45,11.11 -72.25,11.20 -71.96,11.67 -71.32,11.86
|
||||||
|
-68.44,-52.36 -68.46,-52.29 -69.96,-52.01 -71.97,-51.96 -71.95,-51.88 -72.41,-51.54 -72.30,-51.30 -72.38,-51.10 -72.28,-50.91 -72.34,-50.68 -72.51,-50.61 -73.15,-50.74 -73.31,-50.36 -73.50,-50.13 -73.48,-49.77
|
||||||
|
-68.63,-52.65 -68.65,-54.92 -68.06,-54.88 -67.35,-54.93 -66.48,-55.11
|
||||||
|
22.77,54.36 19.60,54.46
|
||||||
|
48.44,28.54 47.67,28.53 47.43,28.99 46.53,29.10
|
||||||
|
44.77,37.14 44.88,36.80 45.02,36.70 45.05,36.47 45.24,36.36 45.36,36.02 45.56,35.98 45.78,35.82 46.27,35.77 46.00,35.61 45.98,35.48 46.11,35.32 46.13,35.13 45.92,35.03 45.68,34.80 45.64,34.57 45.50,34.58 45.44,34.42 45.54,34.22 45.40,33.97 45.74,33.60 45.88,33.61 45.87,33.49 46.02,33.42 46.15,33.23 46.08,33.09 46.11,32.96 46.38,32.93 47.12,32.47 47.37,32.42 47.51,32.15 47.83,31.79 47.68,31.40 47.68,31.00 48.01,30.99 48.01,30.47 48.33,30.29 48.43,30.04 48.55,29.96
|
||||||
|
61.26,35.62 61.25,35.87 61.15,35.98 61.21,36.19 61.12,36.64 60.34,36.64 60.06,36.96 59.45,37.25 59.30,37.51 58.82,37.68 58.26,37.67 57.98,37.83 57.35,37.97 57.31,38.13 57.19,38.22 56.44,38.25 56.32,38.19 56.27,38.08 55.58,38.10 55.38,38.05 54.90,37.78 54.70,37.47 54.19,37.33 53.91,37.34
|
||||||
|
140.97,-2.60 140.97,-6.35 140.86,-6.74 140.98,-6.91 140.98,-9.12
|
||||||
|
9.52,30.23 9.31,30.12 9.64,29.64 9.81,29.18 9.92,27.79 9.75,27.33 9.89,26.85 9.88,26.63 9.86,26.55 9.49,26.33 9.45,26.07 10.00,25.33 10.03,25.05 10.26,24.59 10.40,24.49 10.69,24.55 11.51,24.31 11.97,23.52
|
||||||
|
-13.73,12.67 -15.20,12.68 -15.57,12.49 -16.71,12.35
|
||||||
|
-11.39,12.40 -12.40,12.34 -12.93,12.53 -13.06,12.49 -13.08,12.63 -13.73,12.67
|
||||||
|
0.90,10.99 0.49,10.95 -0.07,11.12
|
||||||
|
-139.06,60.00 -139.19,60.08 -139.08,60.34 -139.68,60.33 -139.97,60.18 -140.45,60.30 -140.53,60.22 -141.00,60.30 -141.00,69.65
|
||||||
|
-130.62,54.71 -130.21,55.03 -130.02,55.28 -130.12,55.69 -130.03,55.89 -130.06,56.07 -130.41,56.12 -130.48,56.23 -130.74,56.34 -131.58,56.60 -131.82,56.59 -131.87,56.79 -132.10,56.86 -132.03,57.03 -132.34,57.08 -132.23,57.20 -133.42,58.34 -133.40,58.41 -133.82,58.71 -134.30,58.90 -134.44,59.09 -134.94,59.29 -135.07,59.44 -135.05,59.58 -135.48,59.79 -136.32,59.60 -136.25,59.53 -136.35,59.46 -136.47,59.46 -136.47,59.28 -136.58,59.15 -136.81,59.15 -137.44,58.90 -137.52,58.92 -137.48,58.99 -137.59,59.23 -138.63,59.78 -138.71,59.90 -139.06,60.00
|
||||||
|
-123.31,48.99 -122.69,48.99
|
||||||
|
-88.89,15.89 -89.24,15.89 -89.16,17.81
|
||||||
|
-75.28,-0.11 -75.48,-0.16 -75.63,-0.12 -75.26,-0.59 -75.25,-0.95 -75.41,-0.92 -75.57,-1.53 -76.09,-2.13 -76.68,-2.56 -77.86,-2.98 -78.18,-3.35 -78.16,-3.47 -78.23,-3.49 -78.35,-3.40 -78.42,-3.78 -78.68,-4.33 -78.69,-4.56 -78.91,-4.71 -78.92,-4.86 -79.03,-4.97 -79.33,-4.93 -79.50,-4.67 -79.52,-4.54 -79.64,-4.45 -79.80,-4.48 -80.14,-4.30 -80.38,-4.46 -80.48,-4.43 -80.35,-4.21 -80.49,-4.17 -80.49,-4.01 -80.30,-4.01 -80.18,-3.88 -80.32,-3.39
|
||||||
|
-69.97,-4.24 -70.34,-3.81 -70.53,-3.87 -70.74,-3.78 -70.07,-2.75 -70.10,-2.66 -70.97,-2.21 -71.40,-2.33 -71.75,-2.15 -71.98,-2.33 -72.22,-2.40 -72.40,-2.43 -72.63,-2.35 -72.94,-2.39 -73.15,-2.28 -73.13,-2.08 -73.20,-1.83 -73.50,-1.69 -73.52,-1.45 -73.66,-1.25 -74.25,-0.97 -74.42,-0.58 -74.80,-0.20 -74.95,-0.19 -75.14,-0.05 -75.28,-0.11
|
||||||
|
-66.88,1.22 -67.08,1.19 -67.12,1.70 -67.40,2.12 -67.56,2.07 -67.82,1.79 -67.94,1.75 -68.19,1.99 -68.26,1.85 -68.18,1.72 -69.32,1.72 -69.54,1.77 -69.85,1.71 -69.85,1.06 -69.31,1.05 -69.16,0.86 -69.15,0.66 -69.28,0.63 -69.47,0.73 -70.05,0.58 -70.07,-0.14 -69.92,-0.32 -69.63,-0.51 -69.61,-0.76 -69.45,-1.00 -69.40,-1.19 -69.97,-4.24
|
||||||
|
-69.58,-10.95 -68.69,-12.50 -68.76,-12.69 -68.98,-12.88 -68.98,-13.50 -69.07,-13.68 -68.87,-14.17 -69.00,-14.27 -69.01,-14.38 -69.36,-14.80 -69.37,-14.96 -69.17,-15.24 -69.42,-15.64 -69.22,-16.15 -68.84,-16.34 -69.03,-16.48 -69.02,-16.64 -69.20,-16.77 -69.44,-17.09 -69.62,-17.20 -69.51,-17.50
|
||||||
|
-69.58,-10.95 -68.85,-11.01 -68.73,-11.12 -68.62,-11.11 -68.31,-10.98 -68.07,-10.70 -67.72,-10.68 -67.42,-10.39 -67.11,-10.27 -66.58,-9.90 -65.92,-9.79 -65.56,-9.80 -65.40,-9.71 -65.31,-9.87 -65.30,-10.15 -65.45,-10.51 -65.32,-11.02 -65.39,-11.25 -65.18,-11.65 -65.19,-11.75 -65.09,-11.74 -64.99,-11.98 -64.51,-12.25 -64.42,-12.44 -63.94,-12.53 -63.69,-12.48 -63.35,-12.68 -63.07,-12.67 -63.02,-12.81 -62.77,-13.00 -62.12,-13.16 -61.79,-13.53 -61.08,-13.49 -60.51,-13.79 -60.40,-14.02 -60.47,-14.18 -60.30,-14.62 -60.27,-15.09 -60.58,-15.10 -60.24,-15.48 -60.18,-16.27 -58.54,-16.33 -58.35,-16.28 -58.35,-16.49 -58.48,-16.70 -58.40,-17.23 -57.99,-17.51 -57.83,-17.51 -57.59,-18.12 -57.50,-18.21 -57.57,-18.28 -57.78,-18.91 -57.73,-18.92 -57.72,-19.04 -57.80,-19.08 -58.13,-19.74 -57.86,-19.98 -58.16,-20.16
|
||||||
|
-69.97,-4.24 -70.05,-4.33 -70.24,-4.30 -70.40,-4.15 -70.80,-4.17 -70.97,-4.35 -71.84,-4.50 -72.35,-4.79 -72.70,-5.07 -72.89,-5.12 -72.98,-5.63 -73.24,-6.10 -73.14,-6.47 -73.76,-6.91 -73.80,-7.08 -73.72,-7.31 -73.96,-7.38 -74.00,-7.56 -73.72,-7.78 -73.78,-7.94 -73.64,-8.07 -73.55,-8.35 -73.36,-8.48 -73.30,-8.65 -72.97,-8.99 -72.97,-9.12 -73.21,-9.41 -72.81,-9.41 -72.38,-9.51 -72.17,-9.84 -72.14,-10.01 -71.24,-9.97 -70.54,-9.44 -70.64,-9.82 -70.64,-11.01 -70.53,-10.95 -70.29,-11.06 -69.96,-10.93 -69.58,-10.95
|
||||||
|
-62.65,-22.23 -62.28,-21.07 -62.28,-20.56 -61.92,-20.06 -61.76,-19.65 -60.01,-19.30 -59.09,-19.29 -58.18,-19.82 -58.16,-20.16
|
||||||
|
-17.05,20.81 -16.96,21.33 -13.02,21.33 -13.15,22.82 -13.03,23.00 -12.62,23.27 -12.02,23.47 -12.02,26.00 -8.68,26.00 -8.68,27.29
|
||||||
|
-8.68,27.29 -4.82,25.00
|
||||||
|
-8.68,27.66 -8.66,28.72 -7.69,29.35 -7.49,29.39 -7.16,29.61 -6.64,29.57 -6.52,29.66 -6.48,29.82 -6.00,29.83 -5.45,29.96 -5.18,30.17 -4.97,30.47 -4.32,30.70 -3.99,30.91 -3.67,30.96 -3.62,31.07 -3.83,31.20 -3.79,31.36 -3.83,31.66 -3.02,31.83 -2.86,32.07 -2.45,32.13 -1.28,32.09
|
||||||
|
-8.68,27.29 -8.68,27.66
|
||||||
|
-12.28,14.81 -12.86,15.24 -12.93,15.45 -13.21,15.62 -13.41,16.06 -13.51,16.14 -13.87,16.15 -14.09,16.42 -14.30,16.58 -14.53,16.66 -14.99,16.68 -15.77,16.49 -16.24,16.53 -16.44,16.20 -16.54,15.84
|
||||||
|
-4.82,25.00 -6.59,24.99 -5.63,16.57 -5.36,16.28 -5.51,15.50 -9.18,15.50 -9.34,15.53 -9.35,15.68 -9.43,15.62 -9.45,15.46 -9.94,15.37 -10.70,15.42 -10.95,15.15 -11.50,15.64 -11.76,15.43 -11.94,14.89 -12.10,14.75 -12.28,14.81
|
||||||
|
4.23,19.14 3.40,18.99 3.26,19.01 3.11,19.15 3.26,19.41 3.20,19.79 3.13,19.85 2.41,20.06 2.22,20.25 1.75,20.33 1.61,20.56 1.17,20.82 1.15,21.10 -4.82,25.00
|
||||||
|
4.23,19.14 5.84,19.48 7.48,20.87 11.97,23.52
|
||||||
|
9.52,30.23 9.04,32.07 8.33,32.54 8.11,33.06 7.73,33.27 7.50,33.83 7.51,34.08 7.75,34.25 7.84,34.41 8.12,34.56 8.25,34.73 8.28,34.98 8.39,35.20 8.25,35.80 8.35,36.37 8.21,36.52 8.44,36.76 8.60,36.83 8.58,36.94
|
||||||
|
23.98,19.50 15.98,23.45 14.98,23.00
|
||||||
|
24.98,22.00 24.98,20.00 23.98,20.00 23.98,19.50
|
||||||
|
25.15,31.65 24.85,31.33 24.96,30.68 24.70,30.20 24.98,29.18 24.98,22.00
|
||||||
|
22.86,10.92 22.92,11.34 22.59,11.58 22.58,11.99 22.47,12.07 22.35,12.66 22.23,12.71 21.93,12.68 21.83,12.79 21.99,13.11 22.23,13.33 22.11,13.80 22.17,13.91 22.54,14.16 22.38,14.55 22.67,14.72 22.76,15.00 22.93,15.16 22.93,15.53 23.11,15.70 23.97,15.72 23.98,19.50
|
||||||
|
24.98,22.00 31.21,21.99 31.40,22.20 31.49,22.15 31.43,22.00 36.87,22.00
|
||||||
|
41.88,3.98 41.34,3.20 40.96,2.81 40.98,-0.87 41.52,-1.57 41.53,-1.70
|
||||||
|
42.92,11.00 42.66,10.60 42.84,10.20 43.07,9.93 43.18,9.88 43.48,9.38 43.62,9.34 43.98,9.01 46.98,8.00 47.98,8.00
|
||||||
|
30.75,-8.19 28.90,-8.49 28.93,-8.59 28.87,-8.79 28.40,-9.22 28.60,-9.68 28.65,-10.55 28.36,-11.48 28.38,-11.57 28.48,-11.81 29.06,-12.35 29.49,-12.42 29.51,-12.23 29.80,-12.16 29.78,-13.44 29.65,-13.41 29.63,-13.30 29.55,-13.25 29.20,-13.40 29.01,-13.37 28.73,-12.93 28.55,-12.84 28.41,-12.52 27.57,-12.23 27.42,-11.94 27.24,-11.78 27.16,-11.58 27.05,-11.62 26.95,-11.90 26.82,-11.97 26.03,-11.89 25.51,-11.75 25.35,-11.62 25.29,-11.21 24.52,-11.44 24.38,-11.42 24.34,-11.37 24.40,-11.26 24.37,-11.13 24.14,-11.03 24.08,-10.89 23.97,-10.87
|
||||||
|
23.38,-17.64 22.15,-16.60 22.04,-16.26 21.98,-15.96 21.98,-13.00 23.96,-12.99 23.88,-12.80 23.99,-12.42 23.97,-11.64 24.05,-11.41 23.97,-10.87
|
||||||
|
33.90,-1.00 33.94,0.17 34.16,0.61 34.41,0.87 34.48,1.04 34.80,1.24 34.78,1.38 34.97,1.64 34.98,1.77 34.91,2.48 34.74,2.82 34.59,2.92 34.52,3.12 34.45,3.16 34.40,3.41 34.44,3.65 34.17,3.81 34.19,3.87 33.98,4.22
|
||||||
|
33.90,-1.00 37.64,-3.05 37.69,-3.25 37.62,-3.51 39.22,-4.69
|
||||||
|
11.33,2.17 9.98,2.17 9.80,2.30
|
||||||
|
11.33,2.17 11.34,1.00 10.03,1.00 9.91,0.96 9.76,1.07 9.59,1.03
|
||||||
|
25.26,-17.79 24.91,-17.82 24.53,-18.05 24.36,-17.98 24.24,-18.02 23.60,-18.46 23.30,-18.03 23.22,-18.00 20.97,-18.32 20.98,-21.96 19.98,-22.00 19.98,-24.75
|
||||||
|
19.98,-24.78 19.98,-28.45 19.54,-28.57 19.31,-28.73 19.25,-28.90 19.16,-28.94 18.84,-28.87 18.10,-28.87 17.45,-28.70 17.35,-28.50 17.36,-28.27 17.06,-28.03 16.88,-28.13 16.76,-28.45 16.45,-28.62
|
||||||
|
19.98,-24.75 19.98,-24.78
|
||||||
|
0.90,10.99 0.79,10.71 0.76,10.39 1.33,10.00 1.39,9.36 1.60,9.05 1.62,7.00 1.53,6.99 1.60,6.61 1.78,6.29 1.62,6.22
|
||||||
|
42.80,16.37 43.17,16.69 43.18,16.81 43.12,16.94 43.16,17.21 43.24,17.27 43.19,17.36 43.42,17.52 43.92,17.32 44.16,17.40 45.15,17.43 45.54,17.30 46.31,17.23 46.73,17.27 46.98,16.95 47.14,16.95 47.44,17.11 47.58,17.45 48.17,18.16 49.04,18.58 51.98,19.00
|
||||||
|
51.98,19.00 52.73,17.30 52.80,17.27 53.09,16.65
|
||||||
|
55.19,22.70 55.64,22.00 54.98,20.00 51.98,19.00
|
||||||
|
55.19,22.70 55.10,22.62 52.56,22.93 51.59,24.08 51.57,24.29
|
||||||
|
42.36,37.11 41.79,36.60 41.42,36.51 41.30,36.38 41.25,36.07 41.35,35.64 41.22,35.29 41.19,34.77 40.99,34.43 40.69,34.33 38.77,33.37
|
||||||
|
35.79,32.73 36.37,32.39 36.82,32.32 38.77,33.37
|
||||||
|
35.61,32.68 35.79,32.73
|
||||||
|
39.15,32.12 39.29,32.24 39.25,32.35 39.04,32.31 38.98,32.47 39.06,32.49 38.77,33.37
|
||||||
|
34.95,29.35 36.07,29.20 36.48,29.50 36.76,29.87 37.47,30.00 37.65,30.33 37.98,30.50 36.96,31.49 38.96,31.99 39.15,32.12
|
||||||
|
46.53,29.10 46.36,29.06 44.69,29.20 42.07,31.08 40.37,31.94 39.15,32.12
|
||||||
|
61.59,25.20 61.66,25.75 61.75,25.84 61.84,26.23 62.13,26.37 62.24,26.36 62.31,26.49 62.44,26.56 63.16,26.65 63.30,27.15 63.17,27.25 62.76,27.25 62.81,27.50 62.76,28.24 62.56,28.24 62.35,28.41 61.89,28.55 61.62,28.79 61.32,29.37 60.84,29.86
|
||||||
|
60.84,29.86 61.81,30.91 61.76,31.29 61.66,31.38 60.82,31.50 60.83,32.25 60.56,33.06 60.56,33.14 60.92,33.51 60.51,33.64 60.49,34.09 60.64,34.31 60.89,34.32 60.73,34.52 60.95,34.65 61.08,34.86 61.15,35.09 61.10,35.27 61.19,35.31 61.28,35.51 61.26,35.62
|
||||||
|
73.73,36.89 74.04,36.83 74.54,37.02
|
||||||
|
60.84,29.86 62.48,29.41 63.57,29.50 64.10,29.39 64.39,29.54 65.10,29.56 66.18,29.84 66.31,29.97 66.24,30.11 66.35,30.80 66.40,30.91 66.60,31.02 66.83,31.26 66.92,31.31 67.45,31.23 67.74,31.34 67.58,31.51 68.02,31.68 68.16,31.80 68.44,31.75 68.60,31.80 68.87,31.63 69.08,31.74 69.28,31.94 69.24,32.43 69.41,32.68 69.50,33.02 69.92,33.11 70.26,33.29 70.28,33.37 70.13,33.62 69.87,33.90 69.89,34.01 69.99,34.05 70.33,33.96 70.65,33.95 71.05,34.05 71.10,34.37 70.97,34.53 71.62,35.18 71.55,35.29 71.60,35.41 71.57,35.55 71.40,35.88 71.19,36.04 71.23,36.12 71.62,36.44 71.77,36.43 72.25,36.73 72.62,36.83 73.73,36.89
|
||||||
|
70.95,42.25 70.72,42.17 70.58,42.04 70.42,42.08 70.10,41.82 69.15,41.43 68.99,41.21 68.58,40.88 68.57,40.62 68.42,40.62 68.05,40.81 68.11,41.03 67.94,41.20 66.71,41.18 66.50,41.99 66.01,42.00 66.10,42.99 65.80,42.88 65.50,43.31 64.91,43.71 64.44,43.55 63.21,43.63 61.99,43.49 61.16,44.17 61.01,44.39 58.56,45.56 55.98,44.99 55.98,41.32
|
||||||
|
13.61,13.70 13.45,14.38 14.37,15.75 15.47,16.91 15.74,19.90 15.96,20.35 15.59,20.73 15.54,20.87 15.61,20.95 15.18,21.52 15.17,21.92 14.98,23.00
|
||||||
|
11.97,23.52 13.48,23.18 14.22,22.62 14.98,23.00
|
||||||
|
0.22,14.91 0.29,14.98 0.95,14.98 1.30,15.27 3.00,15.34 3.06,15.43 3.50,15.36 3.52,15.48 3.84,15.70 4.12,16.36 4.23,17.00 4.23,19.14
|
||||||
|
-0.07,11.12 -0.30,11.17 -0.63,10.93 -0.70,10.99 -1.04,11.01 -2.83,11.00 -2.91,10.59 -2.79,10.43 -2.78,9.75 -2.70,9.48
|
||||||
|
-13.29,9.05 -13.03,9.10 -12.96,9.26 -12.76,9.37 -12.50,9.86 -12.43,9.90 -12.28,9.93 -12.14,9.88 -11.92,9.92 -11.91,9.99 -11.27,10.00 -10.69,9.31 -10.75,9.10 -10.62,9.06 -10.61,8.87 -10.50,8.69 -10.71,8.34 -10.56,8.32 -10.39,8.48 -10.28,8.49
|
||||||
|
5.79,49.54 6.01,49.45 6.18,49.50 6.34,49.45
|
||||||
|
56.08,26.06 56.17,26.05 56.14,25.69 56.30,25.65
|
||||||
|
30.51,-1.07 30.36,-1.07 30.10,-1.37 29.93,-1.47 29.83,-1.34 29.58,-1.39
|
||||||
|
30.51,-1.07 30.51,-1.21 30.81,-1.56 30.88,-2.14 30.83,-2.34 30.55,-2.40
|
||||||
|
33.90,-1.00 30.81,-0.99 30.51,-1.07
|
||||||
|
34.59,-11.97 34.54,-12.02 34.62,-12.07 34.59,-11.97
|
||||||
|
87.32,49.09 87.82,49.16
|
||||||
|
45.50,40.66 45.48,40.61 45.55,40.62 45.50,40.66
|
||||||
|
44.99,41.09 45.00,41.02 44.99,41.09
|
||||||
|
12.44,43.98 12.40,43.93 12.49,43.90 12.50,43.99 12.44,43.98
|
||||||
|
70.57,41.02 70.65,40.94 70.57,41.02
|
||||||
|
-117.13,32.53 -114.72,32.72 -114.84,32.51 -111.04,31.32 -108.21,31.33 -108.21,31.78 -106.45,31.77 -106.15,31.45 -104.92,30.58 -104.68,30.13 -104.62,29.85 -104.40,29.57 -103.26,29.00 -103.09,29.04 -102.89,29.22 -102.73,29.64 -102.34,29.86 -101.99,29.80 -101.55,29.81 -101.38,29.74 -100.75,29.18 -100.33,28.50 -100.30,28.33 -99.75,27.73 -99.51,27.55 -99.46,27.06 -99.23,26.76 -99.11,26.45 -98.28,26.11 -97.80,26.04 -97.38,25.87 -97.15,25.96
|
||||||
|
-92.24,14.55 -92.16,14.69 -92.16,14.96 -92.07,15.07 -92.20,15.28 -91.74,16.07 -90.45,16.07 -90.42,16.39 -90.63,16.51 -90.71,16.71 -91.41,17.26 -90.99,17.25 -90.99,17.82 -89.16,17.81 -89.13,17.97 -89.05,18.00 -88.90,17.91 -88.81,17.97 -88.46,18.48 -88.04,18.42 -88.04,18.20 -87.83,18.18
|
||||||
|
-54.62,-25.58 -54.61,-25.43 -54.44,-25.12 -54.28,-24.31 -54.32,-24.13 -54.24,-24.05 -54.63,-23.81 -55.19,-24.02 -55.42,-23.95 -55.65,-22.89 -55.62,-22.67 -55.85,-22.31 -56.25,-22.26 -56.45,-22.08 -56.63,-22.23 -56.94,-22.27 -57.57,-22.18 -57.72,-22.10 -57.96,-22.11 -57.95,-21.49 -57.83,-21.00 -58.16,-20.16
|
||||||
|
-62.65,-22.23 -62.21,-22.61 -61.80,-23.18 -61.21,-23.56 -61.03,-23.76 -60.51,-23.96 -59.89,-24.09 -59.19,-24.56 -58.37,-24.96 -57.96,-25.05 -57.64,-25.33 -57.57,-25.53 -57.75,-25.70 -57.89,-26.01 -58.11,-26.18 -58.20,-26.38 -58.19,-26.63 -58.32,-26.86 -58.62,-27.13 -58.60,-27.31 -58.17,-27.27 -57.11,-27.47 -56.60,-27.47 -56.44,-27.55 -56.16,-27.32 -55.71,-27.41 -55.43,-27.01 -55.14,-26.93 -54.83,-26.65 -54.68,-26.31 -54.62,-25.58
|
||||||
|
-62.65,-22.23 -62.84,-22.00 -63.92,-22.03 -64.27,-22.60 -64.33,-22.83 -64.61,-22.23 -64.99,-22.11 -65.77,-22.10 -66.22,-21.80 -66.37,-22.11 -66.71,-22.22 -66.80,-22.41 -67.03,-22.55 -67.19,-22.82
|
||||||
|
26.62,48.26 26.85,48.39 27.23,48.37 27.55,48.48 27.82,48.42 28.09,48.26 28.29,48.24 28.46,48.09 28.53,48.15 28.77,48.12 28.92,47.95 29.13,47.96 29.21,47.78 29.13,47.49 29.54,47.27 29.51,47.13 29.57,46.96 29.88,46.83 29.94,46.72 29.92,46.54 30.13,46.42 29.84,46.35 29.71,46.45 29.61,46.40 29.30,46.47 29.20,46.38 29.19,46.52 28.96,46.46 28.93,46.36 29.01,46.18 28.95,46.05 28.74,45.94 28.73,45.85 28.49,45.67 28.50,45.52 28.21,45.45
|
||||||
|
67.76,37.17 67.96,36.97 68.07,36.95 68.91,37.33 69.30,37.12 69.41,37.21 69.40,37.40 69.49,37.55 69.82,37.61 70.04,37.55 70.19,37.58 70.25,37.66 70.21,37.92 70.62,38.33 70.88,38.46 71.26,38.31 71.33,38.17 71.28,37.92 71.58,37.91 71.43,37.13 71.53,36.85 71.67,36.70 71.80,36.69 72.36,36.98 72.66,37.03 72.90,37.27 73.38,37.46 73.72,37.42 73.65,37.24 74.17,37.33 74.35,37.42 74.66,37.39 74.89,37.23
|
||||||
|
66.52,37.35 66.83,37.37 67.07,37.33 67.20,37.24 67.52,37.27 67.76,37.17
|
||||||
|
61.26,35.62 61.34,35.63 61.62,35.43 61.98,35.44 62.31,35.17 62.46,35.25 62.69,35.26 63.06,35.45 63.17,35.68 63.13,35.85 64.01,36.01 64.51,36.34 64.82,37.13 65.09,37.24 65.55,37.25 65.64,37.47 65.77,37.57 66.11,37.41 66.52,37.35
|
||||||
|
92.57,21.98 92.49,22.69 92.36,22.93 92.25,23.68 91.93,23.69 91.94,23.50 91.75,23.29 91.75,23.05 91.62,22.98 91.51,23.03 91.44,23.20 91.37,23.20 91.37,23.07 91.32,23.10 91.16,23.66 91.23,23.92 91.37,24.09 91.88,24.20 91.95,24.36 92.10,24.41 92.23,24.88 92.44,24.85 92.47,24.94 92.05,25.17 90.44,25.16 89.83,25.29 89.82,25.94 89.67,26.21 89.59,26.19 89.55,26.01 89.37,26.01 89.19,26.11 89.02,26.41 88.92,26.38 88.97,26.25 88.83,26.25 88.68,26.29 88.62,26.43 88.42,26.57 88.35,26.50 88.44,26.37 88.15,26.09 88.08,25.89 88.50,25.54 88.77,25.49 88.95,25.26 88.82,25.18 88.46,25.19 88.31,24.88 88.15,24.91 88.02,24.63 88.15,24.49 88.29,24.48 88.72,24.27 88.70,24.00 88.57,23.67 88.74,23.44 88.72,23.25 88.93,23.19 88.85,23.04 89.05,22.09
|
||||||
|
2.39,11.90 2.37,12.22 2.81,12.38 3.60,11.70
|
||||||
|
18.61,3.48 18.62,3.30 18.49,2.92 18.21,2.41 18.07,2.01 18.06,1.53 17.90,1.12 17.89,0.23 17.72,-0.28 17.75,-0.55 17.28,-1.00 16.88,-1.23 16.54,-1.84 16.22,-2.18 16.22,-3.03 16.15,-3.46 15.99,-3.77 15.87,-3.93 15.60,-4.03 15.27,-4.31 14.71,-4.88 14.41,-4.83 14.37,-4.59 14.45,-4.45 14.36,-4.30 13.94,-4.48 13.72,-4.45 13.66,-4.72 13.41,-4.84 13.14,-4.60 13.07,-4.63
|
||||||
|
28.98,-30.00 29.10,-29.92 29.14,-29.70 29.29,-29.57 29.39,-29.27 29.30,-29.09 28.95,-28.88 28.63,-28.58 27.74,-28.94 27.29,-29.52 27.06,-29.63 27.39,-30.32 27.75,-30.60 28.06,-30.63 28.39,-30.15 28.65,-30.13 28.98,-30.00
|
||||||
|
29.36,-22.19 29.66,-22.15 30.19,-22.29 30.92,-22.29 31.29,-22.40
|
||||||
|
19.98,-24.78 20.43,-25.15 20.79,-25.92 20.82,-26.16 20.63,-26.44 20.69,-26.82 21.69,-26.84 21.79,-26.71 22.09,-26.58 22.22,-26.39 22.60,-26.13 22.88,-25.46 23.06,-25.31 23.39,-25.29 23.89,-25.60 24.19,-25.63 24.33,-25.74 24.75,-25.82 25.44,-25.71 25.58,-25.61 25.91,-24.75 26.40,-24.61 26.84,-24.24 26.99,-23.70 27.09,-23.58 27.76,-23.20 28.21,-22.69 28.38,-22.59 28.84,-22.48 29.13,-22.21 29.36,-22.19
|
||||||
|
34.96,-11.58 35.56,-11.60 35.79,-11.45 35.91,-11.45 36.31,-11.71 36.67,-11.68 36.98,-11.57 37.37,-11.71 37.72,-11.58 37.92,-11.29 38.18,-11.28 38.49,-11.41 38.79,-11.23 39.32,-11.12 39.99,-10.82 40.46,-10.46
|
||||||
|
-84.35,10.98 -84.17,10.78 -83.92,10.74 -83.71,10.79 -83.64,10.92
|
||||||
|
28.15,56.14 27.90,56.08 27.64,55.91 27.58,55.80 27.05,55.83 26.59,55.67
|
||||||
|
28.21,45.45 28.45,45.29 28.76,45.23 28.78,45.31 29.40,45.42 29.71,45.26
|
||||||
|
21.36,44.83 21.91,44.67 22.09,44.54 22.50,44.71 22.64,44.65 22.73,44.57 22.55,44.54 22.49,44.44 22.71,44.24
|
||||||
|
56.23,25.30 56.24,25.21 56.29,25.28 56.23,25.30
|
||||||
|
32.11,-26.84 32.89,-26.85
|
||||||
|
71.74,39.98 71.67,39.95 71.75,39.91 71.79,40.00 71.74,39.98
|
||||||
|
-122.69,48.99 -95.16,48.99 -95.16,49.37 -94.85,49.30 -94.80,49.00 -94.62,48.74 -94.06,48.66 -93.71,48.53 -93.38,48.62 -93.00,48.61 -92.50,48.44 -92.41,48.28 -92.17,48.34 -92.01,48.30 -91.52,48.06 -90.92,48.21 -90.74,48.10 -90.09,48.12 -89.90,48.00 -89.46,48.00 -88.38,48.30 -84.88,46.90 -84.78,46.64 -84.56,46.46 -84.15,46.54 -84.09,46.23 -83.98,46.08 -83.62,46.12 -83.47,45.99 -83.59,45.82 -82.55,45.35 -82.14,43.57 -82.41,43.07 -82.55,42.62 -83.07,42.30 -83.15,42.14 -83.14,41.98 -83.03,41.83 -82.69,41.68 -82.44,41.67 -81.28,42.21 -80.25,42.37 -79.04,42.80 -78.92,42.91 -79.03,43.02 -79.06,43.28 -79.17,43.47 -78.85,43.58 -76.82,43.63 -76.46,44.06 -76.15,44.30 -75.00,44.97 -71.52,45.01 -71.33,45.29 -71.13,45.26 -71.00,45.34 -70.87,45.27 -70.80,45.40 -70.69,45.43 -70.70,45.55 -70.42,45.74 -70.30,45.91 -70.25,46.25 -70.07,46.44 -70.01,46.71 -69.24,47.46 -69.05,47.43 -69.05,47.27 -68.94,47.21 -68.24,47.35 -67.81,47.08 -67.80,45.73 -67.43,45.60 -67.49,45.47 -67.43,45.38 -67.47,45.28 -67.37,45.17 -67.12,45.17
|
||||||
|
34.96,-11.58 34.89,-11.39 34.77,-11.34 34.61,-11.08 34.66,-10.71 34.58,-10.53 34.52,-10.03 34.32,-9.73 34.09,-9.54 34.00,-9.50 33.95,-9.66 33.89,-9.67
|
||||||
|
107.97,21.51 107.76,21.66 107.35,21.61 107.27,21.71 107.06,21.79 106.97,21.92 106.66,21.98 106.65,22.24 106.55,22.34 106.55,22.50 106.78,22.78 106.54,22.91 106.28,22.86 106.15,22.97 105.84,22.92 105.55,23.07 105.28,23.35 104.86,23.14 104.80,22.91 104.69,22.82 104.37,22.70 104.14,22.80 104.05,22.75 103.94,22.54 103.62,22.78 103.49,22.59 103.33,22.77 102.98,22.45 102.83,22.59 102.47,22.75 102.13,22.38
|
||||||
|
7.44,43.75 7.38,43.73
|
||||||
|
12.43,41.91 12.43,41.91
|
||||||
|
113.55,22.22 113.48,22.20
|
||||||
|
114.02,22.51 114.12,22.57 114.27,22.54
|
||||||
|
70.61,39.79 70.48,39.88 70.70,39.85 70.61,39.79
|
||||||
|
71.22,39.91 71.01,39.90 71.04,39.99 70.97,40.04 70.98,40.13 71.23,40.05 71.18,39.98 71.22,39.91
|
||||||
|
145.94,43.10 145.78,43.48 145.69,43.55 145.54,43.51 145.24,43.78 145.27,43.91 145.62,44.29 145.66,44.45 145.58,44.79
|
||||||
|
-1.28,32.09 -1.23,32.11 -1.24,32.34 -1.07,32.47 -1.45,32.78 -1.68,33.32 -1.63,33.57 -1.71,33.78 -1.71,34.18 -1.79,34.37 -1.73,34.47 -1.85,34.61 -1.80,34.75 -2.13,34.97 -2.22,35.10
|
||||||
|
29.00,9.61 29.47,9.77 29.60,9.92 29.61,10.07 30.00,10.28 30.76,9.73 31.22,9.80 31.79,10.38 31.93,10.66 32.42,11.09 32.34,11.31 32.34,11.72 32.07,12.01 32.74,12.01 32.72,12.22 33.20,12.22 33.07,11.59 33.17,10.85 33.13,10.76 33.38,10.65 33.91,10.18 33.96,9.86 33.87,9.51 34.08,9.46
|
||||||
|
24.15,8.67 24.21,8.77 24.53,8.89 24.67,9.39 24.78,9.53 24.79,9.81 24.96,9.99 25.02,10.24 25.10,10.31 25.86,10.41 25.92,10.17 26.55,9.53 26.66,9.48 27.07,9.61 27.88,9.60
|
||||||
|
27.88,9.60 28.05,9.33 28.84,9.33 28.84,9.46 29.00,9.61
|
||||||
|
-56.48,1.94 -56.70,2.04 -57.20,2.85 -57.30,3.38 -57.60,3.37
|
||||||
|
-54.62,2.33 -54.40,2.46 -54.20,2.82 -54.20,3.14 -54.08,3.33
|
||||||
|
33.89,-9.67 33.70,-9.60 33.42,-9.61 33.33,-9.52 32.92,-9.41
|
||||||
|
33.59,46.10 33.66,46.22 34.22,46.10 34.45,45.97 34.69,45.98 34.79,45.89 34.80,45.79 35.00,45.73
|
||||||
|
77.05,35.11 76.77,35.66
|
||||||
|
77.05,35.11 77.80,35.50
|
||||||
|
79.12,33.23 79.38,33.15 79.33,32.98 79.50,32.73 79.38,32.57 79.22,32.50
|
||||||
|
79.22,32.50 78.92,32.36 78.70,32.60 78.41,32.56 78.49,32.23
|
||||||
|
78.49,32.23 78.73,31.98 78.72,31.88
|
||||||
|
78.72,31.88 78.69,31.74 78.80,31.62 78.73,31.47 78.74,31.32 78.84,31.30
|
||||||
|
78.84,31.30 78.97,31.33 79.01,31.41 79.11,31.40 79.37,31.08
|
||||||
|
79.37,31.08 79.57,30.95 79.79,30.97 79.91,30.90
|
||||||
|
79.91,30.90 80.19,30.76 80.19,30.57
|
||||||
|
80.19,30.57 80.68,30.41 80.99,30.24 81.01,30.16
|
||||||
|
90.33,28.12 90.35,28.24 90.22,28.28 89.98,28.31 89.80,28.24
|
||||||
|
89.80,28.24 89.48,28.06 89.12,27.62
|
||||||
|
89.12,27.62 89.02,27.51
|
||||||
|
89.02,27.51 88.89,27.32
|
||||||
|
75.30,32.32 75.32,32.22 75.25,32.14 74.74,31.95 74.56,31.82 74.51,31.71 74.59,31.47 74.52,31.19 74.63,31.03 74.34,30.89 73.90,30.44 73.93,30.22 73.81,30.09 73.38,29.93 73.23,29.55 72.90,29.03 72.34,28.75 72.13,28.35 71.95,28.18 71.87,27.96 70.80,27.71 70.69,27.77 70.63,27.94 70.40,28.03 70.14,27.85 69.90,27.47 69.54,27.12 69.47,26.80 69.51,26.74 70.15,26.51 70.10,25.91 70.26,25.71 70.65,25.67 70.65,25.42 71.05,24.69 70.97,24.57 71.04,24.40 70.72,24.24 70.58,24.28 70.55,24.42 70.49,24.41 70.10,24.29 70.02,24.19 69.81,24.17 69.56,24.27 68.83,24.26 68.78,24.31 68.73,24.27 68.72,23.96 68.38,23.95 68.17,23.86
|
||||||
|
36.69,45.62 36.69,45.43 36.59,45.30 36.64,45.26 36.51,45.20 36.61,44.95
|
||||||
|
-55.81,-31.03 -56.00,-31.08 -56.00,-30.85
|
||||||
|
-56.00,-30.85 -56.18,-30.63 -56.83,-30.11 -57.12,-30.14 -57.21,-30.28 -57.55,-30.26 -57.61,-30.19
|
||||||
|
-73.48,-49.77 -73.58,-49.58 -73.46,-49.31 -73.14,-49.30
|
||||||
|
-73.14,-49.30 -73.15,-49.19 -73.03,-49.01 -72.65,-48.84 -72.58,-48.48 -72.35,-48.37 -72.29,-48.23 -72.33,-48.11 -72.51,-47.97 -72.52,-47.88 -72.35,-47.49 -72.04,-47.24 -71.90,-47.20 -71.96,-47.02 -71.94,-46.83 -71.70,-46.65 -71.76,-46.32 -71.88,-46.16 -71.63,-45.95 -71.75,-45.84 -71.75,-45.58 -71.51,-45.51 -71.35,-45.33 -71.35,-45.23 -71.60,-44.98 -72.04,-44.90 -72.06,-44.77 -71.26,-44.76 -71.16,-44.56 -71.21,-44.44 -71.82,-44.38 -71.81,-44.11 -71.68,-43.93 -71.79,-43.75 -71.74,-43.70 -71.75,-43.59 -71.90,-43.44 -71.90,-43.35 -71.75,-43.24 -71.78,-43.17 -72.10,-43.07 -72.14,-42.58 -72.05,-42.47 -72.11,-42.25 -72.03,-42.15 -71.86,-42.15 -71.75,-42.05 -71.91,-41.65 -71.87,-40.89 -71.94,-40.79 -71.80,-40.44 -71.70,-40.34 -71.82,-40.18 -71.66,-40.02 -71.64,-39.89 -71.72,-39.64 -71.54,-39.60 -71.42,-39.29 -71.40,-38.94 -70.95,-38.74 -70.85,-38.54 -70.97,-38.45 -71.03,-38.04 -71.17,-37.76 -71.13,-37.45 -71.20,-37.30 -71.12,-37.11 -71.19,-36.84 -71.06,-36.52 -70.75,-36.39 -70.72,-36.28 -70.40,-36.06 -70.42,-35.52 -70.56,-35.25 -70.39,-35.15 -70.29,-34.73 -70.05,-34.30 -69.85,-34.22 -69.89,-33.73 -69.80,-33.40 -69.82,-33.28 -70.02,-33.27 -70.08,-33.20 -70.09,-33.03 -70.02,-32.88 -70.12,-32.81 -70.17,-32.47 -70.32,-32.27 -70.36,-32.08 -70.25,-31.96 -70.45,-31.84 -70.59,-31.57 -70.52,-31.15 -70.39,-31.12 -70.31,-31.02 -70.35,-30.90 -70.15,-30.36 -69.96,-30.36 -69.84,-30.18 -69.96,-30.08 -69.93,-29.77 -70.03,-29.32 -69.83,-29.10 -69.66,-28.41 -69.17,-27.92 -68.85,-27.15 -68.71,-27.10 -68.59,-27.14 -68.32,-26.97 -68.32,-26.88 -68.59,-26.47 -68.58,-26.35 -68.41,-26.15 -68.59,-25.42 -68.50,-25.16 -68.38,-25.09 -68.56,-24.84 -68.56,-24.75 -68.25,-24.39 -67.36,-24.03 -67.01,-23.00 -67.19,-22.82
|
||||||
|
75.32,34.58 74.30,34.77 73.96,34.65 73.81,34.42 73.81,34.33 73.97,34.24 73.92,34.04 74.25,33.99 74.22,33.89 74.00,33.79 73.98,33.72 74.15,33.51 73.99,33.22 74.30,32.99 74.31,32.81
|
||||||
|
23.61,51.52 23.54,51.71 23.63,51.81 23.65,52.04 23.18,52.29 23.41,52.52 23.84,52.66 23.92,52.77 23.86,53.11 23.48,53.94
|
||||||
|
34.08,9.46 34.16,9.85 34.31,10.19 34.28,10.53 34.34,10.66 34.57,10.88 34.77,10.75 34.93,10.86 34.96,11.28 35.11,11.82 35.67,12.62 36.13,12.76 36.16,13.09 36.39,13.63 36.52,14.26
|
||||||
|
2.39,11.90 2.29,11.69 1.98,11.42 1.43,11.45 1.32,11.30 1.15,11.25 1.08,11.08 0.99,11.08 0.90,10.99
|
||||||
|
31.95,-25.96 31.87,-25.98 31.42,-25.75 31.21,-25.84 30.80,-26.41 30.79,-26.76 30.88,-26.79 31.06,-27.11 31.47,-27.30 31.96,-27.31 31.99,-26.82 32.11,-26.84
|
||||||
|
32.11,-26.84 32.06,-26.02 31.95,-25.96
|
||||||
|
76.77,35.66 76.56,35.77 76.55,35.89 76.15,35.83 76.07,35.98 75.91,36.05 75.97,36.38
|
||||||
|
27.33,5.19 27.79,4.64 28.19,4.35 28.43,4.32 28.73,4.50 29.22,4.39 29.47,4.61 29.68,4.59 30.19,3.98 30.51,3.84 30.59,3.62 30.76,3.62 30.84,3.49
|
||||||
|
22.86,10.92 23.31,10.39 23.65,9.82 23.62,9.34 23.47,9.11 23.55,8.94 23.54,8.82 23.68,8.73 24.15,8.67
|
||||||
|
46.11,38.88 46.49,38.91
|
||||||
|
44.78,39.68 45.14,39.25 45.48,39.01 46.11,38.88
|
||||||
|
46.49,38.91 47.48,39.50 47.77,39.65 48.00,39.68 48.32,39.40 48.10,39.24 48.29,39.02 48.05,38.93 48.00,38.85 48.59,38.41 48.87,38.44
|
||||||
|
81.01,30.16 80.85,30.14 80.40,29.73 80.25,29.42 80.23,29.19 80.08,28.99 80.07,28.83 80.42,28.61 80.59,28.65 81.85,27.87 81.99,27.91 82.45,27.67 82.68,27.67 82.73,27.52 83.29,27.37 83.45,27.47 83.83,27.38 84.09,27.49 84.61,27.30 84.69,27.04 85.13,26.86 85.24,26.75 85.57,26.84 85.70,26.78 85.79,26.60 86.01,26.65 86.70,26.44 87.02,26.56 87.09,26.43 87.29,26.36 87.41,26.42 87.85,26.44 88.00,26.38 88.16,26.72 87.98,27.13 88.15,27.75 88.11,27.87
|
||||||
|
75.97,36.38 75.84,36.65 75.67,36.74 75.42,36.74 75.35,36.91 74.54,37.02
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||||
|
<path fill="#6d4aff" d="M4 12a4 4 0 0 1 4-4h11.2a4 4 0 0 1 2.9 1.2L25 12h15a4 4 0 0 1 4 4v22a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4z"/>
|
||||||
|
<path fill="#8a6eff" d="M4 19a3 3 0 0 1 3-3h34a3 3 0 0 1 3 3v19a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4z"/>
|
||||||
|
<path fill="#ffffff" fill-opacity="0.18" d="M7 16h34a3 3 0 0 1 3 3v1a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v-1a3 3 0 0 1 3-3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 442 B |
@@ -10,14 +10,18 @@
|
|||||||
<file preprocess="xml-stripblanks">scalable/actions/pdfs-filter-symbolic.svg</file>
|
<file preprocess="xml-stripblanks">scalable/actions/pdfs-filter-symbolic.svg</file>
|
||||||
<file preprocess="xml-stripblanks">scalable/actions/pdfs-offline-symbolic.svg</file>
|
<file preprocess="xml-stripblanks">scalable/actions/pdfs-offline-symbolic.svg</file>
|
||||||
<file preprocess="xml-stripblanks">scalable/actions/pdfs-online-only-symbolic.svg</file>
|
<file preprocess="xml-stripblanks">scalable/actions/pdfs-online-only-symbolic.svg</file>
|
||||||
|
<!-- The Proton theme's folder: a full-colour icon from the icon theme
|
||||||
|
can't be recoloured by CSS. -->
|
||||||
|
<file preprocess="xml-stripblanks">scalable/places/pdfs-proton-folder.svg</file>
|
||||||
<!-- The app icon, from the one file packaging installs, so the in-app brand
|
<!-- The app icon, from the one file packaging installs, so the in-app brand
|
||||||
matches the launcher and resolves even when nothing is installed. -->
|
matches the launcher and resolves even when nothing is installed. -->
|
||||||
<file alias="scalable/apps/io.narl.proton-drive-linux.svg" preprocess="xml-stripblanks">../../../packaging/io.narl.proton-drive-linux.svg</file>
|
<file alias="scalable/apps/io.narl.proton-drive-linux.svg" preprocess="xml-stripblanks">../../../packaging/io.narl.proton-drive-linux.svg</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
<!-- The app stylesheet, loaded once at startup. Colours come from the
|
<!-- The app stylesheet, loaded once at startup. Colours come from the
|
||||||
libadwaita palette so the app follows the system accent, light/dark and
|
libadwaita palette so the app follows the system accent, light/dark and
|
||||||
high contrast; the Proton purple accent is a separate, opt-in provider. -->
|
high contrast. The Proton theme is a separate, opt-in provider. -->
|
||||||
<gresource prefix="/de/nils/protondrivelinux">
|
<gresource prefix="/de/nils/protondrivelinux">
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
|
<file>proton-theme.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/* Proton theme — the Proton web apps' Snow (light) and Carbon (dark) palettes.
|
||||||
|
*
|
||||||
|
* Loaded by main.rs after libadwaita's own stylesheet, which it re-applies on
|
||||||
|
* top of the user's GTK theme so that theme's hard-coded colours can't show
|
||||||
|
* through. Only libadwaita's colour variables are set here; every widget
|
||||||
|
* derives its look from them. Values from ProtonMail/WebClients,
|
||||||
|
* packages/colors/themes/src/{snow,carbon}/standard-base.css.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--accent-bg-color: #6d4aff;
|
||||||
|
--accent-fg-color: #ffffff;
|
||||||
|
--destructive-bg-color: #dc3251;
|
||||||
|
--destructive-fg-color: #ffffff;
|
||||||
|
--error-bg-color: #dc3251;
|
||||||
|
--error-fg-color: #ffffff;
|
||||||
|
--success-bg-color: #1ea885;
|
||||||
|
--success-fg-color: #ffffff;
|
||||||
|
--warning-bg-color: #ff9900;
|
||||||
|
--warning-fg-color: rgb(0 0 0 / 80%);
|
||||||
|
|
||||||
|
--window-bg-color: #ffffff;
|
||||||
|
--window-fg-color: #0c0c14;
|
||||||
|
--view-bg-color: #ffffff;
|
||||||
|
--view-fg-color: #0c0c14;
|
||||||
|
--headerbar-bg-color: #ffffff;
|
||||||
|
--headerbar-fg-color: #0c0c14;
|
||||||
|
--headerbar-backdrop-color: #ffffff;
|
||||||
|
--sidebar-bg-color: #f5f4f2;
|
||||||
|
--sidebar-fg-color: #0c0c14;
|
||||||
|
--sidebar-backdrop-color: #f8f8f6;
|
||||||
|
--secondary-sidebar-bg-color: #f8f8f6;
|
||||||
|
--secondary-sidebar-fg-color: #0c0c14;
|
||||||
|
--secondary-sidebar-backdrop-color: #f8f8f6;
|
||||||
|
--card-bg-color: #f5f4f2;
|
||||||
|
--card-fg-color: #0c0c14;
|
||||||
|
--dialog-bg-color: #ffffff;
|
||||||
|
--dialog-fg-color: #0c0c14;
|
||||||
|
--popover-bg-color: #ffffff;
|
||||||
|
--popover-fg-color: #0c0c14;
|
||||||
|
--thumbnail-bg-color: #f5f4f2;
|
||||||
|
--thumbnail-fg-color: #0c0c14;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--destructive-bg-color: #f5385a;
|
||||||
|
--error-bg-color: #f5385a;
|
||||||
|
|
||||||
|
--window-bg-color: #1e1c26;
|
||||||
|
--window-fg-color: #ffffff;
|
||||||
|
--view-bg-color: #1e1c26;
|
||||||
|
--view-fg-color: #ffffff;
|
||||||
|
--headerbar-bg-color: #1e1c26;
|
||||||
|
--headerbar-fg-color: #ffffff;
|
||||||
|
--headerbar-backdrop-color: #1e1c26;
|
||||||
|
--sidebar-bg-color: #16141c;
|
||||||
|
--sidebar-fg-color: #ffffff;
|
||||||
|
--sidebar-backdrop-color: #16141c;
|
||||||
|
--secondary-sidebar-bg-color: #1a1821;
|
||||||
|
--secondary-sidebar-fg-color: #ffffff;
|
||||||
|
--secondary-sidebar-backdrop-color: #1a1821;
|
||||||
|
--card-bg-color: #292733;
|
||||||
|
--card-fg-color: #ffffff;
|
||||||
|
--dialog-bg-color: #292733;
|
||||||
|
--dialog-fg-color: #ffffff;
|
||||||
|
--popover-bg-color: #292733;
|
||||||
|
--popover-fg-color: #ffffff;
|
||||||
|
--thumbnail-bg-color: #292733;
|
||||||
|
--thumbnail-fg-color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
* var(--success-color), ...) so the app follows the system accent, light/dark
|
* var(--success-color), ...) so the app follows the system accent, light/dark
|
||||||
* and high contrast. The one exception is the photo viewer, which always sits
|
* and high contrast. The one exception is the photo viewer, which always sits
|
||||||
* on a near-black backdrop and so uses fixed light-on-dark colours. The Proton
|
* on a near-black backdrop and so uses fixed light-on-dark colours. The Proton
|
||||||
* purple accent is opt-in and lives in its own provider (see main.rs).
|
* theme is opt-in and lives in its own provider (see main.rs and
|
||||||
|
* proton-theme.css).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Brand */
|
/* Brand */
|
||||||
@@ -89,7 +90,11 @@ progressbar.browser-status-meter trough, progressbar.browser-status-meter progre
|
|||||||
|
|
||||||
/* Photos: how many timeline filters are on, as a small badge on the Filters button. */
|
/* Photos: how many timeline filters are on, as a small badge on the Filters button. */
|
||||||
.filter-count { min-width: 18px; padding: 0 5px; border-radius: 999px; font-size: 0.8em; font-weight: 700; color: var(--accent-fg-color); background-color: var(--accent-bg-color); }
|
.filter-count { min-width: 18px; padding: 0 5px; border-radius: 999px; font-size: 0.8em; font-weight: 700; color: var(--accent-fg-color); background-color: var(--accent-bg-color); }
|
||||||
/* The Places map: water a faint wash of the text colour, bubbles (drawn in
|
/* The Places map: sea and land come in the map's own colours; markers (drawn
|
||||||
* the area's own colour) in the accent. */
|
* in the area's own colour) in the accent. */
|
||||||
.places-map-frame { border-radius: 12px; background: color-mix(in srgb, currentColor 5%, transparent); }
|
.places-map-frame { border-radius: 12px; }
|
||||||
.places-map { color: var(--accent-bg-color); }
|
.places-map { color: var(--accent-bg-color); }
|
||||||
|
/* Credit on the Places street map, in the corner as on every web map. */
|
||||||
|
.places-spot-photo { padding: 0; }
|
||||||
|
.places-spot-photo picture { border-radius: 6px; overflow: hidden; }
|
||||||
|
.places-map-credit { padding: 2px 8px; border-top-left-radius: 8px; color: var(--window-fg-color); background: color-mix(in srgb, var(--window-bg-color) 80%, transparent); }
|
||||||
+112
-21
@@ -69,10 +69,6 @@ use pdfs_core::service;
|
|||||||
|
|
||||||
const APP_ID: &str = "io.narl.proton-drive-linux";
|
const APP_ID: &str = "io.narl.proton-drive-linux";
|
||||||
|
|
||||||
/// Proton brand purple, applied as the libadwaita accent when the user opts in
|
|
||||||
/// (Preferences → Appearance), so switches, buttons and links all pick it up.
|
|
||||||
const PROTON_PURPLE: &str = "#6d4aff";
|
|
||||||
|
|
||||||
/// How often the window re-reads mount status, cache usage and the pin list.
|
/// How often the window re-reads mount status, cache usage and the pin list.
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
@@ -213,7 +209,7 @@ fn main() -> glib::ExitCode {
|
|||||||
app.connect_startup(|_| {
|
app.connect_startup(|_| {
|
||||||
load_proton_theme();
|
load_proton_theme();
|
||||||
if let Ok(dirs) = AppDirs::new() {
|
if let Ok(dirs) = AppDirs::new() {
|
||||||
set_proton_accent(dirs.load_config().proton_accent.unwrap_or(false));
|
set_proton_theme(dirs.load_config().proton_theme.unwrap_or(false));
|
||||||
}
|
}
|
||||||
// Refresh the file manager's right-click pin/unpin scripts, so they always
|
// Refresh the file manager's right-click pin/unpin scripts, so they always
|
||||||
// match the installed `pdfs`.
|
// match the installed `pdfs`.
|
||||||
@@ -289,8 +285,8 @@ pub(crate) fn spawn_tray() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Register the bundled GResources (custom icons, the stylesheet) and load the
|
/// Register the bundled GResources (custom icons, the stylesheet) and load the
|
||||||
/// app stylesheet. The Proton accent is a separate provider, applied by
|
/// app stylesheet. The Proton theme is a separate provider, applied by
|
||||||
/// [`set_proton_accent`] from the saved preference.
|
/// [`set_proton_theme`] from the saved preference.
|
||||||
fn load_proton_theme() {
|
fn load_proton_theme() {
|
||||||
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/pdfs.gresource"));
|
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/pdfs.gresource"));
|
||||||
let resource_data = glib::Bytes::from_static(bytes);
|
let resource_data = glib::Bytes::from_static(bytes);
|
||||||
@@ -309,38 +305,124 @@ fn load_proton_theme() {
|
|||||||
// `button { padding: 6px 10px; min-width: 16px }` would otherwise pad every
|
// `button { padding: 6px 10px; min-width: 16px }` would otherwise pad every
|
||||||
// photo tile and push the grid past the window. Every rule in the sheet is
|
// photo tile and push the grid past the window. Every rule in the sheet is
|
||||||
// scoped to the app's own classes, so nothing else the user styled changes.
|
// scoped to the app's own classes, so nothing else the user styled changes.
|
||||||
|
// One above the Proton theme too, so the app's rules refine it.
|
||||||
gtk4::style_context_add_provider_for_display(
|
gtk4::style_context_add_provider_for_display(
|
||||||
&display,
|
&display,
|
||||||
&provider,
|
&provider,
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_USER + 1,
|
gtk4::STYLE_PROVIDER_PRIORITY_USER + 2,
|
||||||
);
|
);
|
||||||
gtk4::IconTheme::for_display(&display).add_resource_path("/de/nils/protondrivelinux/icons");
|
gtk4::IconTheme::for_display(&display).add_resource_path("/de/nils/protondrivelinux/icons");
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
/// The Proton purple accent provider while it is installed. Kept so turning
|
/// The Proton theme provider while it is installed. Kept so turning the
|
||||||
/// the preference off can remove exactly what turning it on added.
|
/// preference off can remove exactly what turning it on added.
|
||||||
static PROTON_ACCENT: RefCell<Option<gtk4::CssProvider>> = const { RefCell::new(None) };
|
static PROTON_THEME: RefCell<Option<gtk4::CssProvider>> = const { RefCell::new(None) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint the app in Proton purple (`on`) or follow the system accent colour.
|
/// Whether the Proton theme is on.
|
||||||
/// Every accent-coloured widget reads `--accent-bg-color`, so overriding that
|
fn proton_theme_active() -> bool {
|
||||||
/// one variable recolours the whole app.
|
PROTON_THEME.with(|slot| slot.borrow().is_some())
|
||||||
fn set_proton_accent(on: bool) {
|
}
|
||||||
|
|
||||||
|
/// libadwaita's own stylesheet, bundled in the library.
|
||||||
|
const ADWAITA_STYLESHEET: &str = "/org/gnome/Adwaita/styles/gtk.css";
|
||||||
|
|
||||||
|
/// The Proton palette laid over it, from our own bundle.
|
||||||
|
const PROTON_PALETTE: &str = "/de/nils/protondrivelinux/proton-theme.css";
|
||||||
|
|
||||||
|
/// Make the theme provider's `@media (prefers-color-scheme)` and
|
||||||
|
/// `(prefers-contrast)` rules match what libadwaita shows, now and whenever it
|
||||||
|
/// changes. A provider evaluates those itself, and a new one assumes light.
|
||||||
|
///
|
||||||
|
/// The properties are set by name because they only exist since GTK 4.20, and
|
||||||
|
/// the app still runs on older GTK, whose libadwaita has no such rules anyway.
|
||||||
|
fn follow_style_manager(provider: >k4::CssProvider) {
|
||||||
|
fn set_enum(provider: >k4::CssProvider, property: &str, nick: &str) {
|
||||||
|
let Some(pspec) = provider.find_property(property) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(value) = glib::EnumClass::with_type(pspec.value_type())
|
||||||
|
.and_then(|class| class.to_value_by_nick(nick))
|
||||||
|
{
|
||||||
|
provider.set_property_from_value(property, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn apply(provider: >k4::CssProvider, manager: &adw::StyleManager) {
|
||||||
|
let scheme = if manager.is_dark() { "dark" } else { "light" };
|
||||||
|
set_enum(provider, "prefers-color-scheme", scheme);
|
||||||
|
let contrast = if manager.is_high_contrast() {
|
||||||
|
"more"
|
||||||
|
} else {
|
||||||
|
"no-preference"
|
||||||
|
};
|
||||||
|
set_enum(provider, "prefers-contrast", contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = adw::StyleManager::default();
|
||||||
|
apply(provider, &manager);
|
||||||
|
// Weak, so a provider the preference removed isn't kept alive; its
|
||||||
|
// handlers then do nothing.
|
||||||
|
let weak = provider.downgrade();
|
||||||
|
let weak_dark = weak.clone();
|
||||||
|
manager.connect_dark_notify(move |manager| {
|
||||||
|
if let Some(provider) = weak_dark.upgrade() {
|
||||||
|
apply(&provider, manager);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
manager.connect_high_contrast_notify(move |manager| {
|
||||||
|
if let Some(provider) = weak.upgrade() {
|
||||||
|
apply(&provider, manager);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paint the app in the Proton web apps' colours (`on`) or follow the system
|
||||||
|
/// theme and accent colour.
|
||||||
|
///
|
||||||
|
/// Setting libadwaita's colour variables alone isn't enough: a GTK theme the
|
||||||
|
/// user's `gtk.css` imports (Catppuccin, Fluent, ...) sits above the app and
|
||||||
|
/// hard-codes its own accent into hundreds of rules. So the theme provider,
|
||||||
|
/// one above the user's CSS, first resets every property, then re-applies
|
||||||
|
/// libadwaita's whole stylesheet and finally the Proton palette, which leaves
|
||||||
|
/// nothing of the user's theme inside the app.
|
||||||
|
fn set_proton_theme(on: bool) {
|
||||||
let Some(display) = gtk4::gdk::Display::default() else {
|
let Some(display) = gtk4::gdk::Display::default() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
PROTON_ACCENT.with(|slot| {
|
PROTON_THEME.with(|slot| {
|
||||||
let mut slot = slot.borrow_mut();
|
let mut slot = slot.borrow_mut();
|
||||||
match (on, slot.as_ref()) {
|
match (on, slot.as_ref()) {
|
||||||
(true, None) => {
|
(true, None) => {
|
||||||
|
let stylesheet = |path| {
|
||||||
|
gio::resources_lookup_data(path, gio::ResourceLookupFlags::NONE)
|
||||||
|
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
|
||||||
|
};
|
||||||
|
let palette = stylesheet(PROTON_PALETTE).unwrap_or_default();
|
||||||
|
let css = match stylesheet(ADWAITA_STYLESHEET) {
|
||||||
|
// Its images are named relative to the sheet, which a
|
||||||
|
// string loaded here doesn't have.
|
||||||
|
Ok(adwaita) => format!(
|
||||||
|
"* {{ all: unset; }}\n{}\n{palette}",
|
||||||
|
adwaita.replace(
|
||||||
|
"url(\"assets/",
|
||||||
|
"url(\"resource:///org/gnome/Adwaita/styles/assets/"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
// Still recolours everything that follows libadwaita's
|
||||||
|
// variables; only a custom theme's own rules show through.
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("libadwaita stylesheet not found, only recolouring: {e}");
|
||||||
|
palette
|
||||||
|
}
|
||||||
|
};
|
||||||
let provider = gtk4::CssProvider::new();
|
let provider = gtk4::CssProvider::new();
|
||||||
provider
|
follow_style_manager(&provider);
|
||||||
.load_from_string(&format!(":root {{ --accent-bg-color: {PROTON_PURPLE}; }}"));
|
provider.load_from_string(&css);
|
||||||
gtk4::style_context_add_provider_for_display(
|
gtk4::style_context_add_provider_for_display(
|
||||||
&display,
|
&display,
|
||||||
&provider,
|
&provider,
|
||||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
|
gtk4::STYLE_PROVIDER_PRIORITY_USER + 1,
|
||||||
);
|
);
|
||||||
*slot = Some(provider);
|
*slot = Some(provider);
|
||||||
}
|
}
|
||||||
@@ -454,7 +536,8 @@ fn build_window(app: &adw::Application) {
|
|||||||
upload_limit_row: main_widgets.upload_limit_row.clone(),
|
upload_limit_row: main_widgets.upload_limit_row.clone(),
|
||||||
download_limit_row: main_widgets.download_limit_row.clone(),
|
download_limit_row: main_widgets.download_limit_row.clone(),
|
||||||
mountpoint_row: main_widgets.mountpoint_row.clone(),
|
mountpoint_row: main_widgets.mountpoint_row.clone(),
|
||||||
accent_row: main_widgets.accent_row.clone(),
|
theme_row: main_widgets.theme_row.clone(),
|
||||||
|
online_map_row: main_widgets.online_map_row.clone(),
|
||||||
tray_row: main_widgets.tray_row.clone(),
|
tray_row: main_widgets.tray_row.clone(),
|
||||||
language_row: main_widgets.language_row.clone(),
|
language_row: main_widgets.language_row.clone(),
|
||||||
settings_suppress: Cell::new(false),
|
settings_suppress: Cell::new(false),
|
||||||
@@ -1368,7 +1451,15 @@ fn install_window_actions(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
|
|||||||
None,
|
None,
|
||||||
gtk4::License::Custom,
|
gtk4::License::Custom,
|
||||||
Some(&gettext(
|
Some(&gettext(
|
||||||
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/\">Natural Earth</a> land outlines, which are in the public domain.",
|
"The places map is drawn from <a href=\"https://www.naturalearthdata.com/\">Natural Earth</a> land outlines and country borders, which are in the public domain.",
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
dialog.add_legal_section(
|
||||||
|
"OpenStreetMap",
|
||||||
|
None,
|
||||||
|
gtk4::License::Custom,
|
||||||
|
Some(&gettext(
|
||||||
|
"The street map, when turned on, comes from <a href=\"https://openfreemap.org/\">OpenFreeMap</a> in the <a href=\"https://www.openmaptiles.org/\">OpenMapTiles</a> schema. Map data © <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors, available under the Open Database License.",
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
dialog.add_legal_section(
|
dialog.add_legal_section(
|
||||||
|
|||||||
@@ -2797,6 +2797,15 @@ pub(crate) fn attach_drop(ui: &Rc<Ui>, item: >k4::ListItem, anchor: >k4::Box
|
|||||||
anchor.add_controller(target);
|
anchor.add_controller(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The full-colour icon for an entry: [`icon_base_for`], except that folders
|
||||||
|
/// are Proton's own purple ones while the Proton theme is on.
|
||||||
|
pub(crate) fn icon_for(entry: &DirEntry) -> &'static str {
|
||||||
|
if entry.is_dir && proton_theme_active() {
|
||||||
|
return "pdfs-proton-folder";
|
||||||
|
}
|
||||||
|
icon_base_for(entry)
|
||||||
|
}
|
||||||
|
|
||||||
/// Pick a freedesktop icon base name for an entry from its kind / extension.
|
/// Pick a freedesktop icon base name for an entry from its kind / extension.
|
||||||
/// Callers append `-symbolic` for the column view's small icons.
|
/// Callers append `-symbolic` for the column view's small icons.
|
||||||
pub(crate) fn icon_base_for(entry: &DirEntry) -> &'static str {
|
pub(crate) fn icon_base_for(entry: &DirEntry) -> &'static str {
|
||||||
|
|||||||
@@ -0,0 +1,877 @@
|
|||||||
|
//! The online street map under the Places map: vector tiles from OpenFreeMap,
|
||||||
|
//! fetched and drawn on a worker thread, and handed to the map as finished
|
||||||
|
//! images.
|
||||||
|
//!
|
||||||
|
//! It is off unless turned on in Preferences, since every tile fetched tells
|
||||||
|
//! OpenFreeMap which part of the world is being looked at. OpenFreeMap needs
|
||||||
|
//! no account or key, and its tiles are OpenStreetMap data in the
|
||||||
|
//! OpenMapTiles schema; both have to be credited wherever the map shows.
|
||||||
|
//!
|
||||||
|
//! Tiles are kept on disk under the cache directory, per build of the tile
|
||||||
|
//! set. A build never changes, so a cached tile is good until OpenFreeMap
|
||||||
|
//! publishes the next one, and then the old build's directory goes. Town and
|
||||||
|
//! country names are not baked into the images: they come back as [`Label`]s
|
||||||
|
//! for the map to set on top, where it can keep them upright, sharp and apart
|
||||||
|
//! from each other.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use gtk4::cairo;
|
||||||
|
|
||||||
|
use super::mvt::{self, Kind, Layer};
|
||||||
|
|
||||||
|
/// Where OpenFreeMap describes its current tile set.
|
||||||
|
const TILEJSON_URL: &str = "https://tiles.openfreemap.org/planet";
|
||||||
|
|
||||||
|
/// The deepest zoom OpenFreeMap has tiles for. Closer in, a tile is drawn
|
||||||
|
/// from part of its ancestor at this zoom.
|
||||||
|
pub(crate) const DATA_MAX_ZOOM: u8 = 14;
|
||||||
|
|
||||||
|
/// A tile's edge in logical px at the zoom it was made for.
|
||||||
|
pub(crate) const TILE_EDGE: f64 = 256.0;
|
||||||
|
|
||||||
|
/// How many tiles are fetched at once.
|
||||||
|
const PARALLEL_FETCHES: usize = 6;
|
||||||
|
|
||||||
|
/// How long one fetch may take.
|
||||||
|
const FETCH_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
|
/// Past this size the tile cache is emptied rather than left to grow.
|
||||||
|
const DISK_BUDGET: u64 = 256 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// One tile of the Web Mercator grid: at zoom `z` the world is `2^z` tiles
|
||||||
|
/// wide and tall, counted from the north-west.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub(crate) struct TileKey {
|
||||||
|
pub(crate) z: u8,
|
||||||
|
pub(crate) x: u32,
|
||||||
|
pub(crate) y: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileKey {
|
||||||
|
pub(crate) fn parent(self) -> Option<Self> {
|
||||||
|
(self.z > 0).then(|| Self {
|
||||||
|
z: self.z - 1,
|
||||||
|
x: self.x / 2,
|
||||||
|
y: self.y / 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tile whose data this one is drawn from.
|
||||||
|
fn data(self) -> Self {
|
||||||
|
let mut key = self;
|
||||||
|
while key.z > DATA_MAX_ZOOM {
|
||||||
|
key = key.parent().unwrap_or(key);
|
||||||
|
}
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The colours of the map, for a light or a dark window.
|
||||||
|
pub(crate) struct Palette {
|
||||||
|
pub(crate) land: u32,
|
||||||
|
pub(crate) water: u32,
|
||||||
|
wood: u32,
|
||||||
|
grass: u32,
|
||||||
|
ice: u32,
|
||||||
|
sand: u32,
|
||||||
|
residential: u32,
|
||||||
|
industrial: u32,
|
||||||
|
building: u32,
|
||||||
|
motorway: u32,
|
||||||
|
primary: u32,
|
||||||
|
road: u32,
|
||||||
|
casing: u32,
|
||||||
|
rail: u32,
|
||||||
|
path: u32,
|
||||||
|
pub(crate) border: u32,
|
||||||
|
state_border: u32,
|
||||||
|
pub(crate) coast: u32,
|
||||||
|
pub(crate) text: u32,
|
||||||
|
pub(crate) halo: u32,
|
||||||
|
pub(crate) water_text: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const LIGHT: Palette = Palette {
|
||||||
|
land: 0xf4f2ee,
|
||||||
|
water: 0xb3cde3,
|
||||||
|
wood: 0xd3e5c5,
|
||||||
|
grass: 0xdcebcf,
|
||||||
|
ice: 0xf2f6f9,
|
||||||
|
sand: 0xefe7d2,
|
||||||
|
residential: 0xebe7e2,
|
||||||
|
industrial: 0xebe4e8,
|
||||||
|
building: 0xdcd5ce,
|
||||||
|
motorway: 0xf2c98f,
|
||||||
|
primary: 0xfbe4ad,
|
||||||
|
road: 0xffffff,
|
||||||
|
casing: 0xd2cbc1,
|
||||||
|
rail: 0xb5afa8,
|
||||||
|
path: 0xbfae9c,
|
||||||
|
border: 0x9d94b5,
|
||||||
|
state_border: 0xc2bbd2,
|
||||||
|
coast: 0x9fbbd4,
|
||||||
|
text: 0x34313d,
|
||||||
|
halo: 0xffffff,
|
||||||
|
water_text: 0x4f7398,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) const DARK: Palette = Palette {
|
||||||
|
land: 0x24222d,
|
||||||
|
water: 0x141c2b,
|
||||||
|
wood: 0x212c26,
|
||||||
|
grass: 0x232d27,
|
||||||
|
ice: 0x2c313a,
|
||||||
|
sand: 0x2d2b27,
|
||||||
|
residential: 0x292733,
|
||||||
|
industrial: 0x2b2833,
|
||||||
|
building: 0x322f3c,
|
||||||
|
motorway: 0x7a6647,
|
||||||
|
primary: 0x625a4b,
|
||||||
|
road: 0x46434f,
|
||||||
|
casing: 0x1a1822,
|
||||||
|
rail: 0x4e4a5a,
|
||||||
|
path: 0x3f3b49,
|
||||||
|
border: 0x857ca0,
|
||||||
|
state_border: 0x5c556f,
|
||||||
|
coast: 0x2c3b52,
|
||||||
|
text: 0xe7e4ef,
|
||||||
|
halo: 0x16141c,
|
||||||
|
water_text: 0x86a6cb,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn palette(dark: bool) -> &'static Palette {
|
||||||
|
if dark { &DARK } else { &LIGHT }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_rgb(cr: &cairo::Context, color: u32, alpha: f64) {
|
||||||
|
cr.set_source_rgba(
|
||||||
|
f64::from((color >> 16) & 0xff) / 255.0,
|
||||||
|
f64::from((color >> 8) & 0xff) / 255.0,
|
||||||
|
f64::from(color & 0xff) / 255.0,
|
||||||
|
alpha,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What kind of place a name stands for, which sets how it is written and
|
||||||
|
/// which names win when they would overlap.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub(crate) enum LabelClass {
|
||||||
|
Country,
|
||||||
|
State,
|
||||||
|
City,
|
||||||
|
Town,
|
||||||
|
Village,
|
||||||
|
Suburb,
|
||||||
|
Hamlet,
|
||||||
|
Water,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LabelClass {
|
||||||
|
fn from_place(class: &str) -> Option<Self> {
|
||||||
|
Some(match class {
|
||||||
|
"country" => Self::Country,
|
||||||
|
"state" | "province" => Self::State,
|
||||||
|
"city" => Self::City,
|
||||||
|
"town" => Self::Town,
|
||||||
|
"village" => Self::Village,
|
||||||
|
"suburb" | "quarter" => Self::Suburb,
|
||||||
|
"hamlet" | "neighbourhood" | "isolated_dwelling" => Self::Hamlet,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The zoom from which such names are shown at all.
|
||||||
|
pub(crate) fn min_zoom(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Country | Self::Water | Self::City => 0,
|
||||||
|
Self::State => 4,
|
||||||
|
Self::Town => 8,
|
||||||
|
Self::Village => 11,
|
||||||
|
Self::Suburb => 12,
|
||||||
|
Self::Hamlet => 14,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The zoom past which such names are left out again: a country's name
|
||||||
|
/// is no help once its towns are.
|
||||||
|
pub(crate) fn max_zoom(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Country => 7,
|
||||||
|
Self::State => 9,
|
||||||
|
_ => u8::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A name to set on the map, at its place in world space (0..1 both ways).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Label {
|
||||||
|
pub(crate) x: f64,
|
||||||
|
pub(crate) y: f64,
|
||||||
|
pub(crate) text: String,
|
||||||
|
pub(crate) class: LabelClass,
|
||||||
|
/// Lower is more important, within a class.
|
||||||
|
pub(crate) rank: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A finished tile: premultiplied ARGB pixels, as cairo keeps them.
|
||||||
|
pub(crate) struct Raster {
|
||||||
|
pub(crate) key: TileKey,
|
||||||
|
pub(crate) dark: bool,
|
||||||
|
pub(crate) edge: i32,
|
||||||
|
pub(crate) pixels: Vec<u8>,
|
||||||
|
pub(crate) labels: Vec<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Raster {
|
||||||
|
pub(crate) fn surface(self) -> Option<cairo::ImageSurface> {
|
||||||
|
let stride = self.edge * 4;
|
||||||
|
cairo::ImageSurface::create_for_data(
|
||||||
|
self.pixels,
|
||||||
|
cairo::Format::ARgb32,
|
||||||
|
self.edge,
|
||||||
|
self.edge,
|
||||||
|
stride,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum Done {
|
||||||
|
Tile(Raster),
|
||||||
|
/// Nothing came: no network, or no such tile. The outline shows there.
|
||||||
|
Failed(TileKey),
|
||||||
|
/// The tile went off screen before its turn.
|
||||||
|
Dropped(TileKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Job {
|
||||||
|
key: TileKey,
|
||||||
|
dark: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The worker thread making tiles. Dropping it stops the thread once the
|
||||||
|
/// fetches under way are done.
|
||||||
|
pub(crate) struct Tiles {
|
||||||
|
jobs: async_channel::Sender<Job>,
|
||||||
|
wanted: Arc<Mutex<HashSet<TileKey>>>,
|
||||||
|
/// Device pixels per logical px the tiles are made at.
|
||||||
|
pub(crate) scale: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tiles {
|
||||||
|
/// Start the worker, caching under `cache_dir`, drawing names in
|
||||||
|
/// `language` where the map has them. Finished tiles arrive on the
|
||||||
|
/// returned channel.
|
||||||
|
pub(crate) fn start(
|
||||||
|
cache_dir: PathBuf,
|
||||||
|
language: String,
|
||||||
|
scale: i32,
|
||||||
|
) -> (Self, async_channel::Receiver<Done>) {
|
||||||
|
let (jobs, job_rx) = async_channel::unbounded();
|
||||||
|
let (done_tx, done_rx) = async_channel::unbounded();
|
||||||
|
let wanted = Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
let worker = Worker {
|
||||||
|
dir: cache_dir,
|
||||||
|
language,
|
||||||
|
scale,
|
||||||
|
wanted: wanted.clone(),
|
||||||
|
done: done_tx,
|
||||||
|
};
|
||||||
|
let spawned = std::thread::Builder::new()
|
||||||
|
.name("map-tiles".into())
|
||||||
|
.spawn(move || worker.run(job_rx));
|
||||||
|
if let Err(e) = spawned {
|
||||||
|
tracing::warn!("map tiles: cannot start the worker: {e}");
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
jobs,
|
||||||
|
wanted,
|
||||||
|
scale,
|
||||||
|
},
|
||||||
|
done_rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn request(&self, key: TileKey, dark: bool) {
|
||||||
|
let _ = self.jobs.try_send(Job { key, dark });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tiles on screen now. A queued tile that isn't is skipped.
|
||||||
|
pub(crate) fn set_wanted(&self, keys: HashSet<TileKey>) {
|
||||||
|
if let Ok(mut wanted) = self.wanted.lock() {
|
||||||
|
*wanted = keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Worker {
|
||||||
|
dir: PathBuf,
|
||||||
|
language: String,
|
||||||
|
scale: i32,
|
||||||
|
wanted: Arc<Mutex<HashSet<TileKey>>>,
|
||||||
|
done: async_channel::Sender<Done>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
fn run(self, jobs: async_channel::Receiver<Job>) {
|
||||||
|
let runtime = match tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(runtime) => runtime,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("map tiles: no runtime: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let client = match reqwest::Client::builder()
|
||||||
|
.user_agent(concat!(
|
||||||
|
"proton-drive-linux/",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
" (",
|
||||||
|
env!("CARGO_PKG_REPOSITORY"),
|
||||||
|
")"
|
||||||
|
))
|
||||||
|
.timeout(FETCH_TIMEOUT)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("map tiles: no HTTP client: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let worker = Arc::new(self);
|
||||||
|
runtime.block_on(async move {
|
||||||
|
let source = Arc::new(tokio::sync::Mutex::new(None::<String>));
|
||||||
|
let slots = Arc::new(tokio::sync::Semaphore::new(PARALLEL_FETCHES));
|
||||||
|
while let Ok(job) = jobs.recv().await {
|
||||||
|
let Ok(permit) = slots.clone().acquire_owned().await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let (worker, client, source) = (worker.clone(), client.clone(), source.clone());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let done = worker.make(&client, &source, job).await;
|
||||||
|
let _ = worker.done.send(done).await;
|
||||||
|
drop(permit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Let the tiles under way finish before the runtime goes.
|
||||||
|
let _ = slots.acquire_many(PARALLEL_FETCHES as u32).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
client: &reqwest::Client,
|
||||||
|
source: &tokio::sync::Mutex<Option<String>>,
|
||||||
|
job: Job,
|
||||||
|
) -> Done {
|
||||||
|
let key = job.key;
|
||||||
|
let still_wanted = self.wanted.lock().map(|w| w.contains(&key)).unwrap_or(true);
|
||||||
|
if !still_wanted {
|
||||||
|
return Done::Dropped(key);
|
||||||
|
}
|
||||||
|
let Some(data) = self.load(client, source, key.data()).await else {
|
||||||
|
return Done::Failed(key);
|
||||||
|
};
|
||||||
|
let worker = self.clone();
|
||||||
|
let made = tokio::task::spawn_blocking(move || {
|
||||||
|
let layers = mvt::decode(&data)?;
|
||||||
|
render(&layers, key, job.dark, worker.scale, &worker.language)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match made {
|
||||||
|
Ok(Some(raster)) => Done::Tile(raster),
|
||||||
|
_ => Done::Failed(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tile's data, from the disk cache or else from OpenFreeMap.
|
||||||
|
async fn load(
|
||||||
|
&self,
|
||||||
|
client: &reqwest::Client,
|
||||||
|
source: &tokio::sync::Mutex<Option<String>>,
|
||||||
|
key: TileKey,
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
let template = {
|
||||||
|
let mut source = source.lock().await;
|
||||||
|
if source.is_none() {
|
||||||
|
*source = self.tile_source(client).await;
|
||||||
|
}
|
||||||
|
source.clone()?
|
||||||
|
};
|
||||||
|
let path = self
|
||||||
|
.build_dir(&template)?
|
||||||
|
.join(key.z.to_string())
|
||||||
|
.join(key.x.to_string())
|
||||||
|
.join(format!("{}.pbf", key.y));
|
||||||
|
if let Ok(data) = tokio::fs::read(&path).await {
|
||||||
|
return Some(data);
|
||||||
|
}
|
||||||
|
let url = template
|
||||||
|
.replace("{z}", &key.z.to_string())
|
||||||
|
.replace("{x}", &key.x.to_string())
|
||||||
|
.replace("{y}", &key.y.to_string());
|
||||||
|
let reply = client.get(&url).send().await.ok()?;
|
||||||
|
if !reply.status().is_success() {
|
||||||
|
tracing::debug!("map tile {url}: {}", reply.status());
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let data = reply.bytes().await.ok()?.to_vec();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = tokio::fs::create_dir_all(parent).await;
|
||||||
|
}
|
||||||
|
let _ = tokio::fs::write(&path, &data).await;
|
||||||
|
Some(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The URL template of OpenFreeMap's current build, remembered on disk so
|
||||||
|
/// the cached tiles still show without a network.
|
||||||
|
async fn tile_source(&self, client: &reqwest::Client) -> Option<String> {
|
||||||
|
let saved = self.dir.join("tiles.json");
|
||||||
|
let fetched = async {
|
||||||
|
let reply = client.get(TILEJSON_URL).send().await.ok()?;
|
||||||
|
if !reply.status().is_success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
reply.bytes().await.ok()
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
let (json, fresh) = match fetched {
|
||||||
|
Some(json) => (json.to_vec(), true),
|
||||||
|
None => (tokio::fs::read(&saved).await.ok()?, false),
|
||||||
|
};
|
||||||
|
let template = tile_template(&json)?;
|
||||||
|
if fresh {
|
||||||
|
let _ = tokio::fs::create_dir_all(&self.dir).await;
|
||||||
|
let _ = tokio::fs::write(&saved, &json).await;
|
||||||
|
let (dir, build) = (self.dir.clone(), build_name(&template)?);
|
||||||
|
let _ = tokio::task::spawn_blocking(move || prune(&dir, &build)).await;
|
||||||
|
}
|
||||||
|
Some(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_dir(&self, template: &str) -> Option<PathBuf> {
|
||||||
|
Some(self.dir.join(build_name(template)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first tile URL template in a TileJSON document.
|
||||||
|
fn tile_template(json: &[u8]) -> Option<String> {
|
||||||
|
let doc: serde_json::Value = serde_json::from_slice(json).ok()?;
|
||||||
|
let template = doc.get("tiles")?.get(0)?.as_str()?;
|
||||||
|
template
|
||||||
|
.starts_with("https://")
|
||||||
|
.then(|| template.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tile set's build, the path segment before `/{z}`: a safe directory
|
||||||
|
/// name, or `None`.
|
||||||
|
fn build_name(template: &str) -> Option<String> {
|
||||||
|
let head = template.split("/{z}").next()?;
|
||||||
|
let name = head.rsplit('/').next()?;
|
||||||
|
let safe = !name.is_empty()
|
||||||
|
&& name
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
|
||||||
|
&& name != "."
|
||||||
|
&& name != "..";
|
||||||
|
safe.then(|| name.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the cached builds other than `keep`, and `keep` itself once it
|
||||||
|
/// holds more than [`DISK_BUDGET`].
|
||||||
|
fn prune(dir: &Path, keep: &str) {
|
||||||
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if !path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let stale = entry.file_name() != keep || dir_size(&path) > DISK_BUDGET;
|
||||||
|
if stale && let Err(e) = std::fs::remove_dir_all(&path) {
|
||||||
|
tracing::debug!("map tiles: cannot remove {}: {e}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir_size(dir: &Path) -> u64 {
|
||||||
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
entries
|
||||||
|
.flatten()
|
||||||
|
.map(|entry| match entry.metadata() {
|
||||||
|
Ok(meta) if meta.is_dir() => dir_size(&entry.path()),
|
||||||
|
Ok(meta) => meta.len(),
|
||||||
|
Err(_) => 0,
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw tile `key` from the layers of its data tile, at `scale` device px
|
||||||
|
/// per logical px, and gather the names on it.
|
||||||
|
fn render(
|
||||||
|
layers: &[Layer],
|
||||||
|
key: TileKey,
|
||||||
|
dark: bool,
|
||||||
|
scale: i32,
|
||||||
|
language: &str,
|
||||||
|
) -> Option<Raster> {
|
||||||
|
let edge = (TILE_EDGE as i32) * scale.max(1);
|
||||||
|
let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, edge, edge).ok()?;
|
||||||
|
let labels = {
|
||||||
|
let cr = cairo::Context::new(&surface).ok()?;
|
||||||
|
cr.scale(f64::from(scale), f64::from(scale));
|
||||||
|
let painter = Painter {
|
||||||
|
cr: &cr,
|
||||||
|
key,
|
||||||
|
data: key.data(),
|
||||||
|
colors: palette(dark),
|
||||||
|
};
|
||||||
|
painter.paint(layers);
|
||||||
|
gather_labels(layers, key.data(), language)
|
||||||
|
};
|
||||||
|
surface.flush();
|
||||||
|
let pixels = surface.take_data().ok()?.to_vec();
|
||||||
|
Some(Raster {
|
||||||
|
key,
|
||||||
|
dark,
|
||||||
|
edge,
|
||||||
|
pixels,
|
||||||
|
labels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Painter<'a> {
|
||||||
|
cr: &'a cairo::Context,
|
||||||
|
key: TileKey,
|
||||||
|
data: TileKey,
|
||||||
|
colors: &'static Palette,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Painter<'_> {
|
||||||
|
fn paint(&self, layers: &[Layer]) {
|
||||||
|
let c = self.colors;
|
||||||
|
let z = self.key.z;
|
||||||
|
set_rgb(self.cr, c.land, 1.0);
|
||||||
|
let _ = self.cr.paint();
|
||||||
|
|
||||||
|
let layer = |name: &str| layers.iter().find(|l| l.name == name);
|
||||||
|
if let Some(layer) = layer("landcover") {
|
||||||
|
self.fill(layer, |class| match class {
|
||||||
|
"wood" | "forest" => Some(c.wood),
|
||||||
|
"grass" | "wetland" => Some(c.grass),
|
||||||
|
"ice" | "glacier" => Some(c.ice),
|
||||||
|
"sand" | "beach" => Some(c.sand),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(layer) = layer("landuse") {
|
||||||
|
self.fill(layer, |class| match class {
|
||||||
|
"residential" | "suburb" | "neighbourhood" => Some(c.residential),
|
||||||
|
"industrial" | "commercial" | "retail" | "railway" => Some(c.industrial),
|
||||||
|
"stadium" | "pitch" | "playground" | "cemetery" => Some(c.grass),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(layer) = layer("park") {
|
||||||
|
self.fill(layer, |_| Some(c.grass));
|
||||||
|
}
|
||||||
|
if let Some(layer) = layer("water") {
|
||||||
|
self.fill(layer, |_| Some(c.water));
|
||||||
|
}
|
||||||
|
if let Some(layer) = layer("waterway") {
|
||||||
|
self.stroke(layer, |class| {
|
||||||
|
let (base, from) = match class {
|
||||||
|
"river" => (1.2, 8),
|
||||||
|
"canal" => (1.0, 12),
|
||||||
|
"stream" => (0.6, 13),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
(z >= from).then(|| (c.water, grow(base, z), false))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if z >= 13
|
||||||
|
&& let Some(layer) = layer("building")
|
||||||
|
{
|
||||||
|
self.fill(layer, |_| Some(c.building));
|
||||||
|
}
|
||||||
|
if let Some(layer) = layer("transportation") {
|
||||||
|
self.roads(layer);
|
||||||
|
}
|
||||||
|
if let Some(layer) = layer("boundary") {
|
||||||
|
self.borders(layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map tile units of the data tile onto this tile's logical px.
|
||||||
|
fn to_px(&self, layer: &Layer, (u, v): (f64, f64)) -> (f64, f64) {
|
||||||
|
let k = f64::from(1u32 << (self.key.z - self.data.z));
|
||||||
|
let ox = f64::from(self.key.x) - f64::from(self.data.x) * k;
|
||||||
|
let oy = f64::from(self.key.y) - f64::from(self.data.y) * k;
|
||||||
|
(
|
||||||
|
(u / layer.extent * k - ox) * TILE_EDGE,
|
||||||
|
(v / layer.extent * k - oy) * TILE_EDGE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace(&self, layer: &Layer, part: &[(f64, f64)], close: bool) {
|
||||||
|
for (i, &point) in part.iter().enumerate() {
|
||||||
|
let (x, y) = self.to_px(layer, point);
|
||||||
|
if i == 0 {
|
||||||
|
self.cr.move_to(x, y);
|
||||||
|
} else {
|
||||||
|
self.cr.line_to(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if close {
|
||||||
|
self.cr.close_path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill the layer's polygons in the colour `color` picks by class.
|
||||||
|
fn fill(&self, layer: &Layer, color: impl Fn(&str) -> Option<u32>) {
|
||||||
|
self.cr.set_fill_rule(cairo::FillRule::EvenOdd);
|
||||||
|
for feature in &layer.features {
|
||||||
|
if feature.kind != Kind::Polygon {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let class = layer.text(feature, "class").unwrap_or("");
|
||||||
|
let Some(rgb) = color(class) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
self.cr.new_path();
|
||||||
|
for part in &feature.parts {
|
||||||
|
self.trace(layer, part, true);
|
||||||
|
}
|
||||||
|
set_rgb(self.cr, rgb, 1.0);
|
||||||
|
let _ = self.cr.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stroke the layer's lines as `style` says by class: colour, width in
|
||||||
|
/// px, and whether dashed.
|
||||||
|
fn stroke(&self, layer: &Layer, style: impl Fn(&str) -> Option<(u32, f64, bool)>) {
|
||||||
|
self.cr.set_line_cap(cairo::LineCap::Round);
|
||||||
|
self.cr.set_line_join(cairo::LineJoin::Round);
|
||||||
|
for feature in &layer.features {
|
||||||
|
if feature.kind != Kind::Line {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let class = layer.text(feature, "class").unwrap_or("");
|
||||||
|
let Some((rgb, width, dashed)) = style(class) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
self.cr.new_path();
|
||||||
|
for part in &feature.parts {
|
||||||
|
self.trace(layer, part, false);
|
||||||
|
}
|
||||||
|
set_rgb(self.cr, rgb, 1.0);
|
||||||
|
self.cr.set_line_width(width);
|
||||||
|
let dash = [width * 2.0, width * 2.0];
|
||||||
|
self.cr.set_dash(if dashed { &dash } else { &[] }, 0.0);
|
||||||
|
let _ = self.cr.stroke();
|
||||||
|
}
|
||||||
|
self.cr.set_dash(&[], 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Roads, minor first so the major ones cross over them, each on a
|
||||||
|
/// darker casing once they are wide enough for one.
|
||||||
|
fn roads(&self, layer: &Layer) {
|
||||||
|
let c = self.colors;
|
||||||
|
let z = self.key.z;
|
||||||
|
let road = |class: &str| -> Option<(u32, f64, u8)> {
|
||||||
|
let (color, width, rank) = match class {
|
||||||
|
"motorway" if z >= 6 => (c.motorway, grow(2.2, z), 4),
|
||||||
|
"trunk" if z >= 7 => (c.motorway, grow(2.2, z), 4),
|
||||||
|
"primary" if z >= 8 => (c.primary, grow(1.8, z), 3),
|
||||||
|
"secondary" if z >= 10 => (c.road, grow(1.4, z), 2),
|
||||||
|
"tertiary" if z >= 11 => (c.road, grow(1.4, z), 2),
|
||||||
|
"minor" | "busway" if z >= 12 => (c.road, grow(1.0, z), 1),
|
||||||
|
"service" if z >= 14 => (c.road, grow(0.6, z), 0),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((color, width, rank))
|
||||||
|
};
|
||||||
|
for rank in 0..=4u8 {
|
||||||
|
let pick = |class: &str| road(class).filter(|r| r.2 == rank);
|
||||||
|
if z >= 12 {
|
||||||
|
self.stroke(layer, |class| {
|
||||||
|
pick(class).map(|(_, width, _)| (c.casing, width + 1.5, false))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.stroke(layer, |class| {
|
||||||
|
pick(class).map(|(color, width, _)| (color, width, false))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.stroke(layer, |class| match class {
|
||||||
|
"rail" | "transit" if z >= 10 => Some((c.rail, grow(0.7, z).min(2.5), false)),
|
||||||
|
"path" | "track" if z >= 14 => Some((c.path, 0.8, true)),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn borders(&self, layer: &Layer) {
|
||||||
|
let c = self.colors;
|
||||||
|
let z = self.key.z;
|
||||||
|
self.cr.set_line_cap(cairo::LineCap::Round);
|
||||||
|
for feature in &layer.features {
|
||||||
|
if feature.kind != Kind::Line || layer.number(feature, "maritime") == Some(1.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (color, width, dashed) = match layer.number(feature, "admin_level") {
|
||||||
|
Some(level) if level <= 2.0 => {
|
||||||
|
(c.border, (0.9 * 1.12f64.powi(i32::from(z))).min(2.5), false)
|
||||||
|
}
|
||||||
|
Some(level) if level <= 4.0 && z >= 5 => (c.state_border, 0.8, true),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
let dashed = dashed || layer.number(feature, "disputed") == Some(1.0);
|
||||||
|
self.cr.new_path();
|
||||||
|
for part in &feature.parts {
|
||||||
|
self.trace(layer, part, false);
|
||||||
|
}
|
||||||
|
set_rgb(self.cr, color, 1.0);
|
||||||
|
self.cr.set_line_width(width);
|
||||||
|
self.cr
|
||||||
|
.set_dash(if dashed { &[3.0, 2.5] } else { &[] }, 0.0);
|
||||||
|
let _ = self.cr.stroke();
|
||||||
|
}
|
||||||
|
self.cr.set_dash(&[], 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line width that grows with the zoom, as the things it stands for get
|
||||||
|
/// bigger on screen.
|
||||||
|
fn grow(base: f64, z: u8) -> f64 {
|
||||||
|
(base * 1.4f64.powi(i32::from(z) - 10)).clamp(0.5, 24.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The names of towns, countries and seas on data tile `data`, in world
|
||||||
|
/// space. Names in the tile's buffer beyond its edge are left to the
|
||||||
|
/// neighbour they belong to.
|
||||||
|
fn gather_labels(layers: &[Layer], data: TileKey, language: &str) -> Vec<Label> {
|
||||||
|
let n = f64::from(1u32 << data.z);
|
||||||
|
let mut labels = Vec::new();
|
||||||
|
for layer in layers {
|
||||||
|
let water = match layer.name.as_str() {
|
||||||
|
"place" => false,
|
||||||
|
"water_name" => true,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
for feature in &layer.features {
|
||||||
|
if feature.kind != Kind::Point {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let class = if water {
|
||||||
|
LabelClass::Water
|
||||||
|
} else {
|
||||||
|
let Some(class) = layer
|
||||||
|
.text(feature, "class")
|
||||||
|
.and_then(LabelClass::from_place)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
class
|
||||||
|
};
|
||||||
|
let Some(text) = label_name(layer, feature, language) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let rank = layer.number(feature, "rank").unwrap_or(99.0);
|
||||||
|
for &(u, v) in feature.parts.iter().flatten() {
|
||||||
|
if !(0.0..layer.extent).contains(&u) || !(0.0..layer.extent).contains(&v) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
labels.push(Label {
|
||||||
|
x: (f64::from(data.x) + u / layer.extent) / n,
|
||||||
|
y: (f64::from(data.y) + v / layer.extent) / n,
|
||||||
|
text: text.clone(),
|
||||||
|
class,
|
||||||
|
rank,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A feature's name in `language` where the map has it, else in Latin
|
||||||
|
/// letters, else as written locally.
|
||||||
|
fn label_name(layer: &Layer, feature: &mvt::Feature, language: &str) -> Option<String> {
|
||||||
|
[
|
||||||
|
format!("name:{language}"),
|
||||||
|
format!("name_{language}"),
|
||||||
|
"name:latin".to_owned(),
|
||||||
|
"name".to_owned(),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.find_map(|key| layer.text(feature, key).filter(|name| !name.is_empty()))
|
||||||
|
.map(|name| name.replace('\n', " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deep_tiles_draw_from_their_ancestor_at_the_data_zoom() {
|
||||||
|
let key = TileKey {
|
||||||
|
z: 16,
|
||||||
|
x: 34_336,
|
||||||
|
y: 22_437,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
key.data(),
|
||||||
|
TileKey {
|
||||||
|
z: 14,
|
||||||
|
x: 8_584,
|
||||||
|
y: 5_609
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let shallow = TileKey { z: 3, x: 4, y: 2 };
|
||||||
|
assert_eq!(shallow.data(), shallow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_build_comes_from_the_tile_template() {
|
||||||
|
let json = br#"{"tiles":["https://tiles.openfreemap.org/planet/20260913_164504_pt/{z}/{x}/{y}.pbf"]}"#;
|
||||||
|
let template = tile_template(json).unwrap();
|
||||||
|
assert_eq!(build_name(&template).as_deref(), Some("20260913_164504_pt"));
|
||||||
|
assert_eq!(build_name("https://example.org/../{z}/{x}/{y}.pbf"), None);
|
||||||
|
assert_eq!(
|
||||||
|
tile_template(br#"{"tiles":["http://plain.example/{z}"]}"#),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_old_build_is_pruned_and_the_current_one_kept() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("pdfs-map-prune-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(dir.join("old/1/0")).unwrap();
|
||||||
|
std::fs::create_dir_all(dir.join("new/1/0")).unwrap();
|
||||||
|
std::fs::write(dir.join("new/1/0/0.pbf"), b"tile").unwrap();
|
||||||
|
std::fs::write(dir.join("tiles.json"), b"{}").unwrap();
|
||||||
|
prune(&dir, "new");
|
||||||
|
assert!(!dir.join("old").exists());
|
||||||
|
assert!(dir.join("new/1/0/0.pbf").exists());
|
||||||
|
assert!(dir.join("tiles.json").exists());
|
||||||
|
std::fs::remove_dir_all(&dir).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roads_widen_as_the_map_zooms_in() {
|
||||||
|
assert!(grow(1.4, 14) > grow(1.4, 10));
|
||||||
|
assert_eq!(grow(2.2, 30), 24.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ pub(crate) mod devices;
|
|||||||
pub(crate) mod duplicates;
|
pub(crate) mod duplicates;
|
||||||
pub(crate) mod locations;
|
pub(crate) mod locations;
|
||||||
pub(crate) mod login;
|
pub(crate) mod login;
|
||||||
|
pub(crate) mod map_tiles;
|
||||||
|
pub(crate) mod mvt;
|
||||||
pub(crate) mod photo_viewer;
|
pub(crate) mod photo_viewer;
|
||||||
pub(crate) mod photos;
|
pub(crate) mod photos;
|
||||||
pub(crate) mod places;
|
pub(crate) mod places;
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
//! A reader for Mapbox Vector Tiles, the format the online Places map's tiles
|
||||||
|
//! come in: protobuf, one message per tile, holding named layers of point,
|
||||||
|
//! line and polygon features with key/value tags.
|
||||||
|
//!
|
||||||
|
//! Only what drawing needs is read — layer names, tags and geometry — and a
|
||||||
|
//! tile that doesn't parse is `None` rather than an error: the map then shows
|
||||||
|
//! the outline underneath it. The format is at
|
||||||
|
//! <https://github.com/mapbox/vector-tile-spec>.
|
||||||
|
|
||||||
|
/// One layer of a tile, such as `water` or `transportation`.
|
||||||
|
pub(crate) struct Layer {
|
||||||
|
pub(crate) name: String,
|
||||||
|
/// The tile's width in geometry units; 4096 almost everywhere.
|
||||||
|
pub(crate) extent: f64,
|
||||||
|
keys: Vec<String>,
|
||||||
|
values: Vec<Value>,
|
||||||
|
pub(crate) features: Vec<Feature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) enum Value {
|
||||||
|
Text(String),
|
||||||
|
Number(f64),
|
||||||
|
Bool(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum Kind {
|
||||||
|
Point,
|
||||||
|
Line,
|
||||||
|
Polygon,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Feature {
|
||||||
|
pub(crate) kind: Kind,
|
||||||
|
/// Pairs of indices into the layer's keys and values.
|
||||||
|
tags: Vec<(u32, u32)>,
|
||||||
|
/// Points, lines or polygon rings, in tile units. A point feature holds
|
||||||
|
/// one part with all its points.
|
||||||
|
pub(crate) parts: Vec<Vec<(f64, f64)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layer {
|
||||||
|
/// The value `feature` carries for `key`.
|
||||||
|
pub(crate) fn tag(&self, feature: &Feature, key: &str) -> Option<&Value> {
|
||||||
|
feature.tags.iter().find_map(|&(k, v)| {
|
||||||
|
(self.keys.get(k as usize)? == key).then(|| self.values.get(v as usize))?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text(&self, feature: &Feature, key: &str) -> Option<&str> {
|
||||||
|
match self.tag(feature, key)? {
|
||||||
|
Value::Text(text) => Some(text),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn number(&self, feature: &Feature, key: &str) -> Option<f64> {
|
||||||
|
match self.tag(feature, key)? {
|
||||||
|
Value::Number(n) => Some(*n),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every layer of the tile in `data`.
|
||||||
|
pub(crate) fn decode(data: &[u8]) -> Option<Vec<Layer>> {
|
||||||
|
let mut tile = Reader::new(data);
|
||||||
|
let mut layers = Vec::new();
|
||||||
|
while let Some((field, wire)) = tile.field()? {
|
||||||
|
match field {
|
||||||
|
3 => layers.push(decode_layer(tile.bytes()?)?),
|
||||||
|
_ => tile.skip(wire)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(layers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_layer(data: &[u8]) -> Option<Layer> {
|
||||||
|
let mut layer = Layer {
|
||||||
|
name: String::new(),
|
||||||
|
extent: 4096.0,
|
||||||
|
keys: Vec::new(),
|
||||||
|
values: Vec::new(),
|
||||||
|
features: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut reader = Reader::new(data);
|
||||||
|
while let Some((field, wire)) = reader.field()? {
|
||||||
|
match field {
|
||||||
|
1 => layer.name = reader.text()?,
|
||||||
|
2 => {
|
||||||
|
if let Some(feature) = decode_feature(reader.bytes()?) {
|
||||||
|
layer.features.push(feature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => layer.keys.push(reader.text()?),
|
||||||
|
4 => layer.values.push(decode_value(reader.bytes()?)?),
|
||||||
|
5 => layer.extent = reader.varint()? as f64,
|
||||||
|
_ => reader.skip(wire)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_value(data: &[u8]) -> Option<Value> {
|
||||||
|
let mut reader = Reader::new(data);
|
||||||
|
let mut value = Value::Bool(false);
|
||||||
|
while let Some((field, wire)) = reader.field()? {
|
||||||
|
value = match field {
|
||||||
|
1 => Value::Text(reader.text()?),
|
||||||
|
2 => Value::Number(f64::from(f32::from_bits(reader.fixed32()?))),
|
||||||
|
3 => Value::Number(f64::from_bits(reader.fixed64()?)),
|
||||||
|
4 | 5 => Value::Number(reader.varint()? as i64 as f64),
|
||||||
|
6 => Value::Number(zigzag(reader.varint()?) as f64),
|
||||||
|
7 => Value::Bool(reader.varint()? != 0),
|
||||||
|
_ => {
|
||||||
|
reader.skip(wire)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A feature, or `None` for one this map can't draw (unknown geometry type,
|
||||||
|
/// or broken).
|
||||||
|
fn decode_feature(data: &[u8]) -> Option<Feature> {
|
||||||
|
let mut reader = Reader::new(data);
|
||||||
|
let mut kind = None;
|
||||||
|
let mut tags = Vec::new();
|
||||||
|
let mut geometry = Vec::new();
|
||||||
|
while let Some((field, wire)) = reader.field()? {
|
||||||
|
match field {
|
||||||
|
2 => {
|
||||||
|
let packed = reader.packed()?;
|
||||||
|
tags = packed
|
||||||
|
.as_chunks::<2>()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&[k, v]| (k as u32, v as u32))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
kind = match reader.varint()? {
|
||||||
|
1 => Some(Kind::Point),
|
||||||
|
2 => Some(Kind::Line),
|
||||||
|
3 => Some(Kind::Polygon),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 => geometry = reader.packed()?,
|
||||||
|
_ => reader.skip(wire)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let kind = kind?;
|
||||||
|
Some(Feature {
|
||||||
|
kind,
|
||||||
|
tags,
|
||||||
|
parts: decode_geometry(kind, &geometry)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turn the command stream of a feature's geometry into its parts. Positions
|
||||||
|
/// are deltas from the previous one, carried across parts.
|
||||||
|
fn decode_geometry(kind: Kind, commands: &[u64]) -> Option<Vec<Vec<(f64, f64)>>> {
|
||||||
|
let mut parts: Vec<Vec<(f64, f64)>> = Vec::new();
|
||||||
|
let (mut x, mut y) = (0i64, 0i64);
|
||||||
|
let mut i = 0;
|
||||||
|
while i < commands.len() {
|
||||||
|
let command = commands[i] & 7;
|
||||||
|
let count = (commands[i] >> 3) as usize;
|
||||||
|
i += 1;
|
||||||
|
match command {
|
||||||
|
// MoveTo starts a part — except for points, which all share one.
|
||||||
|
1 | 2 => {
|
||||||
|
if i + 2 * count > commands.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for n in 0..count {
|
||||||
|
x += zigzag(commands[i + 2 * n]);
|
||||||
|
y += zigzag(commands[i + 2 * n + 1]);
|
||||||
|
let starts = command == 1 && (kind != Kind::Point || parts.is_empty());
|
||||||
|
if starts {
|
||||||
|
parts.push(Vec::new());
|
||||||
|
}
|
||||||
|
parts.last_mut()?.push((x as f64, y as f64));
|
||||||
|
}
|
||||||
|
i += 2 * count;
|
||||||
|
}
|
||||||
|
// ClosePath: the ring's end joins its start, which drawing does.
|
||||||
|
7 => {}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zigzag(n: u64) -> i64 {
|
||||||
|
((n >> 1) as i64) ^ -((n & 1) as i64)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The protobuf wire format, as far as vector tiles use it.
|
||||||
|
struct Reader<'a> {
|
||||||
|
data: &'a [u8],
|
||||||
|
at: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Reader<'a> {
|
||||||
|
fn new(data: &'a [u8]) -> Self {
|
||||||
|
Self { data, at: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next field's number and wire type; `Some(None)` at the end, `None`
|
||||||
|
/// on a broken message.
|
||||||
|
fn field(&mut self) -> Option<Option<(u64, u8)>> {
|
||||||
|
if self.at >= self.data.len() {
|
||||||
|
return Some(None);
|
||||||
|
}
|
||||||
|
let key = self.varint()?;
|
||||||
|
Some(Some((key >> 3, (key & 7) as u8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn varint(&mut self) -> Option<u64> {
|
||||||
|
let mut value = 0u64;
|
||||||
|
for shift in (0..64).step_by(7) {
|
||||||
|
let byte = *self.data.get(self.at)?;
|
||||||
|
self.at += 1;
|
||||||
|
value |= u64::from(byte & 0x7f) << shift;
|
||||||
|
if byte & 0x80 == 0 {
|
||||||
|
return Some(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take(&mut self, n: usize) -> Option<&'a [u8]> {
|
||||||
|
let end = self.at.checked_add(n)?;
|
||||||
|
let bytes = self.data.get(self.at..end)?;
|
||||||
|
self.at = end;
|
||||||
|
Some(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(&mut self) -> Option<&'a [u8]> {
|
||||||
|
let len = usize::try_from(self.varint()?).ok()?;
|
||||||
|
self.take(len)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text(&mut self) -> Option<String> {
|
||||||
|
Some(String::from_utf8_lossy(self.bytes()?).into_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixed32(&mut self) -> Option<u32> {
|
||||||
|
Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixed64(&mut self) -> Option<u64> {
|
||||||
|
Some(u64::from_le_bytes(self.take(8)?.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packed(&mut self) -> Option<Vec<u64>> {
|
||||||
|
let mut inner = Reader::new(self.bytes()?);
|
||||||
|
let mut values = Vec::new();
|
||||||
|
while inner.at < inner.data.len() {
|
||||||
|
values.push(inner.varint()?);
|
||||||
|
}
|
||||||
|
Some(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skip(&mut self, wire: u8) -> Option<()> {
|
||||||
|
match wire {
|
||||||
|
0 => {
|
||||||
|
self.varint()?;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
self.take(8)?;
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
self.bytes()?;
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
self.take(4)?;
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn varint(mut n: u64, out: &mut Vec<u8>) {
|
||||||
|
while n >= 0x80 {
|
||||||
|
out.push((n as u8) | 0x80);
|
||||||
|
n >>= 7;
|
||||||
|
}
|
||||||
|
out.push(n as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field(number: u64, bytes: &[u8], out: &mut Vec<u8>) {
|
||||||
|
varint(number << 3 | 2, out);
|
||||||
|
varint(bytes.len() as u64, out);
|
||||||
|
out.extend_from_slice(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn packed(values: &[u64]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for &v in values {
|
||||||
|
varint(v, &mut out);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn zz(n: i64) -> u64 {
|
||||||
|
((n << 1) ^ (n >> 63)) as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tile with one `water` layer holding one square tagged class=lake.
|
||||||
|
fn lake_tile() -> Vec<u8> {
|
||||||
|
let mut feature = Vec::new();
|
||||||
|
field(2, &packed(&[0, 0]), &mut feature);
|
||||||
|
varint(3 << 3, &mut feature);
|
||||||
|
varint(3, &mut feature);
|
||||||
|
let square = [
|
||||||
|
1 | 1 << 3,
|
||||||
|
zz(10),
|
||||||
|
zz(10),
|
||||||
|
2 | 3 << 3,
|
||||||
|
zz(20),
|
||||||
|
zz(0),
|
||||||
|
zz(0),
|
||||||
|
zz(20),
|
||||||
|
zz(-20),
|
||||||
|
zz(0),
|
||||||
|
7 | 1 << 3,
|
||||||
|
];
|
||||||
|
field(4, &packed(&square), &mut feature);
|
||||||
|
|
||||||
|
let mut value = Vec::new();
|
||||||
|
field(1, b"lake", &mut value);
|
||||||
|
|
||||||
|
let mut layer = Vec::new();
|
||||||
|
field(1, b"water", &mut layer);
|
||||||
|
field(2, &feature, &mut layer);
|
||||||
|
field(3, b"class", &mut layer);
|
||||||
|
field(4, &value, &mut layer);
|
||||||
|
varint(5 << 3, &mut layer);
|
||||||
|
varint(4096, &mut layer);
|
||||||
|
|
||||||
|
let mut tile = Vec::new();
|
||||||
|
field(3, &layer, &mut tile);
|
||||||
|
tile
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_polygon_decodes_with_its_tags() {
|
||||||
|
let layers = decode(&lake_tile()).unwrap();
|
||||||
|
let [water] = &layers[..] else {
|
||||||
|
panic!("one layer expected");
|
||||||
|
};
|
||||||
|
assert_eq!(water.name, "water");
|
||||||
|
assert_eq!(water.extent, 4096.0);
|
||||||
|
let feature = &water.features[0];
|
||||||
|
assert_eq!(feature.kind, Kind::Polygon);
|
||||||
|
assert_eq!(water.text(feature, "class"), Some("lake"));
|
||||||
|
assert_eq!(water.text(feature, "name"), None);
|
||||||
|
assert_eq!(
|
||||||
|
feature.parts,
|
||||||
|
vec![vec![(10.0, 10.0), (30.0, 10.0), (30.0, 30.0), (10.0, 30.0)]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_cut_off_tile_is_rejected() {
|
||||||
|
let tile = lake_tile();
|
||||||
|
assert!(decode(&tile[..tile.len() - 3]).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn points_of_one_feature_share_a_part() {
|
||||||
|
let commands = [1 | 2 << 3, zz(5), zz(5), zz(1), zz(-1)];
|
||||||
|
let parts = decode_geometry(Kind::Point, &commands).unwrap();
|
||||||
|
assert_eq!(parts, vec![vec![(5.0, 5.0), (6.0, 4.0)]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1390,12 +1390,14 @@ pub(crate) fn open_photo_viewer(ui: &Rc<Ui>, initial_uid: String) {
|
|||||||
let toggle_off = info_toggle.clone();
|
let toggle_off = info_toggle.clone();
|
||||||
info_close.connect_clicked(move |_| toggle_off.set_active(false));
|
info_close.connect_clicked(move |_| toggle_off.set_active(false));
|
||||||
|
|
||||||
|
let ui_map = ui.clone();
|
||||||
let viewer_map = viewer.clone();
|
let viewer_map = viewer.clone();
|
||||||
|
let w_map = window.clone();
|
||||||
info_map.connect_clicked(move |_| {
|
info_map.connect_clicked(move |_| {
|
||||||
if let Some((lat, lon)) = *viewer_map.coords.borrow() {
|
let coords = *viewer_map.coords.borrow();
|
||||||
open_path(&format!(
|
if let Some((lat, lon)) = coords {
|
||||||
"https://www.openstreetmap.org/?mlat={lat:.6}&mlon={lon:.6}#map=16/{lat:.6}/{lon:.6}"
|
w_map.close();
|
||||||
));
|
show_on_map(&ui_map, lat, lon);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ use crate::*;
|
|||||||
/// Edge length in px of a place's cover, the same as an album's.
|
/// Edge length in px of a place's cover, the same as an album's.
|
||||||
const COVER_EDGE: i32 = 200;
|
const COVER_EDGE: i32 = 200;
|
||||||
|
|
||||||
|
/// Edge length in px of a photo in the list of photos taken on one spot.
|
||||||
|
const SPOT_EDGE: i32 = 96;
|
||||||
|
|
||||||
/// How often the listing is asked for again while photo locations are read.
|
/// How often the listing is asked for again while photo locations are read.
|
||||||
const MAPPING_POLL: std::time::Duration = std::time::Duration::from_secs(2);
|
const MAPPING_POLL: std::time::Duration = std::time::Duration::from_secs(2);
|
||||||
|
|
||||||
@@ -232,6 +235,8 @@ fn place_card(ui: &Rc<Ui>, place: &PlaceInfo) -> gtk4::Button {
|
|||||||
button.set_tooltip_text(Some(&format!("{}\n{subtitle}", place.name)));
|
button.set_tooltip_text(Some(&format!("{}\n{subtitle}", place.name)));
|
||||||
|
|
||||||
want_cover(ui, place.cover.uid.clone(), &picture);
|
want_cover(ui, place.cover.uid.clone(), &picture);
|
||||||
|
// The map marks the town with the same photo.
|
||||||
|
cover_map_marker(ui, &place.cover.uid, &picture);
|
||||||
|
|
||||||
let ui_open = ui.clone();
|
let ui_open = ui.clone();
|
||||||
let place = place.clone();
|
let place = place.clone();
|
||||||
@@ -239,6 +244,175 @@ fn place_card(ui: &Rc<Ui>, place: &PlaceInfo) -> gtk4::Button {
|
|||||||
button
|
button
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hand the map the photo `picture` shows once it is there.
|
||||||
|
fn cover_map_marker(ui: &Rc<Ui>, uid: &str, picture: >k4::Picture) {
|
||||||
|
let map = Rc::downgrade(&ui.gallery.places_map);
|
||||||
|
let set = move |picture: >k4::Picture, uid: &str| {
|
||||||
|
let texture = picture.paintable().and_downcast::<gtk4::gdk::Texture>();
|
||||||
|
if let (Some(map), Some(texture)) = (map.upgrade(), texture) {
|
||||||
|
map.set_cover(uid, &texture);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
set(picture, uid);
|
||||||
|
let uid = uid.to_owned();
|
||||||
|
picture.connect_paintable_notify(move |picture| set(picture, &uid));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the map the thumbnail of a photo it marks on its own, through the
|
||||||
|
/// gallery's thumbnail pipeline.
|
||||||
|
fn want_map_thumb(ui: &Rc<Ui>, photo: &PhotoItem) {
|
||||||
|
if let Some(texture) = ui.gallery.photo_tex.borrow().get(&photo.uid) {
|
||||||
|
ui.gallery.places_map.set_cover(&photo.uid, texture);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A town's card may be waiting for the same photo: share its picture
|
||||||
|
// rather than take its place in the queue.
|
||||||
|
let waiting = ui.gallery.thumb_wanted.borrow().get(&photo.uid).cloned();
|
||||||
|
let picture = waiting.unwrap_or_else(|| {
|
||||||
|
let picture = gtk4::Picture::new();
|
||||||
|
want_thumb(ui, photo, &picture);
|
||||||
|
picture
|
||||||
|
});
|
||||||
|
cover_map_marker(ui, &photo.uid, &picture);
|
||||||
|
schedule_thumbs(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List the photos taken on one spot of the map in a popover at `(x, y)`;
|
||||||
|
/// clicking one opens it in its town.
|
||||||
|
fn show_spot_photos(ui: &Rc<Ui>, photos: Vec<(PlaceInfo, PhotoItem)>, (x, y): (f64, f64)) {
|
||||||
|
let Some((place, _)) = photos.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let popover = gtk4::Popover::new();
|
||||||
|
popover.set_parent(ui.gallery.places_map.widget());
|
||||||
|
popover.set_pointing_to(Some(>k4::gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
|
||||||
|
|
||||||
|
let content = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
|
||||||
|
content.set_margin_top(6);
|
||||||
|
content.set_margin_bottom(6);
|
||||||
|
content.set_margin_start(6);
|
||||||
|
content.set_margin_end(6);
|
||||||
|
let title = gtk4::Label::builder()
|
||||||
|
.label(&place.name)
|
||||||
|
.halign(gtk4::Align::Start)
|
||||||
|
.build();
|
||||||
|
title.add_css_class("heading");
|
||||||
|
let count = gtk4::Label::builder()
|
||||||
|
.label(ngettext_f(
|
||||||
|
"{n} photo",
|
||||||
|
"{n} photos",
|
||||||
|
photos.len() as u64,
|
||||||
|
&[],
|
||||||
|
))
|
||||||
|
.halign(gtk4::Align::Start)
|
||||||
|
.build();
|
||||||
|
count.add_css_class("dim-label");
|
||||||
|
count.add_css_class("caption");
|
||||||
|
content.append(&title);
|
||||||
|
content.append(&count);
|
||||||
|
|
||||||
|
let grid = gtk4::FlowBox::builder()
|
||||||
|
.selection_mode(gtk4::SelectionMode::None)
|
||||||
|
.homogeneous(true)
|
||||||
|
.min_children_per_line(1)
|
||||||
|
.max_children_per_line(4)
|
||||||
|
.row_spacing(4)
|
||||||
|
.column_spacing(4)
|
||||||
|
.build();
|
||||||
|
for (place, photo) in photos {
|
||||||
|
let picture = gtk4::Picture::builder()
|
||||||
|
.content_fit(gtk4::ContentFit::Cover)
|
||||||
|
.can_shrink(true)
|
||||||
|
.width_request(SPOT_EDGE)
|
||||||
|
.height_request(SPOT_EDGE)
|
||||||
|
.build();
|
||||||
|
spot_thumb(ui, &photo, &picture);
|
||||||
|
let button = gtk4::Button::builder()
|
||||||
|
.child(&picture)
|
||||||
|
.tooltip_text(format_capture_time(photo.capture_time))
|
||||||
|
.build();
|
||||||
|
button.add_css_class("flat");
|
||||||
|
button.add_css_class("places-spot-photo");
|
||||||
|
let ui_open = ui.clone();
|
||||||
|
let popover_open = popover.clone();
|
||||||
|
button.connect_clicked(move |_| {
|
||||||
|
popover_open.popdown();
|
||||||
|
open_place_photo(&ui_open, place.clone(), photo.uid.clone());
|
||||||
|
});
|
||||||
|
grid.append(&button);
|
||||||
|
}
|
||||||
|
schedule_thumbs(ui);
|
||||||
|
let scroller = gtk4::ScrolledWindow::builder()
|
||||||
|
.hscrollbar_policy(gtk4::PolicyType::Never)
|
||||||
|
.propagate_natural_width(true)
|
||||||
|
.propagate_natural_height(true)
|
||||||
|
.max_content_height(360)
|
||||||
|
.child(&grid)
|
||||||
|
.build();
|
||||||
|
content.append(&scroller);
|
||||||
|
popover.set_child(Some(&content));
|
||||||
|
// Gone with its last use; its parent is the map, which stays.
|
||||||
|
popover.connect_closed(|popover| {
|
||||||
|
let popover = popover.clone();
|
||||||
|
glib::idle_add_local_once(move || popover.unparent());
|
||||||
|
});
|
||||||
|
popover.popup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show photo's thumbnail in `picture`, sharing one another widget is
|
||||||
|
/// already waiting for.
|
||||||
|
fn spot_thumb(ui: &Rc<Ui>, photo: &PhotoItem, picture: >k4::Picture) {
|
||||||
|
let waiting = ui.gallery.thumb_wanted.borrow().get(&photo.uid).cloned();
|
||||||
|
match waiting {
|
||||||
|
Some(waiting) => {
|
||||||
|
waiting
|
||||||
|
.bind_property("paintable", picture, "paintable")
|
||||||
|
.sync_create()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
None => want_thumb(ui, photo, picture),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the map the photos of town `id` with where each was taken.
|
||||||
|
fn load_place_locations(ui: &Rc<Ui>, id: u32) {
|
||||||
|
let rx = spawn_request(ui.dirs.control_socket(), Request::PlaceLocations { id });
|
||||||
|
let ui = ui.clone();
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(Ok(Response::PhotoLocations { items })) => {
|
||||||
|
ui.gallery.places_map.set_locations(id, items)
|
||||||
|
}
|
||||||
|
// The map keeps showing the town as one marker.
|
||||||
|
Ok(Ok(Response::Error { message, .. })) => {
|
||||||
|
tracing::debug!("place locations failed: {message}")
|
||||||
|
}
|
||||||
|
Ok(Ok(_)) | Ok(Err(_)) | Err(_) => tracing::debug!("place locations: no reply"),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open `place` and then photo `uid` in it, paging through the town until
|
||||||
|
/// the photo has loaded.
|
||||||
|
fn open_place_photo(ui: &Rc<Ui>, place: PlaceInfo, uid: String) {
|
||||||
|
open_place(ui, place);
|
||||||
|
view_when_loaded(ui, uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_when_loaded(ui: &Rc<Ui>, uid: String) {
|
||||||
|
let ui_page = ui.clone();
|
||||||
|
ui.gallery.page_waiters.borrow_mut().push(Box::new(move || {
|
||||||
|
let ui = ui_page;
|
||||||
|
if find_photo_index(&ui.gallery.model, &uid).is_some() {
|
||||||
|
open_photo_viewer(&ui, uid);
|
||||||
|
} else if ui.gallery.has_more.get() && ui.gallery.place.borrow().is_some() {
|
||||||
|
view_when_loaded(&ui, uid);
|
||||||
|
ui.gallery.burst.set(true);
|
||||||
|
load_gallery(&ui, true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/// Open one place in the gallery: the timeline view, paged from that town.
|
/// Open one place in the gallery: the timeline view, paged from that town.
|
||||||
fn open_place(ui: &Rc<Ui>, place: PlaceInfo) {
|
fn open_place(ui: &Rc<Ui>, place: PlaceInfo) {
|
||||||
ui.gallery.title.set_title(&place.name);
|
ui.gallery.title.set_title(&place.name);
|
||||||
@@ -302,10 +476,31 @@ pub(crate) fn wire_places(ui: &Rc<Ui>) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let map = &ui.gallery.places_map;
|
||||||
let ui_open = ui.clone();
|
let ui_open = ui.clone();
|
||||||
ui.gallery
|
map.connect_open(move |place| open_place(&ui_open, place));
|
||||||
.places_map
|
let ui_photo = ui.clone();
|
||||||
.connect_open(move |place| open_place(&ui_open, place));
|
map.connect_open_photo(move |place, uid| open_place_photo(&ui_photo, place, uid));
|
||||||
|
let ui_locations = ui.clone();
|
||||||
|
map.connect_want_locations(move |id| load_place_locations(&ui_locations, id));
|
||||||
|
let ui_thumb = ui.clone();
|
||||||
|
map.connect_want_thumb(move |photo| want_map_thumb(&ui_thumb, &photo));
|
||||||
|
let ui_spot = ui.clone();
|
||||||
|
map.connect_open_spot(move |photos, at| show_spot_photos(&ui_spot, photos, at));
|
||||||
|
map.set_online(ui.dirs.load_config().online_map, ui.dirs.cache_dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch to the Places map and zoom in on `(latitude, longitude)`, where a
|
||||||
|
/// photo in the viewer was taken.
|
||||||
|
pub(crate) fn show_on_map(ui: &Rc<Ui>, latitude: f64, longitude: f64) {
|
||||||
|
ui.gallery.places_map_btn.set_active(true);
|
||||||
|
if ui.gallery.places_btn.is_active() {
|
||||||
|
// Already in Places, perhaps inside a town: back out to the listing.
|
||||||
|
show_places(ui);
|
||||||
|
} else {
|
||||||
|
ui.gallery.places_btn.set_active(true);
|
||||||
|
}
|
||||||
|
ui.gallery.places_map.show_spot(latitude, longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the places as cards or on the map, as the Grid/Map toggle says.
|
/// Show the places as cards or on the map, as the Grid/Map toggle says.
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -44,8 +44,10 @@ pub(crate) struct StatusState {
|
|||||||
pub(crate) download_limit_row: adw::SpinRow,
|
pub(crate) download_limit_row: adw::SpinRow,
|
||||||
/// Shows where the primary mount lives; its Change button picks a new one.
|
/// Shows where the primary mount lives; its Change button picks a new one.
|
||||||
pub(crate) mountpoint_row: adw::ActionRow,
|
pub(crate) mountpoint_row: adw::ActionRow,
|
||||||
/// "Proton purple accent" toggle. Guarded by [`Self::settings_suppress`].
|
/// "Proton theme" toggle. Guarded by [`Self::settings_suppress`].
|
||||||
pub(crate) accent_row: adw::SwitchRow,
|
pub(crate) theme_row: adw::SwitchRow,
|
||||||
|
/// Street map on the Places map. Guarded by [`Self::settings_suppress`].
|
||||||
|
pub(crate) online_map_row: adw::SwitchRow,
|
||||||
/// "Show tray icon" toggle. Guarded by [`Self::settings_suppress`].
|
/// "Show tray icon" toggle. Guarded by [`Self::settings_suppress`].
|
||||||
pub(crate) tray_row: adw::SwitchRow,
|
pub(crate) tray_row: adw::SwitchRow,
|
||||||
/// Language picker: "System Default", then [`i18n::LANGUAGES`]. Guarded by
|
/// Language picker: "System Default", then [`i18n::LANGUAGES`]. Guarded by
|
||||||
@@ -139,7 +141,8 @@ pub(crate) struct MainWidgets {
|
|||||||
/// Shows the active mountpoint; its suffix button picks a new one.
|
/// Shows the active mountpoint; its suffix button picks a new one.
|
||||||
pub(crate) mountpoint_row: adw::ActionRow,
|
pub(crate) mountpoint_row: adw::ActionRow,
|
||||||
pub(crate) mountpoint_button: gtk4::Button,
|
pub(crate) mountpoint_button: gtk4::Button,
|
||||||
pub(crate) accent_row: adw::SwitchRow,
|
pub(crate) theme_row: adw::SwitchRow,
|
||||||
|
pub(crate) online_map_row: adw::SwitchRow,
|
||||||
pub(crate) tray_row: adw::SwitchRow,
|
pub(crate) tray_row: adw::SwitchRow,
|
||||||
pub(crate) language_row: adw::ComboRow,
|
pub(crate) language_row: adw::ComboRow,
|
||||||
}
|
}
|
||||||
@@ -207,13 +210,20 @@ pub(crate) fn build_main_page() -> MainWidgets {
|
|||||||
let appearance_group = adw::PreferencesGroup::builder()
|
let appearance_group = adw::PreferencesGroup::builder()
|
||||||
.title(gettext("Appearance"))
|
.title(gettext("Appearance"))
|
||||||
.build();
|
.build();
|
||||||
let accent_row = adw::SwitchRow::builder()
|
let theme_row = adw::SwitchRow::builder()
|
||||||
.title(gettext("Proton purple accent"))
|
.title(gettext("Proton theme"))
|
||||||
.subtitle(gettext(
|
.subtitle(gettext(
|
||||||
"Use the Proton brand color instead of the system accent color",
|
"Use Proton's colors instead of the system theme and accent color",
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
appearance_group.add(&accent_row);
|
appearance_group.add(&theme_row);
|
||||||
|
let online_map_row = adw::SwitchRow::builder()
|
||||||
|
.title(gettext("Street map for Places"))
|
||||||
|
.subtitle(gettext(
|
||||||
|
"Show streets and town names from OpenFreeMap. It learns which parts of the world you look at.",
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
appearance_group.add(&online_map_row);
|
||||||
let tray_row = adw::SwitchRow::builder()
|
let tray_row = adw::SwitchRow::builder()
|
||||||
.title(gettext("Show tray icon"))
|
.title(gettext("Show tray icon"))
|
||||||
.subtitle(gettext("Sync status and quick actions in the panel"))
|
.subtitle(gettext("Sync status and quick actions in the panel"))
|
||||||
@@ -395,7 +405,8 @@ pub(crate) fn build_main_page() -> MainWidgets {
|
|||||||
purge_button,
|
purge_button,
|
||||||
mountpoint_row,
|
mountpoint_row,
|
||||||
mountpoint_button,
|
mountpoint_button,
|
||||||
accent_row,
|
theme_row,
|
||||||
|
online_map_row,
|
||||||
tray_row,
|
tray_row,
|
||||||
language_row,
|
language_row,
|
||||||
}
|
}
|
||||||
@@ -455,7 +466,7 @@ pub(crate) const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
|
|||||||
pub(crate) const MIB: f64 = 1024.0 * 1024.0;
|
pub(crate) const MIB: f64 = 1024.0 * 1024.0;
|
||||||
|
|
||||||
/// Wire the Preferences controls: the cache-budget editor, the purge button, the
|
/// Wire the Preferences controls: the cache-budget editor, the purge button, the
|
||||||
/// start-on-login switch, the mountpoint chooser and the accent toggle. Initial widget state is
|
/// start-on-login switch, the mountpoint chooser and the theme toggle. Initial widget state is
|
||||||
/// read once from config / systemd here (the refresh loop owns only the live
|
/// read once from config / systemd here (the refresh loop owns only the live
|
||||||
/// mount + cache-usage read-out), with [`Ui::settings_suppress`] set around the
|
/// mount + cache-usage read-out), with [`Ui::settings_suppress`] set around the
|
||||||
/// programmatic populate so the change handlers don't fire on it.
|
/// programmatic populate so the change handlers don't fire on it.
|
||||||
@@ -482,8 +493,9 @@ pub(crate) fn wire_settings(
|
|||||||
.set_subtitle(&ui.dirs.resolved_mountpoint(&config).display().to_string());
|
.set_subtitle(&ui.dirs.resolved_mountpoint(&config).display().to_string());
|
||||||
ui.status.autostart_row.set_active(service::is_enabled());
|
ui.status.autostart_row.set_active(service::is_enabled());
|
||||||
ui.status
|
ui.status
|
||||||
.accent_row
|
.theme_row
|
||||||
.set_active(config.proton_accent.unwrap_or(false));
|
.set_active(config.proton_theme.unwrap_or(false));
|
||||||
|
ui.status.online_map_row.set_active(config.online_map);
|
||||||
ui.status.tray_row.set_active(!config.tray_hidden);
|
ui.status.tray_row.set_active(!config.tray_hidden);
|
||||||
ui.status
|
ui.status
|
||||||
.language_row
|
.language_row
|
||||||
@@ -585,20 +597,45 @@ pub(crate) fn wire_settings(
|
|||||||
let ui_mp = ui.clone();
|
let ui_mp = ui.clone();
|
||||||
mountpoint_button.connect_clicked(move |_| prompt_mountpoint(&ui_mp));
|
mountpoint_button.connect_clicked(move |_| prompt_mountpoint(&ui_mp));
|
||||||
|
|
||||||
// Accent: saved with the rest of the config, applied right away.
|
// Theme: saved with the rest of the config, applied right away.
|
||||||
let ui_accent = ui.clone();
|
let ui_theme = ui.clone();
|
||||||
ui.status.accent_row.connect_active_notify(move |row| {
|
ui.status.theme_row.connect_active_notify(move |row| {
|
||||||
if ui_accent.status.settings_suppress.get() {
|
if ui_theme.status.settings_suppress.get() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let on = row.is_active();
|
let on = row.is_active();
|
||||||
set_proton_accent(on);
|
set_proton_theme(on);
|
||||||
let mut config = ui_accent.dirs.load_config();
|
// Folders swap their icon with the theme; have every row bound again.
|
||||||
config.proton_accent = Some(on);
|
let rows = ui_theme.browser.model.n_items();
|
||||||
if let Err(e) = ui_accent.dirs.save_config(&config) {
|
ui_theme.browser.model.items_changed(0, rows, rows);
|
||||||
|
let mut config = ui_theme.dirs.load_config();
|
||||||
|
config.proton_theme = Some(on);
|
||||||
|
if let Err(e) = ui_theme.dirs.save_config(&config) {
|
||||||
toast_error(
|
toast_error(
|
||||||
&ui_accent,
|
&ui_theme,
|
||||||
&gettext("Couldn't save the accent color"),
|
&gettext("Couldn't save the theme"),
|
||||||
|
&e.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Street map: saved, and the Places map switches over right away.
|
||||||
|
let ui_map = ui.clone();
|
||||||
|
ui.status.online_map_row.connect_active_notify(move |row| {
|
||||||
|
if ui_map.status.settings_suppress.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let on = row.is_active();
|
||||||
|
ui_map
|
||||||
|
.gallery
|
||||||
|
.places_map
|
||||||
|
.set_online(on, ui_map.dirs.cache_dir());
|
||||||
|
let mut config = ui_map.dirs.load_config();
|
||||||
|
config.online_map = on;
|
||||||
|
if let Err(e) = ui_map.dirs.save_config(&config) {
|
||||||
|
toast_error(
|
||||||
|
&ui_map,
|
||||||
|
&gettext("Couldn't save the map setting"),
|
||||||
&e.to_string(),
|
&e.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
|
|||||||
pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) {
|
pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) {
|
||||||
ui.details.details_suppress.set(true);
|
ui.details.details_suppress.set(true);
|
||||||
let d = &ui.details.details;
|
let d = &ui.details.details;
|
||||||
d.icon.set_icon_name(Some(icon_base_for(entry)));
|
d.icon.set_icon_name(Some(icon_for(entry)));
|
||||||
d.name.set_label(&entry.name);
|
d.name.set_label(&entry.name);
|
||||||
d.kind.set_label(&if entry.is_dir {
|
d.kind.set_label(&if entry.is_dir {
|
||||||
gettext("Folder")
|
gettext("Folder")
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ pub(crate) fn bind_file_thumbnail(
|
|||||||
let fallback_name = if symbolic_fallback {
|
let fallback_name = if symbolic_fallback {
|
||||||
format!("{}-symbolic", icon_base_for(entry))
|
format!("{}-symbolic", icon_base_for(entry))
|
||||||
} else {
|
} else {
|
||||||
icon_base_for(entry).to_string()
|
icon_for(entry).to_string()
|
||||||
};
|
};
|
||||||
fallback.set_icon_name(Some(&fallback_name));
|
fallback.set_icon_name(Some(&fallback_name));
|
||||||
fallback.set_visible(true);
|
fallback.set_visible(true);
|
||||||
|
|||||||
+18
-2
@@ -20,8 +20,24 @@ far it has got.
|
|||||||
### Added
|
### Added
|
||||||
- **Places.** A third Photos view groups photos by the town they were taken in, as cards or on a
|
- **Places.** A third Photos view groups photos by the town they were taken in, as cards or on a
|
||||||
world map. Towns are looked up offline in bundled GeoNames data (CC BY 4.0), and the map is
|
world map. Towns are looked up offline in bundled GeoNames data (CC BY 4.0), and the map is
|
||||||
drawn offline from Natural Earth land outlines (public domain), so no map tiles are fetched.
|
drawn offline from Natural Earth land outlines and country borders (public domain), with
|
||||||
Install `iso-codes`, a new optional dependency, for country names in your language.
|
sea and land in their own colours. Install `iso-codes`, a new optional dependency, for
|
||||||
|
country names in your language.
|
||||||
|
- **Photos on the map.** Every town is marked with one of its photos and a count, and nearby
|
||||||
|
towns merge into one marker until you zoom in. Zoomed in on a town, each photo gets its own
|
||||||
|
marker where it was taken; clicking one opens it in the viewer, and clicking photos taken
|
||||||
|
on one spot lists them all. "Show on map" in the photo viewer now goes to that spot on the
|
||||||
|
Places map instead of a web browser.
|
||||||
|
- **Street map for Places.** An opt-in setting under Appearance draws the map from
|
||||||
|
[OpenFreeMap](https://openfreemap.org/) (OpenStreetMap data), with streets, water, parks and
|
||||||
|
town names in your language. It is off by default because the tile server learns which parts
|
||||||
|
of the world you look at. Tiles are cached under the cache directory (at most 256 MB), and
|
||||||
|
the built-in outline shows wherever they haven't loaded.
|
||||||
|
- **Proton theme.** The "Proton purple accent" preference is now a whole Proton theme: the
|
||||||
|
Proton web apps' light and dark colours, not just the accent. It also holds when `gtk.css`
|
||||||
|
imports another GTK theme (Catppuccin, Fluent, ...), whose own colours used to show through
|
||||||
|
almost everywhere. Folders get Proton's purple folder icon rather than the icon theme's.
|
||||||
|
Anyone who had the accent turned on gets the theme.
|
||||||
- **On this day.** A strip at the top of the timeline shows a card for every earlier year with
|
- **On this day.** A strip at the top of the timeline shows a card for every earlier year with
|
||||||
photos taken on today's date.
|
photos taken on today's date.
|
||||||
- **Find duplicates.** Photos can list every set of byte-identical copies and move the copies
|
- **Find duplicates.** Photos can list every set of byte-identical copies and move the copies
|
||||||
|
|||||||
+265
-215
File diff suppressed because it is too large.
Load diff
+246
-212
File diff suppressed because it is too large.
Load diff
+265
-215
File diff suppressed because it is too large.
Load diff
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Regenerate crates/pdfs-gui/data/land.txt from Natural Earth's land polygons.
|
"""Regenerate crates/pdfs-gui/data/land.txt from Natural Earth's land polygons,
|
||||||
|
and crates/pdfs-gui/data/borders.txt from its country borders.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
curl -LO https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_land.geojson
|
curl -LO https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_land.geojson
|
||||||
scripts/natural-earth-land.py ne_50m_land.geojson crates/pdfs-gui/data/land.txt
|
scripts/natural-earth-land.py ne_50m_land.geojson crates/pdfs-gui/data/land.txt
|
||||||
|
curl -LO https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_50m_admin_0_boundary_lines_land.geojson
|
||||||
|
scripts/natural-earth-land.py ne_50m_admin_0_boundary_lines_land.geojson crates/pdfs-gui/data/borders.txt
|
||||||
|
|
||||||
Natural Earth is in the public domain; the About dialog credits it anyway.
|
Natural Earth is in the public domain; the About dialog credits it anyway.
|
||||||
|
|
||||||
@@ -12,7 +15,8 @@ pairs rounded to two decimals (about a kilometre). Each ring is simplified
|
|||||||
with Ramer-Douglas-Peucker to TOLERANCE degrees, which is finer than the map
|
with Ramer-Douglas-Peucker to TOLERANCE degrees, which is finer than the map
|
||||||
can show at its closest zoom, and rings left with fewer than four points
|
can show at its closest zoom, and rings left with fewer than four points
|
||||||
(islets smaller than the tolerance) are dropped. Holes (lakes) are dropped
|
(islets smaller than the tolerance) are dropped. Holes (lakes) are dropped
|
||||||
too: the map fills the land and draws nothing on top of it.
|
too: the map fills the land and draws nothing on top of it. Borders are lines
|
||||||
|
rather than rings, simplified the same way; a line needs two points.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -54,16 +58,27 @@ def main(source, target):
|
|||||||
lines = []
|
lines = []
|
||||||
for feature in features:
|
for feature in features:
|
||||||
geometry = feature["geometry"]
|
geometry = feature["geometry"]
|
||||||
polygons = (
|
kind = geometry["type"]
|
||||||
|
if kind in ("LineString", "MultiLineString"):
|
||||||
|
paths = (
|
||||||
geometry["coordinates"]
|
geometry["coordinates"]
|
||||||
if geometry["type"] == "MultiPolygon"
|
if kind == "MultiLineString"
|
||||||
else [geometry["coordinates"]]
|
else [geometry["coordinates"]]
|
||||||
)
|
)
|
||||||
for polygon in polygons:
|
shortest = 2
|
||||||
ring = simplify(polygon[0])
|
else:
|
||||||
if len(ring) < 4:
|
polygons = (
|
||||||
|
geometry["coordinates"]
|
||||||
|
if kind == "MultiPolygon"
|
||||||
|
else [geometry["coordinates"]]
|
||||||
|
)
|
||||||
|
paths = [polygon[0] for polygon in polygons]
|
||||||
|
shortest = 4
|
||||||
|
for path in paths:
|
||||||
|
path = simplify(path)
|
||||||
|
if len(path) < shortest:
|
||||||
continue
|
continue
|
||||||
lines.append(" ".join(f"{lon:.2f},{lat:.2f}" for lon, lat in ring))
|
lines.append(" ".join(f"{lon:.2f},{lat:.2f}" for lon, lat in path))
|
||||||
with open(target, "w") as out:
|
with open(target, "w") as out:
|
||||||
out.write("\n".join(lines) + "\n")
|
out.write("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user