Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ hmac = "0.12"
sha2 = "0.10"
quick-xml = { version = "0.36", features = ["serialize"] }
unicode-width = "0.2"
# Map tiles: the same `image` GPUI decodes with, only the formats tile
# servers send.
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }

[dev-dependencies]
rust_xlsxwriter = "0.99"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ducklocal warehouse.duckdb # or an existing DuckDB database

Each CSV, TSV, Parquet, JSON, or Excel file becomes queryable as the window opens. You can also drag files or folders onto the window, or pick them from the file dialog — and they stay registered, so the next launch starts with the same workspace. One open request attaches at most 256 files.

Your data stays on your machine: nothing is uploaded, and there is no account.
Your data stays on your machine: nothing is uploaded, and there is no account. Map charts can draw an OpenStreetMap base map under their points, off until you turn it on (View → Online base map): its tile requests reveal the area being viewed, never your rows.

## Features

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ducklocal warehouse.duckdb # 或已有的 DuckDB 数据库

每个 CSV / TSV / Parquet / JSON / Excel 文件在窗口打开时即可查询。也可以把文件或文件夹拖进窗口,或用文件对话框选择;它们会被记住,下次启动直接回到同一个工作区。一次打开请求最多挂载 256 个文件。

数据只留在这台机器上:不上传、不需要账号。
数据只留在这台机器上:不上传、不需要账号。地图图表可以在点下方绘制 OpenStreetMap 底图,默认关闭,需在「视图 → 在线底图」中开启:瓦片请求会暴露正在查看的区域,但绝不包含你的数据行。

## 功能

Expand Down
5 changes: 5 additions & 0 deletions src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,11 @@ static STRINGS: &[(&str, &str, &str)] = &[
("menu.zoom_in", "放大界面", "Larger interface"),
("menu.zoom_out", "缩小界面", "Smaller interface"),
("menu.zoom_reset", "默认大小", "Default size"),
(
"menu.base_map",
"在线底图(OpenStreetMap)",
"Online base map (OpenStreetMap)",
),
("workspace.stop.tooltip", "中断正在运行的查询", "Interrupt the running query"),
("workspace.format", "格式化", "Format"),
("workspace.format.tooltip", "格式化当前 SQL", "Format current SQL"),
Expand Down
82 changes: 72 additions & 10 deletions src/ui/geo.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//! Map mode of the chart tab: a result with a latitude and a longitude column
//! plots each row as a point on a Web Mercator projection, fitted to the
//! points' extent, over a graticule labelled in degrees.
//! points' extent, over a graticule labelled in degrees — and OpenStreetMap
//! tiles, once someone turns them on (`tiles`).
//!
//! There is no base map: tiles would need the network, and an embedded
//! Without tiles the points draw the shape on their own; an embedded
//! coastline would say nothing at the city scale most station, store or
//! sensor tables live at. The points themselves draw the shape. When a low
//! sensor tables live at. When a low
//! cardinality text column is present (a `type`, a `country`), points are
//! colored by it, so the map says something beyond "where".
//!
Expand Down Expand Up @@ -433,6 +434,16 @@ fn fold_category(column: usize, sorted: Vec<(String, usize)>) -> Category {
}
}

/// A latitude's projected ordinate, for the tile layer's tests.
#[cfg(test)]
pub(crate) fn project_lat(lat: f64) -> f64 {
mercator_y(lat)
}

/// Most tiles a map asks for at once; a bigger plot drops a zoom level
/// rather than filling the policy's queue with one view.
const MAX_TILES: usize = 48;

fn mercator_y(lat: f64) -> f64 {
let phi = lat.clamp(-MAX_MERCATOR_LAT, MAX_MERCATOR_LAT).to_radians();
(FRAC_PI_4 + phi / 2.).tan().ln().to_degrees()
Expand Down Expand Up @@ -579,6 +590,8 @@ impl Plot for GeoPlot {
theme.background,
);

let dark = theme.mode.is_dark();

// The plot area reads as a map sheet: a faint fill and a frame.
window.paint_quad(quad(
area,
Expand All @@ -589,8 +602,40 @@ impl Plot for GeoPlot {
BorderStyle::default(),
));

// The base map, when tiles are on and have arrived. Whatever has not
// arrived yet leaves the sheet showing through.
let base_map = crate::ui::tiles::enabled(cx);
let mut drew_tiles = false;
if base_map {
let mut z = crate::ui::tiles::zoom_for(view.scale);
let mut wanted = crate::ui::tiles::covering(view.visible(), z);
while wanted.len() > MAX_TILES && z > 0 {
z -= 1;
wanted = crate::ui::tiles::covering(view.visible(), z);
}
for (key, image) in crate::ui::tiles::visible(&wanted, window, cx) {
let (x0, x1, y0, y1) = crate::ui::tiles::extent_of(key);
let (left, top) = view.project(x0, y1);
let (right, bottom) = view.project(x1, y0);
let tile = Bounds::from_corners(
bounds.origin + point(px(left), px(top)),
bounds.origin + point(px(right), px(bottom)),
);
drew_tiles |= window
.paint_image(area, tile, Corners::all(px(4.)), image, 0, false)
.is_ok();
}
// Tiles are drawn for daylight; in the dark theme a veil of the
// background keeps them from glaring behind the points.
if drew_tiles && dark {
window.paint_quad(fill(area, background.opacity(0.35)).corner_radii(px(4.)));
}
}
let theme = cx.theme();

// Graticule: meridians at round longitudes, parallels at round
// latitudes (unevenly spaced, as Mercator spaces them).
// latitudes (unevenly spaced, as Mercator spaces them). Over a base
// map the streets are the reference; only the labels stay.
let (vx0, vx1, vy0, vy1) = view.visible();
let lng_step = nice_step(vx1 - vx0, view.area.size.width as f64);
let (lat0, lat1) = (inverse_mercator_y(vy0), inverse_mercator_y(vy1));
Expand All @@ -601,12 +646,14 @@ impl Plot for GeoPlot {
let parallels: Vec<(f32, f64)> = steps_within(lat0.max(-85.), lat1.min(85.), lat_step)
.map(|lat| (view.project(0., mercator_y(lat)).1, lat))
.collect();
Grid::new()
.x(meridians.iter().map(|(x, _)| px(x - view.area.origin.x)))
.y(parallels.iter().map(|(y, _)| px(y - view.area.origin.y)))
.stroke(grid)
.dash_array(&[px(4.), px(2.)])
.paint(&area, window);
if !drew_tiles {
Grid::new()
.x(meridians.iter().map(|(x, _)| px(x - view.area.origin.x)))
.y(parallels.iter().map(|(y, _)| px(y - view.area.origin.y)))
.stroke(grid)
.dash_array(&[px(4.), px(2.)])
.paint(&area, window);
}

let mut labels: Vec<Text> = meridians
.iter()
Expand All @@ -627,6 +674,21 @@ impl Plot for GeoPlot {
)
.align(TextAlign::Right)
}));
// The tile policy asks for the credit on the map itself, whenever its
// tiles are shown.
if drew_tiles {
labels.push(
Text::new(
crate::ui::tiles::ATTRIBUTION,
point(
px(view.area.origin.x + view.area.size.width - 6.),
px(view.area.origin.y + view.area.size.height - TEXT_SIZE - 6.),
),
if dark { theme.foreground } else { muted },
)
.align(TextAlign::Right),
);
}
PlotLabel::new(labels).paint(&bounds, window, cx);

// One quad per point, ringed in the background color so overlapping
Expand Down
11 changes: 11 additions & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod scale;
pub mod setup_dialog;
pub mod sidebar;
pub mod status_bar;
pub mod tiles;
pub mod title_bar;
pub mod workspace;

Expand All @@ -33,6 +34,7 @@ gpui_kit::actions!(
CloseTab,
OpenData,
OpenSetup,
ToggleBaseMap,
Quit
]
);
Expand Down Expand Up @@ -72,6 +74,11 @@ pub fn init(cx: &mut App) {
cx.on_action(|_: &ZoomReset, cx| scale::set(scale::UiSize::Default, cx));
cx.on_action(|_: &StopQuery, _| crate::db::interrupt());
cx.on_action(|_: &Quit, cx| cx.quit());
cx.on_action(|_: &ToggleBaseMap, cx| {
tiles::toggle(cx);
// The item's check mark is part of the menu, built once.
set_menus(cx);
});
// After the bindings: the menu bar reads each item's shortcut from the
// keymap at the moment it is built.
set_menus(cx);
Expand All @@ -87,6 +94,7 @@ pub fn set_menus(cx: &mut App) {
items,
disabled: false,
};
let base_map = tiles::enabled(cx);
cx.set_menus(vec![
Menu {
name: "DuckLocal".into(),
Expand Down Expand Up @@ -134,6 +142,9 @@ pub fn set_menus(cx: &mut App) {
MenuItem::action(tr("menu.zoom_in"), ZoomIn),
MenuItem::action(tr("menu.zoom_out"), ZoomOut),
MenuItem::action(tr("menu.zoom_reset"), ZoomReset),
MenuItem::Separator,
MenuItem::action(tr("menu.base_map"), ToggleBaseMap)
.checked(base_map),
],
),
]);
Expand Down
Loading
Loading