diff --git a/README.md b/README.md index 924b51f..747107f 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,25 @@ This repo is for all the supporting code used to find and display the longest line of sight on the planet. -The main viewshed algorithm is another repo https://github.com/tombh/total-viewsheds +The main viewshed algorithm is another repo https://github.com/AllTheLines/CacheTVS -The raw elevation data is, as of writing (October 2025), from https://www.viewfinderpanoramas.org/Coverage%20map%20viewfinderpanoramas_org3.htm Other sources of data are available, notably via AWS's https://registry.opendata.aws/terrain-tiles, but as far as I can tell viewfinderpanoramas.org offers a cleaned version, removing noisy data and filling in voids with other sources. +The raw elevation data is from [www.viewfinderpanoramas.org](https://www.viewfinderpanoramas.org/Coverage%20map%20viewfinderpanoramas_org3.htm). + +There are 7 main parts to this repo: +1. The Packer +2. The Stitcher +3. Calculating Total Viewsheds +4. Preparing Assets For The Cloud +5. Atlas: Automating Steps 2, 3 and 4 +6. Static Site Website +7. Map Web App ## Packer I wrote an in-depth blog post about the Packer https://tombh.co.uk/packing-world-lines-of-sight -![Map of all the longest line of sight tiles in the world](https://alltheviews.world/world_packed.webp) - -This map shows a not-terrible packing of the minimum tiles needed to guarantee searching every line of sight on the planet. +This map shows a not-terrible packing of the minimum tiles needed to guarantee searching every line of sight on the planet: +![Map of all the longest line of sight tiles in the world](https://tombh.co.uk/images/tile-packing-red.webp) To calculate any [viewshed](https://en.wikipedia.org/wiki/Viewshed), and therefore line of sight, you must inevitably provide more data than ends up being used. This is to say that you don't know the limits of what you can see from a given point until you actually calculate it. The only limit you can calculate beforehand is the longest _theoretical_ line of sight based on the highest points within the region you're interested in. @@ -26,7 +34,7 @@ Here are some examples, they are for 2 peaks of the same height and the maximum * 0.5km 160km * 1.65m 9km (height of an average human) -Formula: √(2 * Earth Radius * height) * 2 +Formula: `√(2 * Earth Radius * height) * 2` So as long as you provide the raw data within these theoretical limits then you are at least guaranteed to have complete viewsheds. The worst that can happen is that RAM and CPU cycles are wasted on calculating lines that have already terminated. @@ -133,12 +141,9 @@ bunch of tiles have been processed: * Create the gigantic (10s of GBs) global `.pmtile` that contains the TVS heatmap for the entire planet. ``` -# This requires a machine with a lot of RAM and CPU. As of writing, with a ~10% world run, -# an 80Gb machine with 48 cores, took around 20 minutes. +# This requires a machine with a lot of disk, RAM and CPU. As of writing, I'd recommend +# 3TB disk, >150GB RAM and at least 48 cores. # Replace `latest` with `local` to skip syncing files to S3. -./ctl.sh make_prod_pmtiles latest - -# This is exactly the same but without some more agressive settings. ./ctl.sh make_pmtiles latest work/world.pmtiles ``` @@ -156,8 +161,22 @@ RUST_LOG=off,tasks=trace cargo run --release --bin tasks -- \ --tiffs work/longest_lines ``` -## Website +## Static Site Website + +Uses [Hugo](https://gohugo.io). Run development server with `hugo server --buildDrafts` in `ssg/` direectory. + +Live at [alltheviews.world](https://alltheviews.world) + +Automatically deployed by pushing to either `staging` or `main` branches. + +Manually deploy to production from current checked out code: `.ctl.sh static_site_deploy` + +## Web App + +Run development server with `pnpm run dev` in `website/` direectory. + +Live at [map.alltheviews.world](https://map.alltheviews.world) -https://alltheviews.world Still in development so expect daily breakages. +Automatically deployed by pushing to either `staging` or `main` branches. -`.ctl.sh website_deploy` +Manually deploy to production from current checked out code: `.ctl.sh webapp_deploy` diff --git a/scripts/website.bash b/scripts/website.bash deleted file mode 100644 index 0417ae9..0000000 --- a/scripts/website.bash +++ /dev/null @@ -1,6 +0,0 @@ -function website_deploy { - _pushd "$PROJECT_ROOT"/website - pnpm run build - npx wrangler deploy - _popd -} diff --git a/scripts/websites.bash b/scripts/websites.bash new file mode 100644 index 0000000..9fff828 --- /dev/null +++ b/scripts/websites.bash @@ -0,0 +1,13 @@ +function webapp_deploy { + _pushd "$PROJECT_ROOT"/website + pnpm run build + npx wrangler deploy + _popd +} + +function static_site_deploy { + _pushd "$PROJECT_ROOT"/ssg + hugo + npx wrangler deploy + _popd +} diff --git a/ssg/content/_index.md b/ssg/content/_index.md index 77dfce1..410bc6e 100644 --- a/ssg/content/_index.md +++ b/ssg/content/_index.md @@ -4,7 +4,7 @@ With the help of a custom-developed algorithm, [CacheTVS](https://github.com/All checked _every single_ view on Earth in search of the coveted **longest line of sight on the planet**. Based on the method we detail [here](/faq/), we present the greatest view of all: -## [1. Hindu Kush to Pik Dankova (530km) ↗](https://map.alltheviews.world/longest/78.76539611816406-36.31400680541992) +## [1. Hindu Kush to Pik Dankova (530km) ↗](https://map.alltheviews.world/longest/78.76539611816406_36.31400680541992) ![Longest Line Of Site](/longest_line_1.webp 'The longest line of sight on the planet, at 530km, from the Hindu Kush to Pik Dankova') @@ -12,13 +12,13 @@ Based on the method we detail [here](/faq/), we present the greatest view of all Longest lines of sight tend to group together around peaks and ridges. So the following are more of our own curated list rather than the technically correct runners up. We chose them based on being in notably different regions of the world. -## [2. Telkik Shan to Kongur Tagh (507km) ↗](https://map.alltheviews.world/longest/80.29535675048828-36.52555465698242) +## [2. Telkik Shan to Kongur Tagh (507km) ↗](https://map.alltheviews.world/longest/80.29535675048828_36.52555465698242) ![Second Longest Line Of Site](/longest_line_2.webp 'The second longest line of sight, at 507km, between the Telkik Shan and Kongur Tah') Another long line of sight in the Himalays, over the Tarim Basin and Tibetan Plateau. With such tall mountain ranges on either side, they're everywhere! -## [3. Antioquia to Pico Cristobal (504km) ↗](https://map.alltheviews.world/longest/-75.72223663330078-6.75514030456543) +## [3. Antioquia to Pico Cristobal (504km) ↗](https://map.alltheviews.world/longest/-75.72223663330078_6.75514030456543) ![Third Longest Line Of Site](/longest_line_3.webp 'The third longest line of sight, at 504km, from Antioquia to Pico Crostobal in Colombia') Now we go right to the other side of the world to Colombia in South America. We've found a line of sight from the department of Antioquia to Pico Cristobal, Colombia's highest mountain. diff --git a/ssg/content/about.md b/ssg/content/about.md index 13595b6..601ed3c 100644 --- a/ssg/content/about.md +++ b/ssg/content/about.md @@ -1,17 +1,20 @@ - -# About Us +--- +title: About Us +--- Tom and Ryan are two friends united behind a single problem: finding the longest line of sight. Ryan met Tom through a tech forum, [HackerNews](https://news.ycombonator.com), after reaching out to recruit him for a company opening. They instantly clicked and have kept in touch since. -Recently, Tom started working on an algorithm to find the longest line of sight, +Recently (in July of 2025), Tom started working on an algorithm to find the longest line of sight, and Ryan noticed it through a post on a tech forum explaining the project. For the last 6 months both have spent innumerous hours engineering [a tool](https://github.com/AllTheViews/CacheTVS) for the longest line of sight for every point on the planet, and to let others explore the beautiful world and all its long lines of sight. +You can contact us at hello@alltheviews.world + ## Tom {{
}} When Tom isn't on his laptop he's meditating or getting to know a new country. He's been a digital nomad since 2015. You can find his longest lines blog posts [here](https://tombh.co.uk/packing-world-lines-of-sight) and [here](https://tombh.co.uk/longest-line-of-sight), or see what else he is up to on his website https://tombh.co.uk diff --git a/ssg/content/faq.md b/ssg/content/faq.md index 27ff404..0ad45ad 100644 --- a/ssg/content/faq.md +++ b/ssg/content/faq.md @@ -1,17 +1,33 @@ +--- +title: FAQs +--- -# FAQs +## How Did You Do It? + +Tom and Ryan go into detail on their respective blogs: + +* [tombh.co.uk/packing-world-lines-of-sight](https://tombh.co.uk/packing-world-lines-of-sight) +* [tombh.co.uk/longest-line-of-sight](https://tombh.co.uk/longest-line-of-sight) +* [ryan.berge.rs/posts/lines-of-sight](https://ryan.berge.rs/posts/lines-of-sight) +* [ryan.berge.rs/posts/total-viewshed-algorithm](https://ryan.berge.rs/posts/total-viewshed-algorithm/) ## What Assumptions Did You Make? -* The original source of our data is from [NASA's SRTM survey](https://www.earthdata.nasa.gov/data/instruments/srtm) which is ~100m resolution analysis of the planet's elevation data. It is known to have some issues so we used a clean version kindly provided by [viewfinderpanoramas.org](https://www.viewfinderpanoramas.org/Coverage%20map%20viewfinderpanoramas_org3.htm). +* The original source of our data is from [NASA's SRTM survey](https://www.earthdata.nasa.gov/data/instruments/srtm) which is ~100m resolution analysis of the planet's elevation data. It is known to have some issues so we used a cleaned version kindly provided by [viewfinderpanoramas.org](https://www.viewfinderpanoramas.org/Coverage%20map%20viewfinderpanoramas_org3.htm). * We used a globe earth meaning it is an approximation of earth's shape, as it is an oblate spheroid. * We take refraction into account, and we use what the GIS community has calculated to be the world average, which is a refraction coefficient of `0.13`. +* The height of the observer is `1.65m` or `5'5"`. * Each viewshed is calculated using 360 lines of sight each seperated by 1°. This could potentially miss some longest lines of sight, but it is considered to be the optimal resolution to balance the accumulation of errors and computational costs. For more details, see: Siham Tabik, Antonio R. Cervilla, Emilio Zapata, Luis F. Romero in their 2014 paper _Efficient Data Structure and Highly Scalable Algorithm for Total-Viewshed Computation_ https://ieeexplore.ieee.org/document/6837455 -* All computation is done on [AEQD](https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection) reprojections of the raw data. For the longest lines of sight on the planet, ~500km, the worst case errors caused by this projection can reach ~0.0685%. This error is only relevant to viewsheds at the edge of the computable area of the tile, therefore those viewsheds around 500km from the centre of the tile. - - +* All computation is done on [AEQD](https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection) reprojections of the raw data. For the longest lines of sight on the planet, ~500km, the worst case errors caused by this projection can reach ~0.0685%. This error is only relevant to viewsheds at the edge of the computable area of [tiles](https://tombh.co.uk/packing-world-lines-of-sight), therefore those viewsheds around 500km from the centre of tiles. ## Is The Source Code Available? Yes. [The core algorithm](https://github.com/AllTheLines/CacheTVS). [The pipeline and web app](https://github.com/AllTheLines/viewview). +## Why Don't You Make The Sea More Visible? + +The sea actually has above average visibility, due to it generally not being blocked by hills and mountains. As such it can generate some often interesting and sometimes beautiful heatmaps, like around [the islands of South Korea](https://map.alltheviews.world/longest/126.9141495423105_33.956726689880966). + +## How Do I Contact You? + +💌 hello@alltheviews.world diff --git a/ssg/content/posts/hello-world.md b/ssg/content/news/hello-world.md similarity index 92% rename from ssg/content/posts/hello-world.md rename to ssg/content/news/hello-world.md index e030b20..a8718b9 100644 --- a/ssg/content/posts/hello-world.md +++ b/ssg/content/news/hello-world.md @@ -1,5 +1,5 @@ +++ -date = '2026-02-09T16:35:33+01:00' +date = '2026-02-09T00:00:00+00:00' title = 'Launch day' +++ diff --git a/ssg/hugo.toml b/ssg/hugo.toml index fcebfcf..5d99514 100644 --- a/ssg/hugo.toml +++ b/ssg/hugo.toml @@ -8,4 +8,4 @@ title = 'All The Views' [params] mainSections = ["posts"] - nav = "[Home](/) [News](/posts/) [FAQ](/faq/) [About](/about/) |  [Map ↗](https://map.alltheviews.world)" + nav = "[Home](/) [News](/news/) [FAQ](/faq/) [About](/about/) |  [Map ↗](https://map.alltheviews.world)" diff --git a/ssg/themes/hugo-trainsh/assets/css/style.css b/ssg/themes/hugo-trainsh/assets/css/style.css index 270065b..458a259 100644 --- a/ssg/themes/hugo-trainsh/assets/css/style.css +++ b/ssg/themes/hugo-trainsh/assets/css/style.css @@ -786,7 +786,7 @@ figure figcaption { } .page-title { - font-size: 1.5em; + font-size: 2em; margin: 0; } diff --git a/ssg/themes/hugo-trainsh/layouts/_default/single.html b/ssg/themes/hugo-trainsh/layouts/_default/single.html index 26b7a7d..942f59e 100644 --- a/ssg/themes/hugo-trainsh/layouts/_default/single.html +++ b/ssg/themes/hugo-trainsh/layouts/_default/single.html @@ -3,25 +3,25 @@ {{- $isPost := in $sections .Section -}}
- {{- if $isPost -}} -
-

{{ .Title }}

- -
- {{- else -}} - - {{- end -}} + {{- if $isPost -}} +
+

{{ .Title }}

+ +
+ {{- else -}} + + {{- end -}} -
- {{ .Content }} -
+
+ {{ .Content }} +
- {{ partial "post/footer-meta.html" . }} + {{ partial "post/footer-meta.html" . }}
{{ end }} diff --git a/ssg/themes/hugo-trainsh/layouts/_partials/post/upvote.html b/ssg/themes/hugo-trainsh/layouts/_partials/post/upvote.html deleted file mode 100644 index 88f7216..0000000 --- a/ssg/themes/hugo-trainsh/layouts/_partials/post/upvote.html +++ /dev/null @@ -1,36 +0,0 @@ -{{- $params := site.Params.upvote -}} -{{- if not ($params.enabled | default false) -}} -{{- return -}} -{{- end -}} -{{- $slug := strings.TrimSuffix "/" .RelPermalink -}} -{{- /* Share upvote across translations by using a canonical slug without the current language prefix. */ -}} -{{- $langPrefix := .Site.LanguagePrefix -}} -{{- if and $langPrefix (ne $langPrefix "/") (strings.HasPrefix $slug $langPrefix) -}} - {{- $slug = strings.TrimPrefix $langPrefix $slug -}} -{{- end -}} -{{- if not $slug -}} -{{- return -}} -{{- end -}} -{{- $endpoint := or $params.endpoint "/api/upvote" -}} -{{- $info := or $params.infoEndpoint "/api/upvote-info" -}} -{{- $title := .Title | default "" -}} -{{- $idSafe := strings.Replace (strings.TrimPrefix "/" $slug) "/" "-" -1 -}} -{{- $formId := printf "upvote-form-%s" $idSafe -}} -
-
- - - - - - - - -
-
diff --git a/telkik_shan_to_kongur_tagh.png b/telkik_shan_to_kongur_tagh.png deleted file mode 100644 index 42a9b59..0000000 Binary files a/telkik_shan_to_kongur_tagh.png and /dev/null differ diff --git a/website/src/ClickEffect.svelte b/website/src/ClickEffect.svelte new file mode 100644 index 0000000..bc0a61a --- /dev/null +++ b/website/src/ClickEffect.svelte @@ -0,0 +1,45 @@ + + +
+ + diff --git a/website/src/HeatmapLayer.ts b/website/src/HeatmapLayer.ts index b320f9a..75d6e05 100644 --- a/website/src/HeatmapLayer.ts +++ b/website/src/HeatmapLayer.ts @@ -12,7 +12,6 @@ import { PMTILES_SERVER, packFloatToU8s, tileKey, - WORLD_PMTILES, } from './utils'; import vertex from './vertex.glsl?raw'; import type { WorkerEvent } from './Worker'; @@ -37,7 +36,7 @@ type Uniforms = { uAverageSurfaceVisibility: WebGLUniformLocation | null; }; -type State = +type HeatmapState = | { map: MapLibre; gl: WebGL2RenderingContext; @@ -60,30 +59,26 @@ const AVERAGE_SURFACE_VISIBILITY = 700000.0; let fillerTile: TileGL; -let state: State; +let heatmapState: HeatmapState; function initialise() { - if (state === undefined) { + if (heatmapState === undefined) { return; } const params = new URLSearchParams(self.location.search); let source = params.get('pmtiles'); if (!source) { - if (import.meta.env.DEV) { - source = `/${WORLD_PMTILES}.pmtiles`; - } else { - source = PMTILES_SERVER; - } + source = PMTILES_SERVER; } - state.worker.postMessage({ type: 'init', source }); - state.worker.onmessage = onWorkerMessage; + heatmapState.worker.postMessage({ type: 'init', source }); + heatmapState.worker.onmessage = onWorkerMessage; makeFillerTile(); } function onWorkerMessage(event: MessageEvent) { - if (state === undefined) { + if (heatmapState === undefined) { return; } @@ -94,10 +89,10 @@ function onWorkerMessage(event: MessageEvent) { return; } - state.tileCache.set(key, tile); + heatmapState.tileCache.set(key, tile); // Should these be throttled? - state.map?.redraw(); + heatmapState.map?.redraw(); } } @@ -154,7 +149,7 @@ const HeatmapLayer: CustomLayerInterface = { ), }; - state = { + heatmapState = { map, gl, program, @@ -169,39 +164,39 @@ const HeatmapLayer: CustomLayerInterface = { }, prerender() { - if (state === undefined) { + if (heatmapState === undefined) { return; } - if (Date.now() - state.lastGC < 60 * 1000) { + if (Date.now() - heatmapState.lastGC < 60 * 1000) { return; } - const mapBounds = state.map.getBounds(); + const mapBounds = heatmapState.map.getBounds(); - for (const [key, tile] of state.tileCache.entries()) { + for (const [key, tile] of heatmapState.tileCache.entries()) { if (isTileIntersectingBounds(tile.bounds, mapBounds)) { } else { - state.tileCache.delete(key); + heatmapState.tileCache.delete(key); } } - state.lastGC = Date.now(); + heatmapState.lastGC = Date.now(); }, async render(gl, matrix) { let max = 0.0; - if (state === undefined) { + if (heatmapState === undefined) { return; } let isSomethingToRender = false; - for (const tile of state.map.coveringTiles({ tileSize: 256 })) { + for (const tile of heatmapState.map.coveringTiles({ tileSize: 256 })) { const key = tileKey(tile.canonical.z, tile.canonical.x, tile.canonical.y); - let cachedTile = state.tileCache.get(key); + let cachedTile = heatmapState.tileCache.get(key); if (!cachedTile) { - state.worker.postMessage({ + heatmapState.worker.postMessage({ type: 'getTile', z: tile.canonical.z, x: tile.canonical.x, @@ -219,7 +214,7 @@ const HeatmapLayer: CustomLayerInterface = { continue; } const parentKey = tileKey(parent.z, parent.x, parent.y); - cachedTile = state.tileCache.get(parentKey); + cachedTile = heatmapState.tileCache.get(parentKey); if (cachedTile) { break; @@ -244,19 +239,22 @@ const HeatmapLayer: CustomLayerInterface = { return; } - gl.useProgram(state.program); - gl.bindBuffer(gl.ARRAY_BUFFER, state.vertexBuffer); - const positionLocation = gl.getAttribLocation(state.program, 'a_pos'); + gl.useProgram(heatmapState.program); + gl.bindBuffer(gl.ARRAY_BUFFER, heatmapState.vertexBuffer); + const positionLocation = gl.getAttribLocation( + heatmapState.program, + 'a_pos', + ); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); - for (const tile of state.map.coveringTiles({ tileSize: 256 })) { + for (const tile of heatmapState.map.coveringTiles({ tileSize: 256 })) { let scaleIfParent = 1.0; let offsetIfParentX = 0.0; let offsetIfParentY = 0.0; const key = tileKey(tile.canonical.z, tile.canonical.x, tile.canonical.y); - let cachedTile = state.tileCache.get(key); + let cachedTile = heatmapState.tileCache.get(key); if (!cachedTile) { let child = { @@ -270,7 +268,7 @@ const HeatmapLayer: CustomLayerInterface = { continue; } const parentKey = tileKey(parent.z, parent.x, parent.y); - cachedTile = state.tileCache.get(parentKey); + cachedTile = heatmapState.tileCache.get(parentKey); if (cachedTile) { const zoomDifference = tile.canonical.z - parent.z; @@ -288,38 +286,42 @@ const HeatmapLayer: CustomLayerInterface = { cachedTile = fillerTile; } - const projection = state.map.transform.getProjectionData({ + const projection = heatmapState.map.transform.getProjectionData({ overscaledTileID: tile, }); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, cachedTile.texture); - gl.uniform1i(state.uniforms.uData, 0); + gl.uniform1i(heatmapState.uniforms.uData, 0); gl.uniform1f( - state.uniforms.uIntensity, + heatmapState.uniforms.uIntensity, sharedState.heatmapConfig.intensity, ); gl.uniform1f( - state.uniforms.uContrast, + heatmapState.uniforms.uContrast, sharedState.heatmapConfig.contrast, ); gl.uniformMatrix4fv( - state.uniforms.uProjectionMatrix, + heatmapState.uniforms.uProjectionMatrix, false, new Float32Array(matrix.defaultProjectionData.mainMatrix), ); gl.uniform4f( - state.uniforms.uTileMatrix, + heatmapState.uniforms.uTileMatrix, ...projection.tileMercatorCoords, ); - gl.uniform1f(state.uniforms.uWorldOffset, tile.wrap); - - gl.uniform1f(state.uniforms.uMax, max); - gl.uniform1f(state.uniforms.uScale, scaleIfParent); - gl.uniform2f(state.uniforms.uOffset, offsetIfParentX, offsetIfParentY); + gl.uniform1f(heatmapState.uniforms.uWorldOffset, tile.wrap); + + gl.uniform1f(heatmapState.uniforms.uMax, max); + gl.uniform1f(heatmapState.uniforms.uScale, scaleIfParent); + gl.uniform2f( + heatmapState.uniforms.uOffset, + offsetIfParentX, + offsetIfParentY, + ); gl.uniform1f( - state.uniforms.uAverageSurfaceVisibility, + heatmapState.uniforms.uAverageSurfaceVisibility, AVERAGE_SURFACE_VISIBILITY, ); @@ -334,42 +336,42 @@ function makeTile( bounds: LngLatBounds, data: Uint8Array, ) { - if (state?.gl === undefined) { + if (heatmapState?.gl === undefined) { console.warn("No GL context, couldn't make tile"); return; } - const texture = state.gl.createTexture(); - state.gl.bindTexture(state.gl.TEXTURE_2D, texture); - state.gl.texParameteri( - state.gl.TEXTURE_2D, - state.gl.TEXTURE_MIN_FILTER, - state.gl.NEAREST, + const texture = heatmapState.gl.createTexture(); + heatmapState.gl.bindTexture(heatmapState.gl.TEXTURE_2D, texture); + heatmapState.gl.texParameteri( + heatmapState.gl.TEXTURE_2D, + heatmapState.gl.TEXTURE_MIN_FILTER, + heatmapState.gl.NEAREST, ); - state.gl.texParameteri( - state.gl.TEXTURE_2D, - state.gl.TEXTURE_MAG_FILTER, - state.gl.NEAREST, + heatmapState.gl.texParameteri( + heatmapState.gl.TEXTURE_2D, + heatmapState.gl.TEXTURE_MAG_FILTER, + heatmapState.gl.NEAREST, ); - state.gl.texParameteri( - state.gl.TEXTURE_2D, - state.gl.TEXTURE_WRAP_S, - state.gl.CLAMP_TO_EDGE, + heatmapState.gl.texParameteri( + heatmapState.gl.TEXTURE_2D, + heatmapState.gl.TEXTURE_WRAP_S, + heatmapState.gl.CLAMP_TO_EDGE, ); - state.gl.texParameteri( - state.gl.TEXTURE_2D, - state.gl.TEXTURE_WRAP_T, - state.gl.CLAMP_TO_EDGE, + heatmapState.gl.texParameteri( + heatmapState.gl.TEXTURE_2D, + heatmapState.gl.TEXTURE_WRAP_T, + heatmapState.gl.CLAMP_TO_EDGE, ); - state.gl.texImage2D( - state.gl.TEXTURE_2D, + heatmapState.gl.texImage2D( + heatmapState.gl.TEXTURE_2D, 0, - state.gl.RGBA8UI, + heatmapState.gl.RGBA8UI, config.tileSize, config.tileSize, 0, - state.gl.RGBA_INTEGER, - state.gl.UNSIGNED_BYTE, + heatmapState.gl.RGBA_INTEGER, + heatmapState.gl.UNSIGNED_BYTE, data, ); diff --git a/website/src/Home.svelte b/website/src/Home.svelte index 0e0fed4..bd46063 100644 --- a/website/src/Home.svelte +++ b/website/src/Home.svelte @@ -10,10 +10,13 @@ import { LngLat, Map as MapLibre, + NavigationControl, type StyleSpecification, + setRTLTextPlugin, } from 'maplibre-gl'; import { onMount } from 'svelte'; import { navigate } from 'svelte5-router'; + import ClickEffect, { initClickEffect } from './ClickEffect.svelte'; import CollapsableModal from './components/CollapsableModal.svelte'; import LayerToggle from './components/LayerToggle.svelte'; import { HeatmapLayer } from './HeatmapLayer.ts'; @@ -26,7 +29,7 @@ import Slider from './Slider.svelte'; import { state } from './state.svelte.ts'; import TopLines from './TopLines.svelte'; - import { lonLatRound } from './utils.ts'; + import { enablePointer, lonLatRound } from './utils.ts'; import { findLongestLineInBoundsBruteForce, findLongestLineInBoundsFromGrid, @@ -104,7 +107,7 @@ state.longestLineInViewport = await findLongestLineInBoundsFromGrid(bounds); } - onMount(() => { + onMount(async () => { state.map = new MapLibre({ container: 'map', zoom: startingZoom, @@ -112,8 +115,24 @@ style: map_vector as StyleSpecification, transformConstrain, }); - + state.map.addControl( + new NavigationControl({ + visualizePitch: true, + visualizeRoll: true, + showZoom: true, + }), + 'bottom-right', + ); + + // https://maplibre.org/maplibre-gl-js/docs/API/functions/setRTLTextPlugin/ + if (typeof window !== 'undefined') { + await setRTLTextPlugin( + 'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.3.0/dist/mapbox-gl-rtl-text.js', + true, + ); + } state.map.on('load', async () => { + initClickEffect(); if (longest === '') { addHeatmapLayer(); } @@ -138,10 +157,18 @@ await updateTopLongestLines(); }); + + state.map?.on('moveend', async () => { + if (state.isFlying) { + enablePointer(); + state.isFlying = false; + } + }); }); +

All The Views In The World

@@ -188,6 +215,7 @@ loading... {:else}