Initial commit

This commit is contained in:
2026-04-15 17:08:39 +02:00
parent ae164c47a8
commit 47fd1c2b7a
1819 changed files with 685388 additions and 0 deletions

116
node_modules/maplibre-gl/LICENSE.txt generated vendored Normal file
View File

@@ -0,0 +1,116 @@
Copyright (c) 2023, MapLibre contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MapLibre GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from mapbox-gl-js v1.13 and earlier
Version v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license
Copyright (c) 2020, Mapbox
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Mapbox GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from glfx.js
Copyright (C) 2011 by Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Contains a portion of d3-color https://github.com/d3/d3-color
Copyright 2010-2016 Mike Bostock
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the author nor the names of contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

159
node_modules/maplibre-gl/README.md generated vendored Normal file
View File

@@ -0,0 +1,159 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://maplibre.org/img/maplibre-logos/maplibre-logo-for-dark-bg.svg">
<source media="(prefers-color-scheme: light)" srcset="https://maplibre.org/img/maplibre-logos/maplibre-logo-for-light-bg.svg">
<img alt="MapLibre Logo" src="https://maplibre.org/img/maplibre-logos/maplibre-logo-for-light-bg.svg" width="200">
</picture>
</p>
# MapLibre GL JS
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg?style=flat)](LICENSE.txt) [![Version](https://img.shields.io/npm/v/maplibre-gl?style=flat)](https://www.npmjs.com/package/maplibre-gl) [![CI](https://github.com/maplibre/maplibre-gl-js/actions/workflows/test-all.yml/badge.svg)](https://github.com/maplibre/maplibre-gl-js/actions/workflows/test-all.yml) [![PRs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](https://opensource.org/licenses/BSD-3-Clause) [![codecov](https://codecov.io/gh/maplibre/maplibre-gl-js/branch/main/graph/badge.svg)](https://codecov.io/gh/maplibre/maplibre-gl-js)
**[MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/API/)** is an open-source library for publishing maps on your websites or webview based apps. Fast displaying of maps is possible thanks to GPU-accelerated vector tile rendering.
It originated as an open-source fork of [mapbox-gl-js](https://github.com/mapbox/mapbox-gl-js), before their switch to a non-OSS license in December 2020. The library's initial versions (1.x) were intended to be a drop-in replacement for the Mapboxs OSS version (1.x) with additional functionality, but have evolved a lot since then.
## Getting Started
Include the JavaScript and CSS files in the `<head>` of your HTML file.
```html
<script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
```
Include the following code in the `<body>` of your HTML file.
```html
<div id='map' style='width: 400px; height: 300px;'></div>
<script>
var map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
center: [-74.5, 40], // starting position [lng, lat]
zoom: 9 // starting zoom
});
</script>
```
Enjoy the map!
<br />
## Documentation
Full documentation for this library [is available here](https://maplibre.org/maplibre-gl-js/docs/API/).
Check out the features through [examples](https://maplibre.org/maplibre-gl-js/docs/examples/).
| Showcases | |
| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| ![Display a map](https://maplibre.org/maplibre-gl-js/docs/assets/examples/display-a-map.png) | ![Third party vector tile source](https://maplibre.org/maplibre-gl-js/docs/assets/examples/3d-terrain.png) |
| ![Animate a series of images](https://maplibre.org/maplibre-gl-js/docs/assets/examples/animate-a-series-of-images.png) | ![Create a heatmap layer](https://maplibre.org/maplibre-gl-js/docs/assets/examples/create-a-heatmap-layer.png) |
| ![3D buildings](https://maplibre.org/maplibre-gl-js/docs/assets/examples/display-buildings-in-3d.png) | ![Visualize population density](https://maplibre.org/maplibre-gl-js/docs/assets/examples/visualize-population-density.png) |
<br />
Want an example? Have a look at the official [MapLibre GL JS Documentation](https://maplibre.org/maplibre-gl-js/docs/examples/).
Use MapLibre GL JS bindings for [React](https://visgl.github.io/react-map-gl/docs/get-started) and [Angular](https://github.com/maplibre/ngx-maplibre-gl). Find more at [awesome-maplibre](https://github.com/maplibre/awesome-maplibre).
<br />
## Contribution
### Getting Involved
Join the #maplibre slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/
Read the [CONTRIBUTING.md](CONTRIBUTING.md) guide in order to get familiar with how we do things around here.
### Avoid Fragmentation
If you depend on a free software alternative to `mapbox-gl-js`, please consider joining our effort! Anyone with a stake in a healthy community-led fork is welcome to help us figure out our next steps. We welcome contributors and leaders! MapLibre GL JS already represents the combined efforts of a few early fork efforts, and we all benefit from "one project" rather than "our way". If you know of other forks, please reach out to them and direct them here.
> **MapLibre GL JS** is developed following [Semantic Versioning (2.0.0)](https://semver.org/spec/v2.0.0.html).
### Bounties
We offer Bounties for some tasks in the MapLibre GL JS repo. Read more about the Bounties in our step-by-step guide:
https://maplibre.org/jobs/step-by-step-bounties-guide/
And find all currently published Bounties in MapLibre GL JS [here](https://github.com/maplibre/maplibre-gl-js/issues?q=is%3Aissue+is%3Aopen+label%3A%22%F0%9F%92%B0+bounty+L%22%2C%22%F0%9F%92%B0+bounty+S%22%2C%22%F0%9F%92%B0+bounty+M%22%2C%22%F0%9F%92%B0+bounty+XL%22%2C%22%F0%9F%92%B0+bounty+XXL%22+).
<br />
## Sponsors
We thank everyone who supported us financially in the past and special thanks to the people and organizations who support us with recurring donations!
Read more about the MapLibre Sponsorship Program at [https://maplibre.org/sponsors/](https://maplibre.org/sponsors/).
Gold:
<a href="https://www.microsoft.com/"><img src="https://maplibre.org/img/msft-logo.svg" alt="Logo MSFT" width="25%"/></a>
Silver:
<a href="https://www.mierune.co.jp/?lang=en"><img src="https://maplibre.org/img/mierune-logo.svg" alt="Logo MIERUNE" width="25%"/></a>
<a href="https://komoot.com/"><img src="https://maplibre.org/img/komoot-logo.svg" alt="Logo komoot" width="25%"/></a>
<a href="https://www.jawg.io/"><img src="https://maplibre.org/img/jawgmaps-logo.svg" alt="Logo JawgMaps" width="25%"/></a>
<a href="https://www.radar.com/"><img src="https://maplibre.org/img/radar-logo.svg" alt="Logo Radar" width="25%"/></a>
<a href="https://www.mapme.com/"><img src="https://maplibre.org/img/mapme-logo.svg" alt="Logo mapme" width="25%"/></a>
<a href="https://www.maptiler.com/"><img src="https://maplibre.org/img/maptiler-logo.svg" alt="Logo maptiler" width="25%"/></a>
<a href="https://aws.amazon.com/location"><img src="https://maplibre.org/img/aws-logo.svg" alt="Logo AWS" width="25%"/></a>
Backers and Supporters:
<a href="https://opencollective.com/maplibre/backer/0/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/0/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/1/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/1/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/2/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/2/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/3/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/3/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/4/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/4/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/5/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/5/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/6/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/6/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/7/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/7/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/8/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/8/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/9/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/9/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/10/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/10/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/11/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/11/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/12/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/12/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/13/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/13/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/14/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/14/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/15/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/15/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/16/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/16/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/17/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/17/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/18/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/18/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/19/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/19/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/20/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/20/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/21/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/21/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/22/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/22/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/23/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/23/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/24/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/24/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/25/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/25/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/26/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/26/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/27/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/27/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/28/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/28/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/29/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/29/avatar.svg?requireActive=false"></a>
<a href="https://opencollective.com/maplibre/backer/30/website?requireActive=false" target="_blank"><img src="https://opencollective.com/maplibre/backer/30/avatar.svg?requireActive=false"></a>
<br />
## Thank you Mapbox 🙏🏽
We'd like to acknowledge the amazing work Mapbox has contributed to open source. The open source community is sad to part ways with them, but we simultaneously feel grateful for everything they already contributed. `mapbox-gl-js` 1.x is an open source achievement that now lives on as `maplibre-gl`. We're proud to develop on the shoulders of giants, thank you Mapbox 🙇🏽‍♀️.
Please keep in mind: Unauthorized backports are the biggest threat to the MapLibre project. It is unacceptable to backport code from mapbox-gl-js, which is not covered by the former BSD-3 license. If you are unsure about this issue, [please ask](https://github.com/maplibre/maplibre-gl-js/discussions)!
<br />
## License
**MapLibre GL JS** is licensed under the [3-Clause BSD license](./LICENSE.txt).

12
node_modules/maplibre-gl/build/banner.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
import {readFileSync} from 'fs';
import {fileURLToPath} from 'url';
import {dirname, join} from 'path';
const packageJSONPath = join(dirname(fileURLToPath(import.meta.url)), '../package.json');
const packageJSON = JSON.parse(readFileSync(packageJSONPath, 'utf8'));
export default
`/**
* MapLibre GL JS
* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v${packageJSON.version}/LICENSE.txt
*/`;

77
node_modules/maplibre-gl/build/check-bundle-size.js generated vendored Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
import fs from "fs";
import zlib from "zlib";
import prettyBytes from "pretty-bytes";
const beforeSourcemap = JSON.parse(fs.readFileSync('./before.json').toString());
const afterSourcemap = JSON.parse(fs.readFileSync('./after.json').toString());
function fileSize(file) {
const {size} = fs.statSync(file);
const gzipped = zlib.gzipSync(fs.readFileSync(file)).length
return {
size,
gzipped
};
}
const beforejs = fileSize('./before/maplibre-gl.js');
const beforecss = fileSize('./before/maplibre-gl.css');
const afterjs = fileSize('./after/maplibre-gl.js');
const aftercss = fileSize('./after/maplibre-gl.css');
console.log('Bundle size report:\n');
console.log(`**Size Change:** ${prettyBytes(afterjs.gzipped + aftercss.gzipped - (beforejs.gzipped + beforecss.gzipped), { signed: true })}`);
console.log(`**Total Size Before:** ${prettyBytes(beforejs.gzipped + beforecss.gzipped)}`);
console.log(`**Total Size After:** ${prettyBytes(afterjs.gzipped + aftercss.gzipped)}`);
console.log(`
| Output file | Before | After | Change |
| :--- | :---: | :---: | :---: |
| maplibre-gl.js | ${prettyBytes(beforejs.gzipped)} | ${prettyBytes(afterjs.gzipped)} | ${prettyBytes(afterjs.gzipped - beforejs.gzipped, { signed: true })} |
| maplibre-gl.css | ${prettyBytes(beforecss.gzipped)} | ${prettyBytes(aftercss.gzipped)} | ${prettyBytes(aftercss.gzipped - beforecss.gzipped, { signed: true })} |`);
const before = {};
beforeSourcemap.results.forEach(result => {
Object.keys(result.files).forEach(filename => {
const {size} = result.files[filename];
before[filename] = size;
});
});
const after = {};
afterSourcemap.results.forEach(result => {
Object.keys(result.files).forEach(filename => {
const {size} = result.files[filename];
after[filename] = size;
});
});
const diffs = [];
Object.keys(Object.assign({}, before, after)).forEach(filename => {
const beforeSize = before[filename] || 0;
const afterSize = after[filename] || 0;
if (Math.abs(afterSize - beforeSize) > 0) {
diffs.push([
afterSize - beforeSize, // for sorting
filename.replace(/^[\./]+/, ''), // omit ../
prettyBytes(beforeSize),
prettyBytes(afterSize),
prettyBytes(afterSize - beforeSize, { signed: true })
]);
}
});
diffs.sort((a, b) => b[0] - a[0]);
console.log(`
<details><summary> <strong>View Details</strong></summary>`);
if (diffs.length) {
console.log(`
| Source file | Before | After | Change |
| :--- | :---: | :---: | :---: |
${diffs.map(diff => '| ' + diff.slice(1).join(' | ') + ' |').join('\n')}
`);
} else {
console.log('No major changes');
}
console.log(`</details>`);

View File

@@ -0,0 +1,26 @@
'use strict';
/// js files in this project are esm.
/// This package.json ensures that node imports from outside see them as such
// https://nodejs.org/api/packages.html#type
import { writeFile, mkdir, copyFile } from "node:fs/promises"
async function ensureDist() {
const dist = new URL("../dist/", import.meta.url);
await mkdir(dist, { recursive: true })
return dist
}
await writeFile(
new URL("package.json", await ensureDist()),
JSON.stringify({
name: "maplibre-gl",
type: "commonjs",
deprecated: "Please install maplibre-gl from parent directory instead",
})
)
await copyFile(
"./LICENSE.txt",
new URL("LICENSE.txt", await ensureDist())
)

80
node_modules/maplibre-gl/build/generate-doc-images.ts generated vendored Normal file
View File

@@ -0,0 +1,80 @@
import path from 'path';
import fs from 'fs';
import puppeteer from 'puppeteer';
import packageJson from '../package.json' with { type: 'json' };
const exampleName = process.argv[2];
const useLocalhost = (process.argv.length > 3) && (process.argv[3] === 'serve');
const examplePath = path.resolve('test', 'examples');
const browser = await puppeteer.launch({headless: exampleName === 'all'});
const page = await browser.newPage();
// set viewport and double deviceScaleFactor to get a closer shot of the map
await page.setViewport({
width: 600,
height: 250,
deviceScaleFactor: 2
});
async function createImage(exampleName) {
// get the example contents
if (useLocalhost) {
console.log('Using localhost to serve examples.');
await page.goto(`http://localhost:9966/test/examples/${exampleName}.html`);
} else {
const html = fs.readFileSync(path.resolve(examplePath, `${exampleName}.html`), 'utf-8');
await page.setContent(html.replaceAll('../../dist', `https://unpkg.com/maplibre-gl@${packageJson.version}/dist`));
}
// Wait for map to load, then wait two more seconds for images, etc. to load.
try {
// @ts-ignore
await page.evaluate(() => document.querySelector('.maplibregl-ctrl-attrib').style.display = 'none');
await page.waitForFunction('map.loaded()', {timeout: 10000});
// Wait for 5 seconds on 3d model examples, since this takes longer to load.
const waitTime = (exampleName.includes('3d-model') || exampleName.includes('globe')) ? 5000 : 1500;
console.log(`waiting for ${waitTime} ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
} catch {
// map.loaded() does not evaluate to true within 3 seconds, it's probably an animated example.
// In this case we take the screenshot immediately.
console.log(`Timed out waiting for map load on ${exampleName}.`);
}
await page
.screenshot({
path: `./docs/assets/examples/${exampleName}.png`,
type: 'png',
clip: {
x: 0,
y: 0,
width: 600,
height: 250
}
})
.then(() => console.log(`Created ./docs/assets/examples/${exampleName}.png`))
.catch((err) => {
console.log(err);
});
}
if (exampleName === 'all') {
const allFiles = fs.readdirSync(examplePath).filter(f => f.endsWith('html'));
console.log(`Generating ${allFiles.length} images.`);
for (const file of allFiles) {
await createImage(file.replace('.html', ''));
}
} else if (exampleName) {
await createImage(exampleName);
} else {
throw new Error(`
Usage: npm run generate-images <file-name|all> [serve]
file-name: the name of the example file in test/examples without the .html extension.
all: generate images for all examples.
serve: use localhost to serve examples - use 'npm run start' with this option, otherwise it will use the latest published version in npm.
Example: npm run generate-images 3d-buildings serve`
);
}
await browser.close();

196
node_modules/maplibre-gl/build/generate-docs.ts generated vendored Normal file
View File

@@ -0,0 +1,196 @@
import fs from 'fs';
import path from 'path';
import typedocConfig from '../typedoc.json' with {type: 'json'};
import packageJson from '../package.json' with {type: 'json'};
import {get} from 'https';
import sharp from 'sharp';
type HtmlDoc = {
title: string;
description: string;
mdFileName: string;
};
function generateAPIIntroMarkdown(lines: string[]): string {
let intro = `# Intro
This file is intended as a reference for the important and public classes of this API.
We recommend looking at the [examples](../examples/index.md) as they will help you the most to start with MapLibre.
Most of the classes written here have an "Options" object for initialization, it is recommended to check which options exist.
It is recommended to import what you need and then use it. Some examples for classes assume you did that.
For example, import the \`Map\` class like this:
\`\`\`ts
import {Map} from 'maplibre-gl';
const map = new Map(...)
\`\`\`
Import declarations are omitted from the examples for brevity.
`;
intro += lines.map(l => l.replace('../', './')).join('\n');
return intro;
}
function generateMarkdownForExample(title: string, description: string, file: string, htmlContent: string): string {
return `
# ${title}
${description}
<iframe src="../${file}" width="100%" style="border:none; height:400px"></iframe>
\`\`\`html
${htmlContent}
\`\`\`
`;
}
async function generateMarkdownIndexFileOfAllExamplesAndPackImages(indexArray: HtmlDoc[]): Promise<string> {
let indexMarkdown = '# Overview \n\n';
const promises: Promise<any>[] = [];
for (const indexArrayItem of indexArray) {
const imagePath = `docs/assets/examples/${indexArrayItem.mdFileName!.replace('.md', '.png')}`;
const outputPath = imagePath.replace('.png', '.webp');
promises.push(sharp(imagePath).webp({quality: 90, lossless: false}).toFile(outputPath));
indexMarkdown += `
## [${indexArrayItem.title}](./${indexArrayItem.mdFileName})
![${indexArrayItem.description}](${outputPath.replace('docs/', '../')}){ loading=lazy }
${indexArrayItem.description}
`;
}
await Promise.all(promises);
return indexMarkdown;
}
/**
* Builds the README.md file by parsing the modules.md file generated by typedoc.
*/
function generateReadme() {
const globalsFile = path.join(typedocConfig.out, 'globals.md');
const content = fs.readFileSync(globalsFile, 'utf-8');
let lines = content.split('\n');
const classesLineIndex = lines.indexOf(lines.find(l => l.endsWith('Classes')) as string);
lines = lines.splice(2, classesLineIndex - 2);
const contentString = generateAPIIntroMarkdown(lines);
fs.writeFileSync(path.join(typedocConfig.out, 'README.md'), contentString);
fs.rmSync(globalsFile);
}
/**
* This takes the examples folder with all the html files and generates a markdown file for each of them.
* It also create an index file with all the examples and their images.
*/
async function generateExamplesFolder() {
const examplesDocsFolder = path.join('docs', 'examples');
if (fs.existsSync(examplesDocsFolder)) {
fs.rmSync(examplesDocsFolder, {recursive: true, force: true});
}
fs.mkdirSync(examplesDocsFolder);
const examplesFolder = path.join('test', 'examples');
const files = fs.readdirSync(examplesFolder).filter(f => f.endsWith('html'));
const maplibreUnpkg = `https://unpkg.com/maplibre-gl@${packageJson.version}/`;
const indexArray = [] as HtmlDoc[];
for (const file of files) {
const htmlFile = path.join(examplesFolder, file);
let htmlContent = fs.readFileSync(htmlFile, 'utf-8');
htmlContent = htmlContent.replace(/\.\.\/\.\.\//g, maplibreUnpkg);
htmlContent = htmlContent.replace(/-dev.js/g, '.js');
const htmlContentLines = htmlContent.split('\n');
const title = htmlContentLines.find(l => l.includes('<title'))?.replace('<title>', '').replace('</title>', '').trim()!;
const description = htmlContentLines.find(l => l.includes('og:description'))?.replace(/.*content=\"(.*)\".*/, '$1')!;
fs.writeFileSync(path.join(examplesDocsFolder, file), htmlContent);
const mdFileName = file.replace('.html', '.md');
indexArray.push({
title,
description,
mdFileName
});
const exampleMarkdown = generateMarkdownForExample(title, description, file, htmlContent);
fs.writeFileSync(path.join(examplesDocsFolder, mdFileName), exampleMarkdown);
}
const indexMarkdown = await generateMarkdownIndexFileOfAllExamplesAndPackImages(indexArray);
fs.writeFileSync(path.join(examplesDocsFolder, 'index.md'), indexMarkdown);
}
async function fetchUrlContent(url: string) {
return new Promise<string>((resolve, reject) => {
get(url, (res) => {
let data = '';
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
reject(new Error(res.statusMessage));
return;
}
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(data);
});
}).on('error', reject);
});
}
async function generatePluginsPage() {
/**
* It extract some sections from Awesome MapLibre README.md so we can integrate it into our plugins page
*
* ```
* header
* <!-- [SOME-ID]:BEGIN -->
* CONTENT-TO-EXTRACT
* <!-- [SOME-ID]:END -->
* footer
* ```
*/
const awesomeReadmeUrl = 'https://raw.githubusercontent.com/maplibre/awesome-maplibre/main/README.md';
const awesomeReadme = await fetchUrlContent(awesomeReadmeUrl);
const contentGroupsRE = /<!--\s*\[([-a-zA-Z]+)\]:BEGIN\s*-->([\s\S]*?)<!--\s*\[\1\]:END\s*-->/g;
const matches = awesomeReadme.matchAll(contentGroupsRE);
const groups = Object.fromEntries(
Array.from(matches).map(([, key, content]) => [key, content])
);
const pluginsContent = `# Plugins
${groups['JAVASCRIPT-PLUGINS']}
## Framework Integrations
${groups['JAVASCRIPT-BINDINGS']}
`;
fs.writeFileSync('docs/plugins.md', pluginsContent, {encoding: 'utf-8'});
}
function updateMapLibreVersionForUNPKG() {
// Read index.md
const indexPath = 'docs/index.md';
let indexContent = fs.readFileSync(indexPath, 'utf-8');
// Replace the version number
indexContent = indexContent.replace(/unpkg\.com\/maplibre-gl@\^(\d+\.\d+\.\d+)/g, `unpkg.com/maplibre-gl@^${packageJson.version}`);
// Save index.md
fs.writeFileSync(indexPath, indexContent);
}
// !!Main flow start here!!
if (!fs.existsSync(typedocConfig.out)) {
throw new Error('Please run typedoc generation first!');
}
fs.rmSync(path.join(typedocConfig.out, 'README.md'));
generateReadme();
await generateExamplesFolder();
await generatePluginsPage();
updateMapLibreVersionForUNPKG();
console.log('Docs generation completed, to see it in action run\n npm run start-docs');

36
node_modules/maplibre-gl/build/generate-shaders.ts generated vendored Normal file
View File

@@ -0,0 +1,36 @@
import fs from 'fs';
import {globSync} from 'glob';
import path from 'path';
console.log('Generating shaders');
/**
* This script is intended to copy the glsl file to the compilation output folder,
* change their extension to .js and export the shaders as strings in javascript.
* It will also minify them a bit if needed and change the extension to .js
* After that it will create a combined typescript definition file and manipulate it a bit
* It will also create a simple package.json file to allow importing this package in webpack
*/
function glslToTs(code: string): string {
code = code
.trim() // strip whitespace at the start/end
.replace(/\s*\/\/[^\n]*\n/g, '\n') // strip double-slash comments
.replace(/\n+/g, '\n') // collapse multi line breaks
.replace(/\n\s+/g, '\n') // strip indentation
.replace(/\s?([+-\/*=,])\s?/g, '$1') // strip whitespace around operators
.replace(/([;\(\),\{\}])\n(?=[^#])/g, '$1'); // strip more line breaks
return `// This file is generated. Edit build/generate-shaders.ts, then run \`npm run codegen\`.
export default ${JSON.stringify(code).replaceAll('"', '\'')};\n`;
}
const shaderFiles = globSync('./src/shaders/*.glsl');
for (const file of shaderFiles) {
const glslFile = fs.readFileSync(file, 'utf8');
const tsSource = glslToTs(glslFile);
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, tsSource);
}
console.log(`Finished converting ${shaderFiles.length} shaders`);

View File

@@ -0,0 +1,440 @@
/*
* Generates the following:
* - data/array_types.js, which consists of:
* - StructArrayLayout_* subclasses, one for each underlying memory layout we need
* - Named exports mapping each conceptual array type (e.g., CircleLayoutArray) to its corresponding StructArrayLayout class
* - Particular, named StructArray subclasses, when fancy struct accessors are needed (e.g. CollisionBoxArray)
*/
'use strict';
import * as fs from 'fs';
import * as util from '../src/util/util';
import {createLayout, viewTypes} from '../src/util/struct_array';
import type {ViewType, StructArrayLayout} from '../src/util/struct_array';
import posAttributes from '../src/data/pos_attributes';
import pos3dAttributes from '../src/data/pos3d_attributes';
import rasterBoundsAttributes from '../src/data/raster_bounds_attributes';
import circleAttributes from '../src/data/bucket/circle_attributes';
import fillAttributes from '../src/data/bucket/fill_attributes';
import fillExtrusionAttributes from '../src/data/bucket/fill_extrusion_attributes';
import {lineLayoutAttributes} from '../src/data/bucket/line_attributes';
import {lineLayoutAttributesExt} from '../src/data/bucket/line_attributes_ext';
import {patternAttributes} from '../src/data/bucket/pattern_attributes';
import {dashAttributes} from '../src/data/bucket/dash_attributes';
// symbol layer specific arrays
import {
symbolLayoutAttributes,
dynamicLayoutAttributes,
placementOpacityAttributes,
collisionBox,
collisionBoxLayout,
collisionCircleLayout,
collisionVertexAttributes,
quadTriangle,
placement,
symbolInstance,
glyphOffset,
lineVertex,
textAnchorOffset
} from '../src/data/bucket/symbol_attributes';
const typeAbbreviations = {
'Int8': 'b',
'Uint8': 'ub',
'Int16': 'i',
'Uint16': 'ui',
'Int32': 'l',
'Uint32': 'ul',
'Float32': 'f'
};
const arraysWithStructAccessors = [];
const arrayTypeEntries = new Set();
const layoutCache = {};
function normalizeMembers(members, usedTypes) {
return members.map((member) => {
if (usedTypes && !usedTypes.has(member.type)) {
usedTypes.add(member.type);
}
return util.extend(member, {
size: sizeOf(member.type),
view: member.type.toLowerCase()
});
});
}
// - If necessary, write the StructArrayLayout_* class for the given layout
// - If `includeStructAccessors`, write the fancy subclass
// - Add an entry for `name` in the array type registry
function createStructArrayType(name: string, layout: StructArrayLayout, includeStructAccessors: boolean = false) {
const hasAnchorPoint = layout.members.some(m => m.name === 'anchorPointX');
// create the underlying StructArrayLayout class exists
const layoutClass = createStructArrayLayoutType(layout);
const arrayClass = `${camelize(name)}Array`;
if (includeStructAccessors) {
const usedTypes = new Set(['Uint8']);
const members = normalizeMembers(layout.members, usedTypes);
arraysWithStructAccessors.push({
arrayClass,
members,
size: layout.size,
usedTypes,
hasAnchorPoint,
layoutClass,
includeStructAccessors
});
} else {
arrayTypeEntries.add(`export class ${arrayClass} extends ${layoutClass} {}`);
}
}
function createStructArrayLayoutType({members, size, alignment}) {
const usedTypes = new Set(['Uint8']);
members = normalizeMembers(members, usedTypes);
// combine consecutive 'members' with same underlying type, summing their
// component counts
if (!alignment || alignment === 1) members = members.reduce((memo, member) => {
if (memo.length > 0 && memo[memo.length - 1].type === member.type) {
const last = memo[memo.length - 1];
return memo.slice(0, -1).concat(util.extend({}, last, {
components: last.components + member.components,
}));
}
return memo.concat(member);
}, []);
const key = `${members.map(m => `${m.components}${typeAbbreviations[m.type]}`).join('')}${size}`;
const className = `StructArrayLayout${key}`;
if (!layoutCache[key]) {
layoutCache[key] = {
className,
members,
size,
usedTypes
};
}
return className;
}
function sizeOf(type: ViewType): number {
return viewTypes[type].BYTES_PER_ELEMENT;
}
function camelize (str) {
return str.replace(/(?:^|[-_])(.)/g, (_, x) => {
return /^[0-9]$/.test(x) ? _ : x.toUpperCase();
});
}
createStructArrayType('pos', posAttributes);
createStructArrayType('pos3d', pos3dAttributes);
createStructArrayType('raster_bounds', rasterBoundsAttributes);
// layout vertex arrays
const layoutAttributes = {
circle: circleAttributes,
fill: fillAttributes,
'fill-extrusion': fillExtrusionAttributes,
heatmap: circleAttributes,
line: lineLayoutAttributes,
lineExt: lineLayoutAttributesExt,
pattern: patternAttributes,
dash: dashAttributes
};
for (const name in layoutAttributes) {
createStructArrayType(`${name.replace(/-/g, '_')}_layout`, layoutAttributes[name]);
}
createStructArrayType('symbol_layout', symbolLayoutAttributes);
createStructArrayType('symbol_dynamic_layout', dynamicLayoutAttributes);
createStructArrayType('symbol_opacity', placementOpacityAttributes);
createStructArrayType('collision_box', collisionBox, true);
createStructArrayType('collision_box_layout', collisionBoxLayout);
createStructArrayType('collision_circle_layout', collisionCircleLayout);
createStructArrayType('collision_vertex', collisionVertexAttributes);
createStructArrayType('quad_triangle', quadTriangle);
createStructArrayType('placed_symbol', placement, true);
createStructArrayType('symbol_instance', symbolInstance, true);
createStructArrayType('glyph_offset', glyphOffset, true);
createStructArrayType('symbol_line_vertex', lineVertex, true);
createStructArrayType('text_anchor_offset', textAnchorOffset, true);
// feature index array
createStructArrayType('feature_index', createLayout([
// the index of the feature in the original vectortile
{type: 'Uint32', name: 'featureIndex'},
// the source layer the feature appears in
{type: 'Uint16', name: 'sourceLayerIndex'},
// the bucket the feature appears in
{type: 'Uint16', name: 'bucketIndex'}
]), true);
// triangle index array
createStructArrayType('triangle_index', createLayout([
{type: 'Uint16', name: 'vertices', components: 3}
]));
// line index array
createStructArrayType('line_index', createLayout([
{type: 'Uint16', name: 'vertices', components: 2}
]));
// line strip index array
createStructArrayType('line_strip_index', createLayout([
{type: 'Uint16', name: 'vertices', components: 1}
]));
// paint vertex arrays
// used by SourceBinder for float properties
createStructArrayLayoutType(createLayout([{
name: 'dummy name (unused for StructArrayLayout)',
type: 'Float32',
components: 1
}], 4));
// used by SourceBinder for color properties and CompositeBinder for float properties
createStructArrayLayoutType(createLayout([{
name: 'dummy name (unused for StructArrayLayout)',
type: 'Float32',
components: 2
}], 4));
// used by CompositeBinder for color properties
createStructArrayLayoutType(createLayout([{
name: 'dummy name (unused for StructArrayLayout)',
type: 'Float32',
components: 4
}], 4));
const layouts = Object.keys(layoutCache).map(k => layoutCache[k]);
function emitStructArrayLayout(locals) {
const output = [];
const {
className,
members,
size,
usedTypes
} = locals;
const structArrayLayoutClass = className;
output.push(
`/**
* @internal
* Implementation of the StructArray layout:`);
for (const member of members) {
output.push(
` * [${member.offset}] - ${member.type}[${member.components}]`);
}
output.push(
` *
*/
class ${structArrayLayoutClass} extends StructArray {`);
for (const type of usedTypes) {
output.push(
` ${type.toLowerCase()}: ${type}Array;`);
}
output.push(`
_refreshViews() {`);
for (const type of usedTypes) {
output.push(
` this.${type.toLowerCase()} = new ${type}Array(this.arrayBuffer);`);
}
output.push(
' }');
// prep for emplaceBack: collect type sizes and count the number of arguments
// we'll need
const bytesPerElement = size;
const usedTypeSizes = [];
const argNames = [];
const argNamesTyped = [];
for (const member of members) {
if (usedTypeSizes.indexOf(member.size) < 0) {
usedTypeSizes.push(member.size);
}
for (let c = 0; c < member.components; c++) {
// arguments v0, v1, v2, ... are, in order, the components of
// member 0, then the components of member 1, etc.
const name = `v${argNames.length}`;
argNames.push(name);
argNamesTyped.push(`${name}: number`);
}
}
output.push(
`
public emplaceBack(${argNamesTyped.join(', ')}) {
const i = this.length;
this.resize(i + 1);
return this.emplace(i, ${argNames.join(', ')});
}
public emplace(i: number, ${argNamesTyped.join(', ')}) {`);
for (const size of usedTypeSizes) {
output.push(
` const o${size.toFixed(0)} = i * ${(bytesPerElement / size).toFixed(0)};`);
}
let argIndex = 0;
for (const member of members) {
for (let c = 0; c < member.components; c++) {
// The index for `member` component `c` into the appropriate type array is:
// this.{TYPE}[o{SIZE} + MEMBER_OFFSET + {c}] = v{X}
// where MEMBER_OFFSET = ROUND(member.offset / size) is the per-element
// offset of this member into the array
const index = `o${member.size.toFixed(0)} + ${(member.offset / member.size + c).toFixed(0)}`;
output.push(
` this.${member.view}[${index}] = v${argIndex++};`);
}
}
output.push(
` return i;
}
}
${structArrayLayoutClass}.prototype.bytesPerElement = ${size};
register('${structArrayLayoutClass}', ${structArrayLayoutClass});
`);
return output.join('\n');
}
function emitStructArray(locals) {
const output = [];
const {
arrayClass,
members,
size,
hasAnchorPoint,
layoutClass,
includeStructAccessors
} = locals;
const structTypeClass = arrayClass.replace('Array', 'Struct');
const structArrayClass = arrayClass;
const structArrayLayoutClass = layoutClass;
// collect components
const components = [];
for (const member of members) {
for (let c = 0; c < member.components; c++) {
let name = member.name;
if (member.components > 1) {
name += c;
}
components.push({name, member, component: c});
}
}
// exceptions for which we generate accessors on the array rather than a separate struct for performance
const useComponentGetters = structArrayClass === 'GlyphOffsetArray' || structArrayClass === 'SymbolLineVertexArray';
if (includeStructAccessors && !useComponentGetters) {
output.push(
`/** @internal */
class ${structTypeClass} extends Struct {
_structArray: ${structArrayClass};`);
for (const {name, member, component} of components) {
const elementOffset = `this._pos${member.size.toFixed(0)}`;
const componentOffset = (member.offset / member.size + component).toFixed(0);
const index = `${elementOffset} + ${componentOffset}`;
const componentAccess = `this._structArray.${member.view}[${index}]`;
output.push(
` get ${name}() { return ${componentAccess}; }`);
// generate setters for properties that are updated during runtime symbol placement; others are read-only
if (name === 'crossTileID' || name === 'placedOrientation' || name === 'hidden') {
output.push(
` set ${name}(x: number) { ${componentAccess} = x; }`);
}
}
// Special case used for the CollisionBoxArray type
if (hasAnchorPoint) {
output.push(
' get anchorPoint() { return new Point(this.anchorPointX, this.anchorPointY); }');
}
output.push(
`}
${structTypeClass}.prototype.size = ${size};
export type ${structTypeClass.replace('Struct', '')} = ${structTypeClass};
`);
} // end 'if (includeStructAccessors)'
output.push(
`/** @internal */
export class ${structArrayClass} extends ${structArrayLayoutClass} {`);
if (useComponentGetters) {
for (const member of members) {
for (let c = 0; c < member.components; c++) {
if (!includeStructAccessors) continue;
let name = `get${member.name}`;
if (member.components > 1) {
name += c;
}
const componentOffset = (member.offset / member.size + c).toFixed(0);
const componentStride = size / member.size;
output.push(
` ${name}(index: number) { return this.${member.view}[index * ${componentStride} + ${componentOffset}]; }`);
}
}
} else if (includeStructAccessors) { // get(i)
output.push(
` /**
* Return the ${structTypeClass} at the given location in the array.
* @param index - The index of the element.
*/
get(index: number): ${structTypeClass} {
return new ${structTypeClass}(this, index);
}`);
}
output.push(
`}
register('${structArrayClass}', ${structArrayClass});
`);
return output.join('\n');
}
fs.writeFileSync('src/data/array_types.g.ts',
`// This file is generated. Edit build/generate-struct-arrays.ts, then run \`npm run codegen\`.
import {Struct, StructArray} from '../util/struct_array';
import {register} from '../util/web_worker_transfer';
import Point from '@mapbox/point-geometry';
${layouts.map(emitStructArrayLayout).join('\n')}
${arraysWithStructAccessors.map(emitStructArray).join('\n')}
${[...arrayTypeEntries].join('\n')}
export {
${layouts.map(layout => layout.className).join(',\n ')}
};
`);

287
node_modules/maplibre-gl/build/generate-style-code.ts generated vendored Normal file
View File

@@ -0,0 +1,287 @@
'use strict';
import * as fs from 'fs';
import {v8} from '@maplibre/maplibre-gl-style-spec';
function camelCase(str: string): string {
return str.replace(/-(.)/g, (_, x) => {
return x.toUpperCase();
});
}
function pascalCase(str: string): string {
const almostCamelized = camelCase(str);
return almostCamelized[0].toUpperCase() + almostCamelized.slice(1);
}
function nativeType(property) {
switch (property.type) {
case 'boolean':
return 'boolean';
case 'number':
return 'number';
case 'string':
return 'string';
case 'enum':
return Object.keys(property.values).map(v => JSON.stringify(v)).join(' | ');
case 'color':
return 'Color';
case 'padding':
return 'Padding';
case 'numberArray':
return 'NumberArray';
case 'colorArray':
return 'ColorArray';
case 'variableAnchorOffsetCollection':
return 'VariableAnchorOffsetCollection';
case 'sprite':
return 'Sprite';
case 'formatted':
return 'Formatted';
case 'resolvedImage':
return 'ResolvedImage';
case 'array':
if (property.length) {
return `[${new Array(property.length).fill(nativeType({type: property.value})).join(', ')}]`;
} else {
return `Array<${nativeType({type: property.value, values: property.values})}>`;
}
default: throw new Error(`unknown type "${property.type}" for "${property.name}"`);
}
}
function possiblyEvaluatedType(property) {
const propType = nativeType(property);
switch (property['property-type']) {
case 'color-ramp':
return 'ColorRampProperty';
case 'cross-faded':
return `CrossFaded<${propType}>`;
case 'cross-faded-data-driven':
return `PossiblyEvaluatedPropertyValue<CrossFaded<${propType}>>`;
case 'data-driven':
return `PossiblyEvaluatedPropertyValue<${propType}>`;
}
return propType;
}
function propertyType(property) {
switch (property['property-type']) {
case 'data-driven':
return `DataDrivenProperty<${nativeType(property)}>`;
case 'cross-faded':
return `CrossFadedProperty<${nativeType(property)}>`;
case 'cross-faded-data-driven':
return `CrossFadedDataDrivenProperty<${nativeType(property)}>`;
case 'color-ramp':
return 'ColorRampProperty';
case 'data-constant':
case 'constant':
return `DataConstantProperty<${nativeType(property)}>`;
default:
throw new Error(`unknown property-type "${property['property-type']}" for ${property.name}`);
}
}
function runtimeType(property) {
switch (property.type) {
case 'boolean':
return 'BooleanType';
case 'number':
return 'NumberType';
case 'string':
case 'enum':
return 'StringType';
case 'color':
return 'ColorType';
case 'padding':
return 'PaddingType';
case 'variableAnchorOffsetCollection':
return 'VariableAnchorOffsetCollectionType';
case 'sprite':
return 'SpriteType';
case 'formatted':
return 'FormattedType';
case 'Image':
return 'ImageType';
case 'array':
if (property.length) {
return `array(${runtimeType({type: property.value})}, ${property.length})`;
} else {
return `array(${runtimeType({type: property.value})})`;
}
default: throw new Error(`unknown type "${property.type}" for "${property.name}"`);
}
}
function overrides(property) {
return `{ runtimeType: ${runtimeType(property)}, getOverride: (o) => o.${camelCase(property.name)}, hasOverride: (o) => !!o.${camelCase(property.name)} }`;
}
function propertyValue(property, type) {
const propertyAsSpec = `styleSpec["${type}_${property.layerType}"]["${property.name}"] as any as StylePropertySpecification`;
switch (property['property-type']) {
case 'data-driven':
if (property.overridable) {
return `new DataDrivenProperty(${propertyAsSpec}, ${overrides(property)})`;
} else {
return `new DataDrivenProperty(${propertyAsSpec})`;
}
case 'cross-faded':
return `new CrossFadedProperty(${propertyAsSpec})`;
case 'cross-faded-data-driven':
return `new CrossFadedDataDrivenProperty(${propertyAsSpec})`;
case 'color-ramp':
return `new ColorRampProperty(${propertyAsSpec})`;
case 'data-constant':
case 'constant':
return `new DataConstantProperty(${propertyAsSpec})`;
default:
throw new Error(`unknown property-type "${property['property-type']}" for ${property.name}`);
}
}
const layers = Object.keys(v8.layer.type.values).map((type) => {
const layoutProperties = Object.keys(v8[`layout_${type}`]).reduce((memo, name) => {
if (name !== 'visibility') {
v8[`layout_${type}`][name].name = name;
v8[`layout_${type}`][name].layerType = type;
memo.push(v8[`layout_${type}`][name]);
}
return memo;
}, []);
const paintProperties = Object.keys(v8[`paint_${type}`]).reduce((memo, name) => {
v8[`paint_${type}`][name].name = name;
v8[`paint_${type}`][name].layerType = type;
memo.push(v8[`paint_${type}`][name]);
return memo;
}, []);
return {type, layoutProperties, paintProperties};
});
function emitlayerProperties(locals) {
const output = [];
const layerType = pascalCase(locals.type);
const {
layoutProperties,
paintProperties
} = locals;
output.push(
`// This file is generated. Edit build/generate-style-code.ts, then run 'npm run codegen'.
/* eslint-disable */
import {latest as styleSpec} from '@maplibre/maplibre-gl-style-spec';
import {
Properties,
DataConstantProperty,
DataDrivenProperty,
CrossFadedDataDrivenProperty,
CrossFadedProperty,
ColorRampProperty,
PossiblyEvaluatedPropertyValue,
CrossFaded
} from '../properties';
import type {Color, Formatted, Padding, NumberArray, ColorArray, ResolvedImage, VariableAnchorOffsetCollection} from '@maplibre/maplibre-gl-style-spec';
import {StylePropertySpecification} from '@maplibre/maplibre-gl-style-spec';
`);
const overridables = paintProperties.filter(p => p.overridable);
if (overridables.length) {
const overridesArray = `import {
${overridables.reduce((imports, prop) => { imports.push(runtimeType(prop)); return imports; }, []).join(',\n ')}
} from '@maplibre/maplibre-gl-style-spec';
`;
output.push(overridesArray);
}
if (layoutProperties.length) {
output.push(
`export type ${layerType}LayoutProps = {`);
for (const property of layoutProperties) {
output.push(
` "${property.name}": ${propertyType(property)},`);
}
output.push(
`};
export type ${layerType}LayoutPropsPossiblyEvaluated = {`);
for (const property of layoutProperties) {
output.push(
` "${property.name}": ${possiblyEvaluatedType(property)},`);
}
output.push(
`};
let layout: Properties<${layerType}LayoutProps>;
const getLayout = () => layout = layout || new Properties({`);
for (const property of layoutProperties) {
output.push(
` "${property.name}": ${propertyValue(property, 'layout')},`);
}
output.push(
'});');
}
if (paintProperties.length) {
output.push(
`
export type ${layerType}PaintProps = {`);
for (const property of paintProperties) {
output.push(
` "${property.name}": ${propertyType(property)},`);
}
output.push(
`};
export type ${layerType}PaintPropsPossiblyEvaluated = {`);
for (const property of paintProperties) {
output.push(
` "${property.name}": ${possiblyEvaluatedType(property)},`);
}
output.push(
'};');
} else {
output.push(
`export type ${layerType}PaintProps = {};`);
}
output.push(
`
let paint: Properties<${layerType}PaintProps>;
const getPaint = () => paint = paint || new Properties({`);
for (const property of paintProperties) {
output.push(
` "${property.name}": ${propertyValue(property, 'paint')},`);
}
output.push(
`});
export default ({ get paint() { return getPaint() }${layoutProperties.length ? ', get layout() { return getLayout() }' : ''} });`);
return output.join('\n');
}
for (const layer of layers) {
fs.writeFileSync(`src/style/style_layer/${layer.type.replace('-', '_')}_style_layer_properties.g.ts`, emitlayerProperties(layer));
}

370
node_modules/maplibre-gl/build/generate-unicode-data.ts generated vendored Normal file
View File

@@ -0,0 +1,370 @@
import * as fs from 'fs';
import * as regenerate from 'regenerate';
/**
* The heuristics in the functions below are based on this version of the
* Unicode Standard. This constant should match the `@unicode/unicode-*` package
* in package.json.
*
* When upgrading to a new version of the standard, consider any new scripts,
* blocks, and characters that may require different script detection.
*/
const unicodeVersion = '17.0.0';
async function createSet(blocks: Array<string>, scripts: Array<string>): Promise<regenerate.regenerate> {
const set = regenerate.default();
for (const block of blocks) {
const slug = block.replace(/[- ]/g, '_');
set.add((await import(`@unicode/unicode-${unicodeVersion}/Block/${slug}/code-points.js`)).default);
}
for (const script of scripts) {
const slug = script.replace(/[- ]/g, '_');
set.add((await import(`@unicode/unicode-${unicodeVersion}/Script/${slug}/code-points.js`)).default);
}
return set;
}
async function usesLocalIdeographFontFamily(): Promise<string> {
// Local rendering is preferred for Unicode code blocks that represent
// writing systems for which TinySDF produces optimal results and greatly
// reduces bandwidth consumption. In general, TinySDF is best for any
// writing system typically set in a monospaced font. With more than 99,000
// codepoints accessed essentially at random, Hanzi/Kanji/Hanja (from the
// CJK Unified Ideographs blocks) is the canonical example of wasteful
// bandwidth consumption when rendered remotely. For visual consistency
// within CJKV text, even relatively small CJKV and other siniform code
// blocks prefer local rendering.
const set = await createSet([
'CJK Compatibility Forms',
'CJK Compatibility',
'CJK Radicals Supplement',
'CJK Strokes',
'CJK Unified Ideographs',
'Enclosed CJK Letters And Months',
'Enclosed Ideographic Supplement',
'Halfwidth And Fullwidth Forms',
'Hangul Syllables',
'Hiragana',
'Ideographic Symbols And Punctuation',
'Kana Extended-A',
'Kana Extended-B',
'Kana Supplement',
'Kangxi Radicals',
'Katakana', // includes "ー"
'Katakana Phonetic Extensions',
// memo: these symbols are not all. others could be added if needed.
'CJK Symbols And Punctuation', // 、。〃〄々〆〇〈〉《》「...
'Halfwidth And Fullwidth Forms',
'Small Kana Extension',
'Vertical Forms',
], [
'Bopomofo',
'Han',
'Hangul',
'Hiragana',
'Katakana',
'Khitan Small Script',
'Nushu',
'Tangut',
'Yi',
]);
set.add((await import(`@unicode/unicode-${unicodeVersion}/Binary_Property/Ideographic/code-points.js`)).default);
return set.toString();
}
async function allowsIdeographicBreaking(): Promise<string> {
// Unicode only considers CJKV to be ideographic, but some other scripts mix
// with CJKV so can also have ideographic line breaking.
const set = await createSet([
'CJK Compatibility Forms',
'CJK Compatibility',
'CJK Radicals Supplement',
'CJK Strokes',
'CJK Symbols And Punctuation',
'Enclosed CJK Letters And Months',
'Enclosed Ideographic Supplement',
'Halfwidth And Fullwidth Forms',
'Ideographic Description Characters',
'Ideographic Symbols And Punctuation',
'Kana Extended-A',
'Kana Extended-B',
'Kana Supplement',
'Kangxi Radicals',
'Katakana Phonetic Extensions',
'Small Kana Extension',
'Vertical Forms',
], [
'Bopomofo',
'Han',
'Hiragana',
'Katakana',
'Khitan Small Script',
'Nushu',
'Tangut',
'Yi',
]);
return set.toString();
}
// The following logic comes from
// <https://www.unicode.org/Public/17.0.0/ucd/VerticalOrientation.txt>.
// Keep it synchronized with
// <https://www.unicode.org/Public/UCD/latest/ucd/VerticalOrientation.txt>.
// The data file denotes with “U” or “Tu” any codepoint that may be drawn
// upright in vertical text but does not distinguish between upright and
// “neutral” characters.
async function hasUprightVerticalOrientation(): Promise<string> {
const set = await createSet([
'Alchemical Symbols',
'Anatolian Hieroglyphs',
'Byzantine Musical Symbols',
'Chess Symbols',
'CJK Compatibility Forms',
'CJK Compatibility',
'CJK Strokes',
'CJK Symbols And Punctuation',
'Counting Rod Numerals',
'Domino Tiles',
'Emoticons',
'Enclosed Alphanumeric Supplement',
'Enclosed CJK Letters And Months',
'Geometric Shapes Extended',
'Halfwidth And Fullwidth Forms',
'Ideographic Description Characters',
'Kanbun',
'Katakana',
'Mahjong Tiles',
'Mayan Numerals',
'Meroitic Hieroglyphs',
'Miscellaneous Symbols And Pictographs',
'Miscellaneous Symbols Supplement',
'Musical Symbols',
'Ornamental Dingbats',
'Playing Cards',
'Siddham',
'Small Form Variants',
'Small Kana Extension',
'Soyombo',
'Supplemental Symbols And Pictographs',
'Sutton SignWriting',
'Symbols And Pictographs Extended-A',
'Tai Xuan Jing Symbols',
'Transport And Map Symbols',
'Vertical Forms',
'Yijing Hexagram Symbols',
'Zanabazar Square',
'Znamenny Musical Notation',
], [
'Bopomofo',
'Canadian Aboriginal',
'Han',
'Hangul',
'Hiragana',
'Katakana',
'Khitan Small Script',
'Nushu',
'Tangut',
'Yi',
]);
set.add(0x02EA /* modifier letter yin departing tone mark */);
set.add(0x02EB /* modifier letter yang departing tone mark */);
// Exceptions to CJK Compatibility Forms
set.removeRange(0xFE49 /* dashed overline */, 0xFE4F /* wavy low line */);
// Exceptions to CJK Symbols and Punctuation
set.removeRange(0x3008 /* left angle bracket */, 0x3011 /* right black lenticular bracket */);
set.removeRange(0x3014 /* left tortoise shell bracket */, 0x301F /* low double prime quotation mark */);
set.remove(0x3030 /* wavy dash */);
// Exceptions to Katakana
set.remove(0x30FC /* katakana-hiragana prolonged sound mark */);
// Exceptions to Halfwidth and Fullwidth Forms
set.remove(0xFF08 /* fullwidth left parenthesis */);
set.remove(0xFF09 /* fullwidth right parenthesis */);
set.remove(0xFF0D /* fullwidth hyphen-minus */);
set.removeRange(0xFF1A /* fullwidth colon */, 0xFF1E /* fullwidth greater-than sign */);
set.remove(0xFF3B /* fullwidth left square bracket */);
set.remove(0xFF3D /* fullwidth right square bracket */);
set.remove(0xFF3F /* fullwidth low line */);
set.removeRange(0xFF5B /* fullwidth left curly bracket */, 0xFFDF);
set.remove(0xFFE3 /* fullwidth macron */);
set.removeRange(0xFFE8 /* halfwidth forms light vertical */, 0xFFEF);
// Exceptions to Small Form Variants
set.removeRange(0xFE58 /* small em dash */, 0xFE5E /* small right tortoise shell bracket */);
set.removeRange(0xFE63 /* small hyphen-minus */, 0xFE66 /* small equals sign */);
return set.toString();
}
async function hasNeutralVerticalOrientation(): Promise<string> {
const set = await createSet([
'CJK Compatibility Forms',
'CJK Symbols And Punctuation',
'Control Pictures',
'Enclosed Alphanumerics',
'Geometric Shapes',
'Halfwidth And Fullwidth Forms',
'Katakana',
'Letterlike Symbols',
'Miscellaneous Symbols',
'Number Forms',
'Optical Character Recognition',
'Private Use Area',
'Small Form Variants',
'Supplementary Private Use Area-A',
'Supplementary Private Use Area-B',
], []);
// Latin-1 Supplement
set.add(0x00A7 /* section sign */);
set.add(0x00A9 /* copyright sign */);
set.add(0x00AE /* registered sign */);
set.add(0x00B1 /* plus-minus sign */);
set.add(0x00BC /* vulgar fraction one quarter */);
set.add(0x00BD /* vulgar fraction one half */);
set.add(0x00BE /* vulgar fraction three quarters */);
set.add(0x00D7 /* multiplication sign */);
set.add(0x00F7 /* division sign */);
// General Punctuation
set.add(0x2016 /* double vertical line */);
set.add(0x2020 /* dagger */);
set.add(0x2021 /* double dagger */);
set.add(0x2030 /* per mille sign */);
set.add(0x2031 /* per ten thousand sign */);
set.add(0x203B /* reference mark */);
set.add(0x203C /* double exclamation mark */);
set.add(0x2042 /* asterism */);
set.add(0x2047 /* double question mark */);
set.add(0x2048 /* question exclamation mark */);
set.add(0x2049 /* exclamation question mark */);
set.add(0x2051 /* two asterisks aligned vertically */);
// Miscellaneous Technical
set.addRange(0x2300 /* diameter sign */, 0x2307 /* wavy line */);
set.addRange(0x230C /* bottom right crop */, 0x231F /* bottom right corner */);
set.addRange(0x2324 /* up arrowhead between two horizontal bars */, 0x2328 /* keyboard */);
set.add(0x232B /* erase to the left */);
set.addRange(0x237D /* shouldered open box */, 0x239A /* clear screen symbol */);
set.addRange(0x23BE /* dentistry symbol light vertical and top right */, 0x23CD /* square foot */);
set.add(0x23CF /* eject symbol */);
set.addRange(0x23D1 /* metrical breve */, 0x23DB /* fuse */);
set.addRange(0x23E2 /* white trapezium */, 0x23FF);
// Exceptions to Control Pictures
set.remove(0x2423 /* open box */);
// Exceptions to Miscellaneous Symbols
set.removeRange(0x261A /* black left pointing index */, 0x261F /* white down pointing index */);
// Miscellaneous Symbols and Arrows
set.addRange(0x2B12 /* square with top half black */, 0x2B2F /* white vertical ellipse */);
set.addRange(0x2B50 /* white medium star */, 0x2B59 /* heavy circled saltire */);
set.addRange(0x2BB8 /* upwards white arrow from bar with horizontal bar */, 0x2BEB);
set.add(0x221E /* infinity */);
set.add(0x2234 /* therefore */);
set.add(0x2235 /* because */);
set.addRange(0x2700 /* black safety scissors */, 0x2767 /* rotated floral heart bullet */);
set.addRange(0x2776 /* dingbat negative circled digit one */, 0x2793 /* dingbat negative circled sans-serif number ten */);
set.add(0xFFFC /* object replacement character */);
set.add(0xFFFD /* replacement character */);
return set.toString();
}
async function requiresComplexTextShaping(): Promise<string> {
// This is a rough heuristic: whether we "can render" a script
// actually depends on the properties of the font being used
// and whether differences from the ideal rendering are considered
// semantically significant.
// These blocks cover common scripts that require
// complex text shaping, based on unicode script metadata:
// https://www.unicode.org/repos/cldr/trunk/common/properties/scriptMetadata.txt
// where "Web Rank <= 32" "Shaping Required = YES"
const set = await createSet([
'Bengali',
'Devanagari',
'Gujarati',
'Gurmukhi',
'Kannada',
'Khmer',
'Malayalam',
'Myanmar',
'Oriya',
'Tamil',
'Telugu',
'Tibetan',
'Sinhala',
], []);
return set.toString();
}
fs.writeFileSync('src/util/unicode_properties.g.ts',
`// This file is generated. Edit build/generate-unicode-data.ts, then run \`npm run generate-unicode-data\`.
/**
* Returns whether the fallback fonts specified by the
* \`localIdeographFontFamily\` map option apply to the given codepoint.
*/
export function codePointUsesLocalIdeographFontFamily(codePoint: number): boolean {
return /${await usesLocalIdeographFontFamily()}/gim.test(String.fromCodePoint(codePoint));
}
/**
* Returns whether the given codepoint participates in ideographic line
* breaking.
*/
export function codePointAllowsIdeographicBreaking(codePoint: number): boolean {
return /${await allowsIdeographicBreaking()}/gim.test(String.fromCodePoint(codePoint));
}
/**
* Returns true if the given Unicode codepoint identifies a character with
* upright orientation.
*
* A character has upright orientation if it is drawn upright (unrotated)
* whether the line is oriented horizontally or vertically, even if both
* adjacent characters can be rotated. For example, a Chinese character is
* always drawn upright. An uprightly oriented character causes an adjacent
* “neutral” character to be drawn upright as well.
*/
export function codePointHasUprightVerticalOrientation(codePoint: number): boolean {
return /${await hasUprightVerticalOrientation()}/gim.test(String.fromCodePoint(codePoint));
}
/**
* Returns true if the given Unicode codepoint identifies a character with
* neutral orientation.
*
* A character has neutral orientation if it may be drawn rotated or unrotated
* when the line is oriented vertically, depending on the orientation of the
* adjacent characters. For example, along a vertically oriented line, the
* vulgar fraction ½ is drawn upright among Chinese characters but rotated among
* Latin letters. A neutrally oriented character does not influence whether an
* adjacent character is drawn upright or rotated.
*/
export function codePointHasNeutralVerticalOrientation(codePoint: number): boolean {
return /${await hasNeutralVerticalOrientation()}/gim.test(String.fromCodePoint(codePoint));
}
/**
* Returns whether the give codepoint is likely to require complex text shaping.
*/
export function codePointRequiresComplexTextShaping(codePoint: number): boolean {
return /${await requiresComplexTextShaping()}/gim.test(String.fromCodePoint(codePoint));
}
`);

52
node_modules/maplibre-gl/build/readme.md generated vendored Normal file
View File

@@ -0,0 +1,52 @@
# Build Scripts
This folder holds common build scripts accessed via the various `npm run` commands.
Codegen is executed when calling `npm install` in order to generate all artifacts needed for the build to pass
## Bundling all the code
The bundling process can be split into several steps:
`npm run build-css`
This command will compile the css code and create the css file.
`npm run build-prod` and `npm run build-dev`
These commands will use rollup to bundle the code. This is where the magic happens and uses some files in this folder.
`banner.ts` is used to create a banner at the beginning of the output file
`rollup_plugins.ts` is used to define common plugins for rollup configurations
`rollup_plugin_minify_style_spec.ts` is used to specify the plugin used in style spec bundling
In the `rollup` folder there are some files that are used as linking files as they link to other files for rollup to pick when bundling.
Rollup is generating 3 files throughout the process of bundling:
`index.ts` a file containing all the code that will run in the main thread.
`shared.ts` a file containing all the code shared between the main and worker code.
`worker.ts` a file containing all the code the will run in the worker threads.
These 3 files are then referenced and used by the `bundle_prelude.js` file. It allows loading the web worker code automatically in web workers without any extra effort from someone who would like to use the library, i.e. it simply works.
<hr>
### `npm run codegen`
The `codegen` command runs the following three scripts, to update the corresponding code files based on the `v8.json` style source, and other data files. Contributors should run this command manually when the underlying style data is modified. The generated code files are then committed to the repo.
#### generate-struct-arrays.ts
Generates `data/array_types.ts`, which consists of:
- `StructArrayLayout_*` subclasses, one for each underlying memory layout
- Named exports mapping each conceptual array type (e.g., `CircleLayoutArray`) to its corresponding `StructArrayLayout` class
- Specific named `StructArray` subclasses, when type-specific struct accessors are needed (e.g., `CollisionBoxArray`)
#### generate-style-code.ts
Generates the various `style/style_layer/[layer type]_style_layer_properties.ts` code files based on the content of `v8.json`. These files provide the type signatures for the paint and layout properties for each type of style layer.
<hr>

View File

@@ -0,0 +1,29 @@
/* eslint-disable */
var maplibregl = {};
var modules = {};
function define(moduleName, _dependencies, moduleFactory) {
modules[moduleName] = moduleFactory;
// to get the list of modules see generated dist/maplibre-gl-dev.js file (look for `define(` calls)
if (moduleName !== 'index') {
return;
}
// we assume that when an index module is initializing then other modules are loaded already
var workerBundleString = 'var sharedModule = {}; (' + modules.shared + ')(sharedModule); (' + modules.worker + ')(sharedModule);'
var sharedModule = {};
// the order of arguments of a module factory depends on rollup (it decides who is whose dependency)
// to check the correct order, see dist/maplibre-gl-dev.js file (look for `define(` calls)
// we assume that for our 3 chunks it will generate 3 modules and their order is predefined like the following
modules.shared(sharedModule);
modules.index(maplibregl, sharedModule);
if (typeof window !== 'undefined') {
maplibregl.setWorkerUrl(window.URL.createObjectURL(new Blob([workerBundleString], { type: 'text/javascript' })));
}
return maplibregl;
};

22
node_modules/maplibre-gl/build/rollup/maplibregl.js generated vendored Normal file
View File

@@ -0,0 +1,22 @@
//
// Our custom intro provides a specialized "define()" function, called by the
// AMD modules below, that sets up the worker blob URL and then executes the
// main module, storing its exported value as 'maplibregl'
// The three "chunks" imported here are produced by a first Rollup pass,
// which outputs them as AMD modules.
// Shared dependencies
import '../../staging/maplibregl/shared';
// Worker and its unique dependencies
// When this wrapper function is passed to our custom define() in build/rollup/bundle_prelude.js,
// it gets stringified, together with the shared wrapper (using
// Function.toString()), and the resulting string of code is made into a
// Blob URL that gets used by the main module to create the web workers.
import '../../staging/maplibregl/worker';
// Main module and its dependencies
import '../../staging/maplibregl/index';
export default maplibregl;

68
node_modules/maplibre-gl/build/rollup_plugins.ts generated vendored Normal file
View File

@@ -0,0 +1,68 @@
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
import {type Plugin} from 'rollup';
import json from '@rollup/plugin-json';
import {visualizer} from 'rollup-plugin-visualizer';
const {BUNDLE} = process.env;
const stats = BUNDLE === 'stats';
// Common set of plugins/transformations shared across different rollup
// builds (main maplibre bundle, style-spec package, benchmarks bundle)
export const nodeResolve = resolve({
browser: true,
preferBuiltins: false
});
export const plugins = (production: boolean): Plugin[] => [
json(),
// https://github.com/zaach/jison/issues/351
replace({
preventAssignment: true,
include: /\/jsonlint-lines-primitives\/lib\/jsonlint.js/,
delimiters: ['', ''],
values: {
'_token_stack:': ''
}
}),
production && terser({
compress: {
pure_getters: true,
passes: 3
},
sourceMap: true
}),
nodeResolve,
typescript(),
commonjs({
// global keyword handling causes Webpack compatibility issues, so we disabled it:
// https://github.com/mapbox/mapbox-gl-js/pull/6956
ignoreGlobal: true
}),
// generate bundle stats in multiple formats (treemap, sunburst, etc...)
...(stats ? (['treemap', 'sunburst', 'flamegraph', 'network'] as const).map(template =>
visualizer({
template: template,
title: `gl-js-${template}`,
filename: `staging/${template}.html`,
gzipSize: true,
brotliSize: true,
sourcemap: true,
open: true
})
) : [])
].filter(Boolean) as Plugin[];
export const watchStagingPlugin: Plugin = {
name: 'watch-external',
buildStart() {
this.addWatchFile('staging/maplibregl/index.js');
this.addWatchFile('staging/maplibregl/shared.js');
this.addWatchFile('staging/maplibregl/worker.js');
}
};

116
node_modules/maplibre-gl/dist/LICENSE.txt generated vendored Normal file
View File

@@ -0,0 +1,116 @@
Copyright (c) 2023, MapLibre contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of MapLibre GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from mapbox-gl-js v1.13 and earlier
Version v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license
Copyright (c) 2020, Mapbox
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Mapbox GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains code from glfx.js
Copyright (C) 2011 by Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Contains a portion of d3-color https://github.com/d3/d3-color
Copyright 2010-2016 Mike Bostock
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the author nor the names of contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

75323
node_modules/maplibre-gl/dist/maplibre-gl-csp-dev.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
node_modules/maplibre-gl/dist/maplibre-gl-csp.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/maplibre-gl/dist/maplibre-gl-csp.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

76729
node_modules/maplibre-gl/dist/maplibre-gl-dev.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/maplibre-gl/dist/maplibre-gl-dev.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/maplibre-gl/dist/maplibre-gl.css generated vendored Normal file

File diff suppressed because one or more lines are too long

15084
node_modules/maplibre-gl/dist/maplibre-gl.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

59
node_modules/maplibre-gl/dist/maplibre-gl.js generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/maplibre-gl/dist/maplibre-gl.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

1
node_modules/maplibre-gl/dist/package.json generated vendored Normal file
View File

@@ -0,0 +1 @@
{"name":"maplibre-gl","type":"commonjs","deprecated":"Please install maplibre-gl from parent directory instead"}

185
node_modules/maplibre-gl/package.json generated vendored Normal file
View File

@@ -0,0 +1,185 @@
{
"name": "maplibre-gl",
"description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
"version": "5.22.0",
"main": "dist/maplibre-gl.js",
"style": "dist/maplibre-gl.css",
"license": "BSD-3-Clause",
"homepage": "https://maplibre.org/",
"funding": "https://github.com/maplibre/maplibre-gl-js?sponsor=1",
"bugs": {
"url": "https://github.com/maplibre/maplibre-gl-js/issues/"
},
"repository": {
"type": "git",
"url": "https://github.com/maplibre/maplibre-gl-js"
},
"types": "dist/maplibre-gl.d.ts",
"type": "module",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/point-geometry": "^1.1.0",
"@mapbox/tiny-sdf": "^2.0.7",
"@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^2.0.4",
"@mapbox/whoots-js": "^3.1.0",
"@maplibre/geojson-vt": "^6.0.4",
"@maplibre/maplibre-gl-style-spec": "^24.8.1",
"@maplibre/mlt": "^1.1.8",
"@maplibre/vt-pbf": "^4.3.0",
"@types/geojson": "^7946.0.16",
"earcut": "^3.0.2",
"gl-matrix": "^3.4.4",
"kdbush": "^4.0.2",
"murmurhash-js": "^1.0.0",
"pbf": "^4.0.1",
"potpack": "^2.1.0",
"quickselect": "^3.0.0",
"tinyqueue": "^3.0.0"
},
"devDependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
"@mapbox/mvt-fixtures": "^3.10.0",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.1.4",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/benchmark": "^2.1.5",
"@types/d3": "^7.4.3",
"@types/earcut": "^3.0.0",
"@types/eslint": "^9.6.1",
"@types/gl": "^6.0.5",
"@types/jsdom": "^28.0.1",
"@types/minimist": "^1.2.5",
"@types/murmurhash-js": "^1.0.7",
"@types/nise": "^1.4.5",
"@types/node": "^25.5.0",
"@types/offscreencanvas": "^2019.7.3",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.5",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/request": "^2.48.13",
"@types/shuffle-seed": "^1.1.3",
"@types/window-or-global": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.57.2",
"@unicode/unicode-17.0.0": "^1.6.16",
"@vitest/coverage-v8": "4.1.2",
"@vitest/eslint-plugin": "^1.6.13",
"@vitest/ui": "4.1.2",
"address": "^2.0.3",
"autoprefixer": "^10.4.27",
"benchmark": "^2.1.4",
"canvas": "^3.2.3",
"cspell": "^9.7.0",
"cssnano": "^7.1.4",
"d3": "^7.9.0",
"d3-queue": "^3.0.7",
"devtools-protocol": "^0.0.1604597",
"diff": "^8.0.4",
"dts-bundle-generator": "^9.5.1",
"esbuild": "^0.27.4",
"eslint": "^9.39.2",
"eslint-plugin-html": "^8.1.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-tsdoc": "0.5.2",
"expect": "^30.3.0",
"glob": "^13.0.6",
"globals": "^17.4.0",
"is-builtin-module": "^5.0.0",
"jsdom": "^29.0.1",
"junit-report-builder": "^5.1.1",
"minimist": "^1.2.8",
"mock-geolocation": "^1.0.11",
"monocart-coverage-reports": "^2.12.9",
"nise": "^6.1.4",
"npm-font-open-sans": "^1.1.0",
"npm-run-all": "^4.1.5",
"pdf-merger-js": "^5.1.2",
"pixelmatch": "^7.1.0",
"pngjs": "^7.0.0",
"postcss": "^8.5.8",
"postcss-cli": "^11.0.1",
"postcss-inline-svg": "^6.0.0",
"pretty-bytes": "^7.1.0",
"puppeteer": "^24.40.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"regenerate": "^1.4.2",
"rollup": "^4.60.1",
"rollup-plugin-sourcemaps2": "^0.5.6",
"rollup-plugin-visualizer": "^7.0.1",
"rw": "^1.3.3",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shuffle-seed": "^1.1.6",
"st": "^3.0.3",
"stylelint": "^16.26.1",
"stylelint-config-standard": "^39.0.1",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typedoc": "^0.28.18",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "^6.0.2",
"vitest": "4.1.2",
"vitest-webgl-canvas-mock": "^1.1.0"
},
"scripts": {
"generate-dist-package": "node --no-warnings --loader ts-node/esm build/generate-dist-package.js",
"generate-unicode-data": "node --no-warnings --loader ts-node/esm build/generate-unicode-data.ts",
"generate-shaders": "node --no-warnings --loader ts-node/esm build/generate-shaders.ts",
"generate-struct-arrays": "node --no-warnings --loader ts-node/esm build/generate-struct-arrays.ts",
"generate-style-code": "node --no-warnings --loader ts-node/esm build/generate-style-code.ts",
"generate-typings": "dts-bundle-generator --export-referenced-types=false --umd-module-name=maplibregl -o ./dist/maplibre-gl.d.ts ./src/index.ts",
"generate-docs": "typedoc && node --no-warnings --loader ts-node/esm build/generate-docs.ts",
"generate-images": "node --no-warnings --loader ts-node/esm build/generate-doc-images.ts",
"build-dist": "npm run build-css && npm run generate-unicode-data && npm run generate-typings && npm run generate-shaders && npm run build-dev && npm run build-csp-dev && npm run build-prod && npm run build-csp",
"build-dev": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:dev",
"watch-dev": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:dev --watch",
"build-prod": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:production",
"build-csp": "rollup --configPlugin @rollup/plugin-typescript -c rollup.config.csp.ts --environment BUILD:production",
"build-csp-dev": "rollup --configPlugin @rollup/plugin-typescript -c rollup.config.csp.ts --environment BUILD:dev",
"build-css": "postcss -o dist/maplibre-gl.css src/css/maplibre-gl.css",
"watch-css": "postcss --watch -o dist/maplibre-gl.css src/css/maplibre-gl.css",
"build-benchmarks": "npm run build-dev && rollup --configPlugin @rollup/plugin-typescript -c test/bench/rollup_config_benchmarks.ts",
"watch-benchmarks": "rollup --configPlugin @rollup/plugin-typescript -c test/bench/rollup_config_benchmarks.ts --watch",
"bundle-stats": "rollup --configPlugin @rollup/plugin-typescript -c --environment BUILD:production,BUNDLE:stats",
"spellcheck": "cspell",
"docs": "npm run generate-docs && docker run --rm -v ${PWD}:/docs zensical/zensical build",
"start-server": "st --no-cache -H localhost --port 9966 .",
"start-docs": "docker run --rm -it -p 8000:8000 -v ${PWD}:/docs zensical/zensical serve --open --dev-addr=0.0.0.0:8000",
"start": "run-p watch-css watch-dev start-server",
"start-bench": "run-p watch-css watch-benchmarks start-server",
"lint": "eslint",
"lint-css": "stylelint **/*.css --fix -f verbose",
"test": "run-p lint lint-css test-render test-unit test-integration test-build",
"test-unit": "vitest run --config vitest.config.unit.ts",
"test-unit-ci": "vitest run --config vitest.config.unit.ts --coverage",
"test-integration": "vitest run --config vitest.config.integration.ts",
"test-integration-ci": "vitest run --config vitest.config.integration.ts --coverage",
"test-build": "vitest run --config vitest.config.build.ts",
"test-build-ci": "vitest run --config vitest.config.build.ts --coverage",
"test-watch-roots": "vitest --config vitest.config.unit.ts --watch",
"test-render": "node --no-warnings --loader ts-node/esm test/integration/render/run_render_tests.ts",
"codegen": "run-p --print-label generate-dist-package generate-style-code generate-unicode-data generate-struct-arrays generate-shaders && npm run generate-typings",
"benchmark": "node --no-warnings --loader ts-node/esm test/bench/run-benchmarks.ts",
"gl-stats": "node --no-warnings --loader ts-node/esm test/bench/gl-stats.ts",
"prepare": "npm run codegen",
"typecheck": "tsc --noEmit && tsc --project tsconfig.dist.json",
"tsnode": "node --experimental-loader=ts-node/esm --no-warnings"
},
"files": [
"build/",
"dist/*",
"src/"
],
"engines": {
"npm": ">=8.1.0",
"node": ">=16.14.0"
}
}

907
node_modules/maplibre-gl/src/css/maplibre-gl.css generated vendored Normal file
View File

@@ -0,0 +1,907 @@
.maplibregl-map {
font: 12px/20px "Helvetica Neue", Arial, Helvetica, sans-serif;
overflow: hidden;
position: relative;
-webkit-tap-highlight-color: rgb(0, 0, 0, 0);
}
.maplibregl-canvas {
position: absolute;
left: 0;
top: 0;
}
.maplibregl-map:fullscreen {
width: 100%;
height: 100%;
}
.maplibregl-ctrl-group button.maplibregl-ctrl-compass {
touch-action: none;
}
.maplibregl-canvas-container.maplibregl-interactive,
.maplibregl-ctrl-group button.maplibregl-ctrl-compass {
cursor: grab;
user-select: none;
}
.maplibregl-canvas-container.maplibregl-interactive.maplibregl-track-pointer {
cursor: pointer;
}
.maplibregl-canvas-container.maplibregl-interactive:active,
.maplibregl-ctrl-group button.maplibregl-ctrl-compass:active {
cursor: grabbing;
}
.maplibregl-canvas-container.maplibregl-touch-zoom-rotate,
.maplibregl-canvas-container.maplibregl-touch-zoom-rotate .maplibregl-canvas {
touch-action: pan-x pan-y;
}
.maplibregl-canvas-container.maplibregl-touch-drag-pan,
.maplibregl-canvas-container.maplibregl-touch-drag-pan .maplibregl-canvas {
touch-action: pinch-zoom;
}
.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan,
.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan .maplibregl-canvas {
touch-action: none;
}
.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures,
.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures .maplibregl-canvas {
touch-action: pan-x pan-y;
}
.maplibregl-ctrl-top-left,
.maplibregl-ctrl-top-right,
.maplibregl-ctrl-bottom-left,
.maplibregl-ctrl-bottom-right {
position: absolute;
pointer-events: none;
z-index: 2;
}
.maplibregl-ctrl-top-left {
top: 0;
left: 0;
}
.maplibregl-ctrl-top-right {
top: 0;
right: 0;
}
.maplibregl-ctrl-bottom-left {
bottom: 0;
left: 0;
}
.maplibregl-ctrl-bottom-right {
right: 0;
bottom: 0;
}
.maplibregl-ctrl {
clear: both;
pointer-events: auto;
/* workaround for a Safari bug https://github.com/mapbox/mapbox-gl-js/issues/8185 */
transform: translate(0, 0);
}
.maplibregl-ctrl-top-left .maplibregl-ctrl {
margin: 10px 0 0 10px;
float: left;
}
.maplibregl-ctrl-top-right .maplibregl-ctrl {
margin: 10px 10px 0 0;
float: right;
}
.maplibregl-ctrl-bottom-left .maplibregl-ctrl {
margin: 0 0 10px 10px;
float: left;
}
.maplibregl-ctrl-bottom-right .maplibregl-ctrl {
margin: 0 10px 10px 0;
float: right;
}
.maplibregl-ctrl-group {
border-radius: 4px;
background: #fff;
}
.maplibregl-ctrl-group:not(:empty) {
box-shadow: 0 0 0 2px rgb(0, 0, 0, 0.1);
}
@media (forced-colors: active) {
.maplibregl-ctrl-group:not(:empty) {
box-shadow: 0 0 0 2px ButtonText;
}
}
.maplibregl-ctrl-group button {
width: 29px;
height: 29px;
display: block;
padding: 0;
outline: none;
border: 0;
box-sizing: border-box;
background-color: transparent;
cursor: pointer;
}
.maplibregl-ctrl-group button + button {
border-top: 1px solid #ddd;
}
.maplibregl-ctrl button .maplibregl-ctrl-icon {
display: block;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center center;
}
@media (forced-colors: active) {
.maplibregl-ctrl-icon {
background-color: transparent;
}
.maplibregl-ctrl-group button + button {
border-top: 1px solid ButtonText;
}
}
/* https://bugzilla.mozilla.org/show_bug.cgi?id=140562 */
.maplibregl-ctrl button::-moz-focus-inner {
border: 0;
padding: 0;
}
.maplibregl-ctrl-attrib-button:focus,
.maplibregl-ctrl-group button:focus {
box-shadow: 0 0 2px 2px rgb(0, 150, 255, 1);
}
.maplibregl-ctrl button:disabled {
cursor: not-allowed;
}
.maplibregl-ctrl button:disabled .maplibregl-ctrl-icon {
opacity: 0.25;
}
@media (hover: hover) {
.maplibregl-ctrl button:not(:disabled):hover {
background-color: rgb(0, 0, 0, 0.05);
}
}
.maplibregl-ctrl button:not(:disabled):active {
background-color: rgb(0, 0, 0, 0.05);
}
.maplibregl-ctrl-group button:focus:focus-visible {
box-shadow: 0 0 2px 2px rgb(0, 150, 255, 1);
}
.maplibregl-ctrl-group button:focus:not(:focus-visible) {
box-shadow: none;
}
.maplibregl-ctrl-group button:focus:first-child {
border-radius: 4px 4px 0 0;
}
.maplibregl-ctrl-group button:focus:last-child {
border-radius: 0 0 4px 4px;
}
.maplibregl-ctrl-group button:focus:only-child {
border-radius: inherit;
}
.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-zoom-out.svg", fill: #333);
}
.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-zoom-in.svg", fill: #333);
}
@media (forced-colors: active) {
.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-zoom-out.svg", fill: #fff);
}
.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-zoom-in.svg", fill: #fff);
}
}
@media (forced-colors: active) and (prefers-color-scheme: light) {
.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-zoom-out.svg", fill: #000);
}
.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-zoom-in.svg", fill: #000);
}
}
.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-fullscreen.svg", fill: #333);
}
.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-shrink.svg");
}
@media (forced-colors: active) {
.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-fullscreen.svg", fill: #fff);
}
.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-shrink.svg", fill: #fff);
}
}
@media (forced-colors: active) and (prefers-color-scheme: light) {
.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-fullscreen.svg", fill: #000);
}
.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-shrink.svg", fill: #000);
}
}
.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-compass.svg", fill: #333);
}
@media (forced-colors: active) {
.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon {
@svg-load ctrl-compass-white url("svg/maplibregl-ctrl-compass.svg") {
fill: #fff;
#south { fill: #999; }
}
background-image: svg-inline(ctrl-compass-white);
}
}
@media (forced-colors: active) and (prefers-color-scheme: light) {
.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon {
background-image: svg-load("svg/maplibregl-ctrl-compass.svg", fill: #000);
}
}
@svg-load ctrl-globe url("svg/maplibregl-ctrl-globe.svg") {
stroke: #333;
}
@svg-load ctrl-globe-enabled url("svg/maplibregl-ctrl-globe.svg") {
stroke: #33b5e5;
}
.maplibregl-ctrl button.maplibregl-ctrl-globe .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-globe);
}
.maplibregl-ctrl button.maplibregl-ctrl-globe-enabled .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-globe-enabled);
}
@svg-load ctrl-terrain url("svg/maplibregl-ctrl-terrain.svg") {
fill: #333;
}
@svg-load ctrl-terrain-enabled url("svg/maplibregl-ctrl-terrain.svg") {
fill: #33b5e5;
}
.maplibregl-ctrl button.maplibregl-ctrl-terrain .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-terrain);
}
.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-terrain-enabled);
}
@svg-load ctrl-geolocate url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #333;
#stroke { display: none; }
}
@svg-load ctrl-geolocate-white url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #fff;
#stroke { display: none; }
}
@svg-load ctrl-geolocate-black url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #000;
#stroke { display: none; }
}
@svg-load ctrl-geolocate-disabled url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #aaa;
#stroke { fill: #f00; }
}
@svg-load ctrl-geolocate-disabled-white url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #999;
#stroke { fill: #f00; }
}
@svg-load ctrl-geolocate-disabled-black url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #666;
#stroke { fill: #f00; }
}
@svg-load ctrl-geolocate-active url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #33b5e5;
#stroke { display: none; }
}
@svg-load ctrl-geolocate-active-error url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #e58978;
#stroke { display: none; }
}
@svg-load ctrl-geolocate-background url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #33b5e5;
#stroke { display: none; }
#dot { display: none; }
}
@svg-load ctrl-geolocate-background-error url("svg/maplibregl-ctrl-geolocate.svg") {
fill: #e54e33;
#stroke { display: none; }
#dot { display: none; }
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-disabled);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-active);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-active-error);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-background);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-background-error);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon {
animation: maplibregl-spin 2s infinite linear;
}
@media (forced-colors: active) {
.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-white);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-disabled-white);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-active);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-active-error);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-background);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-background-error);
}
}
@media (forced-colors: active) and (prefers-color-scheme: light) {
.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-black);
}
.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon {
background-image: svg-inline(ctrl-geolocate-disabled-black);
}
}
@keyframes maplibregl-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
a.maplibregl-ctrl-logo {
width: 88px;
height: 23px;
margin: 0 0 -4px -4px;
display: block;
background-repeat: no-repeat;
cursor: pointer;
overflow: hidden;
background-image: svg-load("svg/maplibregl-ctrl-logo.svg");
}
a.maplibregl-ctrl-logo.maplibregl-compact {
width: 14px;
}
@media (forced-colors: active) {
a.maplibregl-ctrl-logo {
@svg-load ctrl-logo-white url("svg/maplibregl-ctrl-logo.svg") {
#outline { opacity: 1; }
#fill { opacity: 1; }
}
background-color: transparent;
background-image: svg-inline(ctrl-logo-white);
}
}
@media (forced-colors: active) and (prefers-color-scheme: light) {
a.maplibregl-ctrl-logo {
@svg-load ctrl-logo-black url("svg/maplibregl-ctrl-logo.svg") {
#outline { opacity: 1; fill: #fff; stroke: #fff; }
#fill { opacity: 1; fill: #000; }
}
background-image: svg-inline(ctrl-logo-black);
}
}
.maplibregl-ctrl.maplibregl-ctrl-attrib {
padding: 0 5px;
background-color: rgb(255, 255, 255, 0.5);
margin: 0;
}
@media screen {
.maplibregl-ctrl-attrib.maplibregl-compact {
min-height: 20px;
padding: 2px 24px 2px 0;
margin: 10px;
position: relative;
background-color: #fff;
color: #000;
border-radius: 12px;
box-sizing: content-box;
}
.maplibregl-ctrl-attrib.maplibregl-compact-show {
padding: 2px 28px 2px 8px;
visibility: visible;
}
.maplibregl-ctrl-top-left > .maplibregl-ctrl-attrib.maplibregl-compact-show,
.maplibregl-ctrl-bottom-left > .maplibregl-ctrl-attrib.maplibregl-compact-show {
padding: 2px 8px 2px 28px;
border-radius: 12px;
}
.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-inner {
display: none;
}
.maplibregl-ctrl-attrib-button {
display: none;
cursor: pointer;
position: absolute;
background-image: svg-load("svg/maplibregl-ctrl-attrib.svg");
background-color: rgb(255, 255, 255, 0.5);
width: 24px;
height: 24px;
box-sizing: border-box;
border-radius: 12px;
outline: none;
top: 0;
right: 0;
border: 0;
}
.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button {
appearance: none;
list-style: none;
}
.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button::-webkit-details-marker {
display: none;
}
.maplibregl-ctrl-top-left .maplibregl-ctrl-attrib-button,
.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib-button {
left: 0;
}
.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button,
.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-inner {
display: block;
}
.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button {
background-color: rgb(0, 0, 0, 0.05);
}
.maplibregl-ctrl-bottom-right > .maplibregl-ctrl-attrib.maplibregl-compact::after {
bottom: 0;
right: 0;
}
.maplibregl-ctrl-top-right > .maplibregl-ctrl-attrib.maplibregl-compact::after {
top: 0;
right: 0;
}
.maplibregl-ctrl-top-left > .maplibregl-ctrl-attrib.maplibregl-compact::after {
top: 0;
left: 0;
}
.maplibregl-ctrl-bottom-left > .maplibregl-ctrl-attrib.maplibregl-compact::after {
bottom: 0;
left: 0;
}
}
@media screen and (forced-colors: active) {
.maplibregl-ctrl-attrib.maplibregl-compact::after {
background-image: svg-load("svg/maplibregl-ctrl-attrib.svg", fill=#fff);
}
}
@media screen and (forced-colors: active) and (prefers-color-scheme: light) {
.maplibregl-ctrl-attrib.maplibregl-compact::after {
background-image: svg-load("svg/maplibregl-ctrl-attrib.svg");
}
}
.maplibregl-ctrl-attrib a {
color: rgb(0, 0, 0, 0.75);
text-decoration: none;
}
.maplibregl-ctrl-attrib a:hover {
color: inherit;
text-decoration: underline;
}
.maplibregl-attrib-empty {
display: none;
}
.maplibregl-ctrl-scale {
background-color: rgb(255, 255, 255, 0.75);
font-size: 10px;
white-space: nowrap;
border-width: medium 2px 2px;
border-style: none solid solid;
border-color: #333;
padding: 0 5px;
color: #333;
box-sizing: border-box;
}
.maplibregl-popup {
position: absolute;
top: 0;
left: 0;
display: flex;
will-change: transform;
pointer-events: none;
}
.maplibregl-popup-anchor-top,
.maplibregl-popup-anchor-top-left,
.maplibregl-popup-anchor-top-right {
flex-direction: column;
}
.maplibregl-popup-anchor-bottom,
.maplibregl-popup-anchor-bottom-left,
.maplibregl-popup-anchor-bottom-right {
flex-direction: column-reverse;
}
.maplibregl-popup-anchor-left {
flex-direction: row;
}
.maplibregl-popup-anchor-right {
flex-direction: row-reverse;
}
.maplibregl-popup-tip {
width: 0;
height: 0;
border: 10px solid transparent;
z-index: 1;
}
.maplibregl-popup-anchor-top .maplibregl-popup-tip {
align-self: center;
border-top: none;
border-bottom-color: #fff;
}
.maplibregl-popup-anchor-top-left .maplibregl-popup-tip {
align-self: flex-start;
border-top: none;
border-left: none;
border-bottom-color: #fff;
}
.maplibregl-popup-anchor-top-right .maplibregl-popup-tip {
align-self: flex-end;
border-top: none;
border-right: none;
border-bottom-color: #fff;
}
.maplibregl-popup-anchor-bottom .maplibregl-popup-tip {
align-self: center;
border-bottom: none;
border-top-color: #fff;
}
.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip {
align-self: flex-start;
border-bottom: none;
border-left: none;
border-top-color: #fff;
}
.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {
align-self: flex-end;
border-bottom: none;
border-right: none;
border-top-color: #fff;
}
.maplibregl-popup-anchor-left .maplibregl-popup-tip {
align-self: center;
border-left: none;
border-right-color: #fff;
}
.maplibregl-popup-anchor-right .maplibregl-popup-tip {
align-self: center;
border-right: none;
border-left-color: #fff;
}
[dir="rtl"] .maplibregl-popup-anchor-left {
flex-direction: row-reverse;
}
[dir="rtl"] .maplibregl-popup-anchor-right {
flex-direction: row;
}
[dir="rtl"] .maplibregl-popup-anchor-top-left .maplibregl-popup-tip {
align-self: flex-end;
}
[dir="rtl"] .maplibregl-popup-anchor-top-right .maplibregl-popup-tip {
align-self: flex-start;
}
[dir="rtl"] .maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip {
align-self: flex-end;
}
[dir="rtl"] .maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip {
align-self: flex-start;
}
.maplibregl-popup-close-button {
position: absolute;
right: 0;
top: 0;
border: 0;
border-radius: 0 3px 0 0;
cursor: pointer;
background-color: transparent;
}
.maplibregl-popup-close-button:hover {
background-color: rgb(0, 0, 0, 0.05);
}
.maplibregl-popup-content {
position: relative;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgb(0, 0, 0, 0.1);
padding: 15px 10px;
pointer-events: auto;
}
.maplibregl-popup-anchor-top-left .maplibregl-popup-content {
border-top-left-radius: 0;
}
.maplibregl-popup-anchor-top-right .maplibregl-popup-content {
border-top-right-radius: 0;
}
.maplibregl-popup-anchor-bottom-left .maplibregl-popup-content {
border-bottom-left-radius: 0;
}
.maplibregl-popup-anchor-bottom-right .maplibregl-popup-content {
border-bottom-right-radius: 0;
}
.maplibregl-popup-track-pointer {
display: none;
}
.maplibregl-popup-track-pointer * {
pointer-events: none;
user-select: none;
}
.maplibregl-map:hover .maplibregl-popup-track-pointer {
display: flex;
}
.maplibregl-map:active .maplibregl-popup-track-pointer {
display: none;
}
.maplibregl-marker {
position: absolute;
top: 0;
left: 0;
will-change: transform;
transition: opacity 0.2s;
}
.maplibregl-user-location-dot {
background-color: #1da1f2;
width: 15px;
height: 15px;
border-radius: 50%;
}
.maplibregl-user-location-dot::before {
background-color: #1da1f2;
content: "";
width: 15px;
height: 15px;
border-radius: 50%;
position: absolute;
animation: maplibregl-user-location-dot-pulse 2s infinite;
}
.maplibregl-user-location-dot::after {
border-radius: 50%;
border: 2px solid #fff;
content: "";
height: 19px;
left: -2px;
position: absolute;
top: -2px;
width: 19px;
box-sizing: border-box;
box-shadow: 0 0 3px rgb(0, 0, 0, 0.35);
}
@media (prefers-reduced-motion: reduce) {
.maplibregl-user-location-dot::before {
animation: none;
}
}
@keyframes maplibregl-user-location-dot-pulse {
0% { transform: scale(1); opacity: 1; }
70% { transform: scale(3); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
.maplibregl-user-location-dot-stale {
background-color: #aaa;
}
.maplibregl-user-location-dot-stale::after {
display: none;
}
.maplibregl-user-location-accuracy-circle {
background-color: #1da1f233;
width: 1px;
height: 1px;
border-radius: 100%;
}
.maplibregl-crosshair,
.maplibregl-crosshair .maplibregl-interactive,
.maplibregl-crosshair .maplibregl-interactive:active {
cursor: crosshair;
}
.maplibregl-boxzoom {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
background: #fff;
border: 2px dotted #202020;
opacity: 0.5;
}
.maplibregl-cooperative-gesture-screen {
background: rgb(0, 0, 0, 0.4);
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
color: white;
padding: 1rem;
font-size: 1.4em;
line-height: 1.2;
opacity: 0;
pointer-events: none;
transition: opacity 1s ease 1s;
z-index: 99999;
}
.maplibregl-cooperative-gesture-screen.maplibregl-show {
opacity: 1;
transition: opacity 0.05s;
}
.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message {
display: none;
}
@media (hover: none), (pointer: coarse) {
.maplibregl-cooperative-gesture-screen .maplibregl-desktop-message {
display: none;
}
.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message {
display: block;
}
}
.maplibregl-pseudo-fullscreen {
position: fixed !important;
width: 100% !important;
height: 100% !important;
top: 0 !important;
left: 0 !important;
z-index: 99999;
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill-rule="evenodd" viewBox="0 0 20 20">
<path d="M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 29 29">
<path d="m10.5 14 4-8 4 8z"/>
<path fill="#ccc" d="m10.5 16 4 8 4-8z"/>
</svg>

After

Width:  |  Height:  |  Size: 171 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 29 29">
<path d="M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z"/>
</svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 20 20">
<path d="M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7"/>
<circle id="dot" cx="10" cy="10" r="2"/>
<path id="stroke" d="m14 5 1 1-9 9-1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="none" viewBox="0 0 22 22">
<circle cx="11" cy="11" r="8.5" />
<path d="M17.5 11c0 4.819-3.02 8.5-6.5 8.5S4.5 15.819 4.5 11 7.52 2.5 11 2.5s6.5 3.681 6.5 8.5Z"/>
<path d="M13.5 11c0 2.447-.331 4.64-.853 6.206-.262.785-.562 1.384-.872 1.777-.314.399-.58.517-.775.517-.195 0-.461-.118-.775-.517-.31-.393-.61-.992-.872-1.777C8.831 15.64 8.5 13.446 8.5 11c0-2.447.331-4.64.853-6.206.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517.195 0 .461.118.775.517.31.393.61.992.872 1.777.522 1.565.853 3.76.853 6.206Z"/>
<path d="M11 7.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.333 1.333 0 0 1-.224-.138c.047-.038.12-.085.224-.138.299-.151.763-.302 1.379-.434C7.378 5.666 9.091 5.5 11 5.5c1.909 0 3.622.166 4.845.428.616.132 1.08.283 1.379.434.105.053.177.1.224.138-.047.038-.12.085-.224.138-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428ZM4.486 6.436ZM11 16.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.33 1.33 0 0 1-.224-.138 1.33 1.33 0 0 1 .224-.138c.299-.151.763-.302 1.379-.434C7.378 14.666 9.091 14.5 11 14.5c1.909 0 3.622.166 4.845.428.616.132 1.08.283 1.379.434.105.053.177.1.224.138a1.33 1.33 0 0 1-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm-6.514-1.064ZM11 12.5c-2.46 0-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.465 1.465 0 0 1-.39-.272.293.293 0 0 1-.047-.064.293.293 0 0 1 .048-.064c.066-.073.189-.167.389-.272.399-.21 1.009-.413 1.805-.59C6.328 9.722 8.54 9.5 11 9.5s4.672.222 6.256.574c.795.177 1.405.38 1.804.59.2.105.323.2.39.272a.33.33 0 0 1 .047.064.293.293 0 0 1-.048.064 1.435 1.435 0 0 1-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002-.002Zm0 .018v.002-.002Zm17.002.002v-.002.002Zm0-.018v-.002.002Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 29 29">
<path d="M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z"/>
</svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22">
<path d="m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 29 29">
<path d="M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 29 29">
<path d="M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

1170
node_modules/maplibre-gl/src/data/array_types.g.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

130
node_modules/maplibre-gl/src/data/bucket.ts generated vendored Normal file
View File

@@ -0,0 +1,130 @@
import type {CollisionBoxArray} from './array_types.g';
import type {Style} from '../style/style';
import type {TypedStyleLayer} from '../style/style_layer/typed_style_layer';
import type {FeatureIndex} from './feature_index';
import type {Context} from '../gl/context';
import type {FeatureStates} from '../source/source_state';
import type {ImagePosition} from '../render/image_atlas';
import type {CanonicalTileID} from '../tile/tile_id';
import type Point from '@mapbox/point-geometry';
import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings';
import type {DashEntry} from '../render/line_atlas';
import type {Feature as StyleFeature} from '@maplibre/maplibre-gl-style-spec';
import type {VectorTileFeatureLike, VectorTileLayerLike} from '@maplibre/vt-pbf';
export type BucketParameters<Layer extends TypedStyleLayer> = {
index: number;
layers: Array<Layer>;
zoom: number;
pixelRatio: number;
overscaling: number;
collisionBoxArray: CollisionBoxArray;
sourceLayerIndex: number;
sourceID: string;
};
export type PopulateParameters = {
featureIndex: FeatureIndex;
iconDependencies: {};
patternDependencies: {};
glyphDependencies: {};
dashDependencies: Record<string, {round: boolean; dasharray: Array<number>}>;
availableImages: Array<string>;
subdivisionGranularity: SubdivisionGranularitySetting;
};
export type IndexedFeature = {
feature: VectorTileFeatureLike;
id: number | string;
index: number;
sourceLayerIndex: number;
};
export type BucketFeature = {
index: number;
sourceLayerIndex: number;
geometry: Array<Array<Point>>;
properties: any;
type: 0 | 1 | 2 | 3;
id?: any;
readonly patterns: {
[_: string]: {
'min': string;
'mid': string;
'max': string;
};
};
readonly dashes?: NonNullable<StyleFeature['dashes']>;
sortKey?: number;
};
/**
* @hidden
* The `Bucket` interface is the single point of knowledge about turning vector
* tiles into WebGL buffers.
*
* `Bucket` is an abstract interface. An implementation exists for each style layer type.
* Create a bucket via the `StyleLayer.createBucket` method.
*
* The concrete bucket types, using layout options from the style layer,
* transform feature geometries into vertex and index data for use by the
* vertex shader. They also (via `ProgramConfiguration`) use feature
* properties and the zoom level to populate the attributes needed for
* data-driven styling.
*
* Buckets are designed to be built on a worker thread and then serialized and
* transferred back to the main thread for rendering. On the worker side, a
* bucket's vertex, index, and attribute data is stored in `bucket.arrays: ArrayGroup`.
* When a bucket's data is serialized and sent back to the main thread,
* is gets deserialized (using `new Bucket(serializedBucketData)`, with
* the array data now stored in `bucket.buffers: BufferGroup`. BufferGroups
* hold the same data as ArrayGroups, but are tuned for consumption by WebGL.
*/
export interface Bucket {
layerIds: Array<string>;
hasDependencies: boolean;
readonly layers: Array<any>;
readonly stateDependentLayers: Array<any>;
readonly stateDependentLayerIds: Array<string>;
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID): void;
update(states: FeatureStates, vtLayer: VectorTileLayerLike, imagePositions: {[_: string]: ImagePosition}, dashPositions: Record<string, DashEntry>): void;
isEmpty(): boolean;
upload(context: Context): void;
uploadPending(): boolean;
/**
* Release the WebGL resources associated with the buffers. Note that because
* buckets are shared between layers having the same layout properties, they
* must be destroyed in groups (all buckets for a tile, or all symbol buckets).
*/
destroy(): void;
}
export function deserialize(input: Array<Bucket>, style: Style): {[_: string]: Bucket} {
const output = {};
// Guard against the case where the map's style has been set to null while
// this bucket has been parsing.
if (!style) return output;
for (const bucket of input) {
const layers = bucket.layerIds
.map((id) => style.getLayer(id))
.filter(Boolean);
if (layers.length === 0) {
continue;
}
// look up StyleLayer objects from layer ids (since we don't
// want to waste time serializing/copying them from the worker)
(bucket as any).layers = layers;
if (bucket.stateDependentLayerIds) {
(bucket as any).stateDependentLayers = bucket.stateDependentLayerIds.map((lId) => layers.filter((l) => l.id === lId)[0]);
}
for (const layer of layers) {
output[layer.id] = bucket;
}
}
return output;
}

View File

@@ -0,0 +1,8 @@
import {createLayout} from '../../util/struct_array';
const layout = createLayout([
{name: 'a_pos', components: 2, type: 'Int16'}
], 4);
export default layout;
export const {members, size, alignment} = layout;

View File

@@ -0,0 +1,239 @@
import {CircleLayoutArray} from '../array_types.g';
import {members as layoutAttributes} from './circle_attributes';
import {SegmentVector} from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray} from '../index_array_type';
import {loadGeometry} from '../load_geometry';
import {toEvaluationFeature} from '../evaluation_feature';
import {EXTENT} from '../extent';
import {register} from '../../util/web_worker_transfer';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import type {CanonicalTileID} from '../../tile/tile_id';
import type {
Bucket,
BucketParameters,
BucketFeature,
IndexedFeature,
PopulateParameters
} from '../bucket';
import type {CircleStyleLayer} from '../../style/style_layer/circle_style_layer';
import type {HeatmapStyleLayer} from '../../style/style_layer/heatmap_style_layer';
import type {Context} from '../../gl/context';
import type {IndexBuffer} from '../../gl/index_buffer';
import type {VertexBuffer} from '../../gl/vertex_buffer';
import type Point from '@mapbox/point-geometry';
import type {FeatureStates} from '../../source/source_state';
import type {ImagePosition} from '../../render/image_atlas';
import {type CircleGranularity} from '../../render/subdivision_granularity_settings';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
const VERTEX_MIN_VALUE = -32768; // -(2^15)
// Extrude is in range 0..7, which will be mapped to -1..1 in the shader.
function addCircleVertex(layoutVertexArray, x, y, extrudeX, extrudeY) {
// We pack circle position and extrude into range 0..65535, but vertices are stored as *signed* 16-bit integers, so we need to offset the number by 2^15.
layoutVertexArray.emplaceBack(
VERTEX_MIN_VALUE + (x * 8) + extrudeX,
VERTEX_MIN_VALUE + (y * 8) + extrudeY);
}
/**
* @internal
* Circles are represented by two triangles.
*
* Each corner has a pos that is the center of the circle and an extrusion
* vector that is where it points.
*/
export class CircleBucket<Layer extends CircleStyleLayer | HeatmapStyleLayer> implements Bucket {
index: number;
zoom: number;
overscaling: number;
layerIds: Array<string>;
layers: Array<Layer>;
stateDependentLayers: Array<Layer>;
stateDependentLayerIds: Array<string>;
layoutVertexArray: CircleLayoutArray;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<Layer>;
segments: SegmentVector;
uploaded: boolean;
constructor(options: BucketParameters<Layer>) {
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.hasDependencies = false;
this.layoutVertexArray = new CircleLayoutArray();
this.indexArray = new TriangleIndexArray();
this.segments = new SegmentVector();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
const styleLayer = this.layers[0];
const bucketFeatures: BucketFeature[] = [];
let circleSortKey = null;
let sortFeaturesByKey = false;
// Heatmap circles are usually large (and map-pitch-aligned), tessellate them to allow curvature along the globe.
let subdivide = styleLayer.type === 'heatmap';
// Heatmap layers are handled in this bucket and have no evaluated properties, so we check our access
if (styleLayer.type === 'circle') {
const circleStyle = (styleLayer as CircleStyleLayer);
circleSortKey = circleStyle.layout.get('circle-sort-key');
sortFeaturesByKey = !circleSortKey.isConstant();
// Circles that are "printed" onto the map surface should be tessellated to follow the globe's curvature.
subdivide = subdivide || circleStyle.paint.get('circle-pitch-alignment') === 'map';
}
const granularity = subdivide ? options.subdivisionGranularity.circle : 1;
for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry);
if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue;
const sortKey = sortFeaturesByKey ?
circleSortKey.evaluate(evaluationFeature, {}, canonical) :
undefined;
const bucketFeature: BucketFeature = {
id,
properties: feature.properties,
type: feature.type,
sourceLayerIndex,
index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature),
patterns: {},
sortKey
};
bucketFeatures.push(bucketFeature);
}
if (sortFeaturesByKey) {
bucketFeatures.sort((a, b) => a.sortKey - b.sortKey);
}
for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature;
const feature = features[index].feature;
this.addFeature(bucketFeature, geometry, index, canonical, granularity);
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
}
}
update(states: FeatureStates, vtLayer: VectorTileLayerLike, imagePositions: {[_: string]: ImagePosition}) {
if (!this.stateDependentLayers.length) return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, {
imagePositions
});
}
isEmpty() {
return this.layoutVertexArray.length === 0;
}
uploadPending() {
return !this.uploaded || this.programConfigurations.needsUpload;
}
upload(context: Context) {
if (!this.uploaded) {
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
}
this.programConfigurations.upload(context);
this.uploaded = true;
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
}
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, granularity: CircleGranularity = 1) {
// Since we store the circle's center in each vertex, we only have 3 bits for actual vertex position in each axis.
// Thus the valid range of positions is 0..7.
// This gives us 4 possible granularity settings that are symmetrical.
// This array stores vertex positions that should by used by the tessellated quad.
let extrudes: Array<number>;
switch (granularity) {
case 1:
extrudes = [0, 7];
break;
case 3:
extrudes = [0, 2, 5, 7];
break;
case 5:
extrudes = [0, 1, 3, 4, 6, 7];
break;
case 7:
extrudes = [0, 1, 2, 3, 4, 5, 6, 7];
break;
default:
throw new Error(`Invalid circle bucket granularity: ${granularity}; valid values are 1, 3, 5, 7.`);
}
const verticesPerAxis = extrudes.length;
for (const ring of geometry) {
for (const point of ring) {
const vx = point.x;
const vy = point.y;
// Do not include points that are outside the tile boundaries.
if (vx < 0 || vx >= EXTENT || vy < 0 || vy >= EXTENT) {
continue;
}
const segment = this.segments.prepareSegment(verticesPerAxis * verticesPerAxis, this.layoutVertexArray, this.indexArray, feature.sortKey);
const index = segment.vertexLength;
for (let y = 0; y < verticesPerAxis; y++) {
for (let x = 0; x < verticesPerAxis; x++) {
addCircleVertex(this.layoutVertexArray, vx, vy, extrudes[x], extrudes[y]);
}
}
for (let y = 0; y < verticesPerAxis - 1; y++) {
for (let x = 0; x < verticesPerAxis - 1; x++) {
const lowerIndex = index + y * verticesPerAxis + x;
const upperIndex = index + (y + 1) * verticesPerAxis + x;
this.indexArray.emplaceBack(lowerIndex, upperIndex + 1, lowerIndex + 1);
this.indexArray.emplaceBack(lowerIndex, upperIndex, upperIndex + 1);
}
}
segment.vertexLength += verticesPerAxis * verticesPerAxis;
segment.primitiveLength += (verticesPerAxis - 1) * (verticesPerAxis - 1) * 2;
}
}
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {imagePositions: {}, canonical});
}
}
register('CircleBucket', CircleBucket, {omit: ['layers']});

View File

@@ -0,0 +1,7 @@
import {createLayout} from '../../util/struct_array';
export const dashAttributes = createLayout([
// [0, y, height, width]
{name: 'a_dasharray_from', components: 4, type: 'Uint16'},
{name: 'a_dasharray_to', components: 4, type: 'Uint16'},
]);

View File

@@ -0,0 +1,8 @@
import {createLayout} from '../../util/struct_array';
const layout = createLayout([
{name: 'a_pos', components: 2, type: 'Int16'}
], 4);
export default layout;
export const {members, size, alignment} = layout;

View File

@@ -0,0 +1,119 @@
import {test, expect, describe, beforeAll} from 'vitest';
import {type CreateBucketParameters, createPopulateOptions, getFeaturesFromLayer, loadVectorTile} from '../../../test/unit/lib/tile';
import Point from '@mapbox/point-geometry';
import {SegmentVector} from '../segment';
import {FillBucket} from './fill_bucket';
import {FillStyleLayer} from '../../style/style_layer/fill_style_layer';
import {type LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
import {type EvaluationParameters} from '../../style/evaluation_parameters';
import {type ZoomHistory} from '../../style/zoom_history';
import {type BucketFeature, type BucketParameters} from '../bucket';
import {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {CanonicalTileID} from '../../tile/tile_id';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
function createPolygon(numPoints) {
const points = [];
for (let i = 0; i < numPoints; i++) {
points.push(new Point(2048 + 256 * Math.cos(i / numPoints * 2 * Math.PI), 2048 + 256 * Math.sin(i / numPoints * 2 * Math.PI)));
}
return points;
}
function createFillBucket({id, layout, paint, globalState, availableImages}: CreateBucketParameters): FillBucket {
const layer = new FillStyleLayer({
id,
type: 'fill',
layout,
paint
} as LayerSpecification, globalState);
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters,
availableImages as Array<string>);
return new FillBucket({layers: [layer]} as BucketParameters<FillStyleLayer>);
}
describe('FillBucket', () => {
let sourceLayer: VectorTileLayerLike;
let canonicalTileID;
beforeAll(() => {
// Load fill features from fixture tile.
sourceLayer = loadVectorTile().layers.water;
canonicalTileID = new CanonicalTileID(20, 1, 1);
});
test('FillBucket', () => {
expect(() => {
const bucket = createFillBucket({id: 'test', layout: {}});
bucket.addFeature({} as BucketFeature, [[
new Point(0, 0),
new Point(10, 10)
]], undefined, canonicalTileID, undefined, SubdivisionGranularitySetting.noSubdivision);
bucket.addFeature({} as BucketFeature, [[
new Point(0, 0),
new Point(10, 10),
new Point(10, 20)
]], undefined, canonicalTileID, undefined, SubdivisionGranularitySetting.noSubdivision);
const feature = sourceLayer.feature(0);
bucket.addFeature(feature as any, feature.loadGeometry(), undefined, canonicalTileID, undefined, SubdivisionGranularitySetting.noSubdivision);
}).not.toThrow();
});
test('FillBucket segmentation', () => {
// Stub MAX_VERTEX_ARRAY_LENGTH so we can test features
// breaking across array groups without tests taking a _long_ time.
Object.defineProperty(SegmentVector, 'MAX_VERTEX_ARRAY_LENGTH', {value: 256});
const bucket = createFillBucket({id: 'test', layout: {}, paint: {
'fill-color': ['to-color', ['get', 'foo'], '#000']
}});
// first add an initial, small feature to make sure the next one starts at
// a non-zero offset
bucket.addFeature({} as BucketFeature, [createPolygon(10)], undefined, canonicalTileID, undefined, SubdivisionGranularitySetting.noSubdivision);
// add a feature that will break across the group boundary
bucket.addFeature({} as BucketFeature, [
createPolygon(128),
createPolygon(128)
], undefined, canonicalTileID, undefined, SubdivisionGranularitySetting.noSubdivision);
// Each polygon must fit entirely within a segment, so we expect the
// first segment to include the first feature and the first polygon
// of the second feature, and the second segment to include the
// second polygon of the second feature.
expect(bucket.layoutVertexArray).toHaveLength(266);
expect(bucket.segments.get()[0]).toEqual({
vertexOffset: 0,
vertexLength: 138,
vaos: {},
primitiveOffset: 0,
primitiveLength: 134
});
expect(bucket.segments.get()[1]).toEqual({
vertexOffset: 138,
vertexLength: 128,
vaos: {},
primitiveOffset: 134,
primitiveLength: 126
});
});
test('FillBucket fill-pattern with global-state', () => {
const availableImages = [];
const bucket = createFillBucket({id: 'test', paint: {
'fill-pattern': ['coalesce', ['get', 'pattern'], ['global-state', 'pattern']]
}, globalState: {pattern: 'test-pattern'}, availableImages});
bucket.populate(getFeaturesFromLayer(sourceLayer), createPopulateOptions(availableImages), undefined);
expect(bucket.patternFeatures.length).toBeGreaterThan(0);
expect(bucket.patternFeatures[0].patterns).toEqual({
test: {min: 'test-pattern', mid: 'test-pattern', max: 'test-pattern'}
});
});
});

199
node_modules/maplibre-gl/src/data/bucket/fill_bucket.ts generated vendored Normal file
View File

@@ -0,0 +1,199 @@
import {FillLayoutArray} from '../array_types.g';
import {members as layoutAttributes} from './fill_attributes';
import {SegmentVector} from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {LineIndexArray, TriangleIndexArray} from '../index_array_type';
import {classifyRings} from '@maplibre/maplibre-gl-style-spec';
const EARCUT_MAX_RINGS = 500;
import {register} from '../../util/web_worker_transfer';
import {hasPattern, addPatternDependencies} from './pattern_bucket_features';
import {loadGeometry} from '../load_geometry';
import {toEvaluationFeature} from '../evaluation_feature';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import type {CanonicalTileID} from '../../tile/tile_id';
import type {
Bucket,
BucketParameters,
BucketFeature,
IndexedFeature,
PopulateParameters
} from '../bucket';
import type {FillStyleLayer} from '../../style/style_layer/fill_style_layer';
import type {Context} from '../../gl/context';
import type {IndexBuffer} from '../../gl/index_buffer';
import type {VertexBuffer} from '../../gl/vertex_buffer';
import type Point from '@mapbox/point-geometry';
import type {FeatureStates} from '../../source/source_state';
import type {ImagePosition} from '../../render/image_atlas';
import {subdividePolygon} from '../../render/subdivision';
import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {fillLargeMeshArrays} from '../../render/fill_large_mesh_arrays';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
export class FillBucket implements Bucket {
index: number;
zoom: number;
overscaling: number;
layers: Array<FillStyleLayer>;
layerIds: Array<string>;
stateDependentLayers: Array<FillStyleLayer>;
stateDependentLayerIds: Array<string>;
patternFeatures: Array<BucketFeature>;
layoutVertexArray: FillLayoutArray;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
indexArray2: LineIndexArray;
indexBuffer2: IndexBuffer;
hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<FillStyleLayer>;
segments: SegmentVector;
segments2: SegmentVector;
uploaded: boolean;
constructor(options: BucketParameters<FillStyleLayer>) {
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.hasDependencies = false;
this.patternFeatures = [];
this.layoutVertexArray = new FillLayoutArray();
this.indexArray = new TriangleIndexArray();
this.indexArray2 = new LineIndexArray();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.segments = new SegmentVector();
this.segments2 = new SegmentVector();
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
this.hasDependencies = hasPattern('fill', this.layers, options);
const fillSortKey = this.layers[0].layout.get('fill-sort-key');
const sortFeaturesByKey = !fillSortKey.isConstant();
const bucketFeatures: BucketFeature[] = [];
for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry);
if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue;
const sortKey = sortFeaturesByKey ?
fillSortKey.evaluate(evaluationFeature, {}, canonical, options.availableImages) :
undefined;
const bucketFeature: BucketFeature = {
id,
properties: feature.properties,
type: feature.type,
sourceLayerIndex,
index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature),
patterns: {},
sortKey
};
bucketFeatures.push(bucketFeature);
}
if (sortFeaturesByKey) {
bucketFeatures.sort((a, b) => a.sortKey - b.sortKey);
}
for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature;
if (this.hasDependencies) {
const patternFeature = addPatternDependencies('fill', this.layers, bucketFeature, {zoom: this.zoom}, options);
// pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures
this.patternFeatures.push(patternFeature);
} else {
this.addFeature(bucketFeature, geometry, index, canonical, {}, options.subdivisionGranularity);
}
const feature = features[index].feature;
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
}
}
update(states: FeatureStates, vtLayer: VectorTileLayerLike, imagePositions: {
[_: string]: ImagePosition;
}) {
if (!this.stateDependentLayers.length) return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, {
imagePositions
});
}
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {
[_: string]: ImagePosition;
}) {
for (const feature of this.patternFeatures) {
this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, options.subdivisionGranularity);
}
}
isEmpty() {
return this.layoutVertexArray.length === 0;
}
uploadPending(): boolean {
return !this.uploaded || this.programConfigurations.needsUpload;
}
upload(context: Context) {
if (!this.uploaded) {
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
this.indexBuffer2 = context.createIndexBuffer(this.indexArray2);
}
this.programConfigurations.upload(context);
this.uploaded = true;
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.indexBuffer2.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
this.segments2.destroy();
}
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {
[_: string]: ImagePosition;
}, subdivisionGranularity: SubdivisionGranularitySetting) {
for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) {
const subdivided = subdividePolygon(polygon, canonical, subdivisionGranularity.fill.getGranularityForZoomLevel(canonical.z));
const vertexArray = this.layoutVertexArray;
fillLargeMeshArrays(
(x, y) => {
vertexArray.emplaceBack(x, y);
},
this.segments,
this.layoutVertexArray,
this.indexArray,
subdivided.verticesFlattened,
subdivided.indicesTriangles,
this.segments2,
this.indexArray2,
subdivided.indicesLineList,
);
}
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {imagePositions, canonical});
}
}
register('FillBucket', FillBucket, {omit: ['layers', 'patternFeatures']});

View File

@@ -0,0 +1,13 @@
import {createLayout} from '../../util/struct_array';
const layout = createLayout([
{name: 'a_pos', components: 2, type: 'Int16'},
{name: 'a_normal_ed', components: 4, type: 'Int16'},
], 4);
export const centroidAttributes = createLayout([
{name: 'a_centroid', components: 2, type: 'Int16'}
], 4);
export default layout;
export const {members, size, alignment} = layout;

View File

@@ -0,0 +1,46 @@
import {beforeAll, describe, test, expect} from 'vitest';
import {FillExtrusionBucket} from './fill_extrusion_bucket';
import {FillExtrusionStyleLayer} from '../../style/style_layer/fill_extrusion_style_layer';
import {type LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
import {type EvaluationParameters} from '../../style/evaluation_parameters';
import {type ZoomHistory} from '../../style/zoom_history';
import {type BucketParameters} from '../bucket';
import {type CreateBucketParameters, createPopulateOptions, getFeaturesFromLayer, loadVectorTile} from '../../../test/unit/lib/tile';
import {type VectorTileLayerLike} from '@maplibre/vt-pbf';
function createFillExtrusionBucket({id, layout, paint, globalState, availableImages}: CreateBucketParameters): FillExtrusionBucket {
const layer = new FillExtrusionStyleLayer({
id,
type: 'fill-extrusion',
layout,
paint
} as LayerSpecification, globalState);
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters,
availableImages as Array<string>);
return new FillExtrusionBucket({layers: [layer]} as BucketParameters<FillExtrusionStyleLayer>);
}
describe('FillExtrusionBucket', () => {
let sourceLayer: VectorTileLayerLike;
beforeAll(() => {
// Load fill extrusion features from fixture tile.
sourceLayer = loadVectorTile().layers.water;
});
test('FillExtrusionBucket fill-pattern with global-state', () => {
const availableImages = [];
const bucket = createFillExtrusionBucket({id: 'test',
paint: {'fill-extrusion-pattern': ['coalesce', ['get', 'pattern'], ['global-state', 'pattern']]},
globalState: {pattern: 'test-pattern'},
availableImages
});
bucket.populate(getFeaturesFromLayer(sourceLayer), createPopulateOptions(availableImages), undefined);
expect(bucket.features.length).toBeGreaterThan(0);
expect(bucket.features[0].patterns).toEqual({
test: {min: 'test-pattern', mid: 'test-pattern', max: 'test-pattern'}
});
});
});

View File

@@ -0,0 +1,336 @@
import {FillExtrusionLayoutArray, PosArray} from '../array_types.g';
import {members as layoutAttributes, centroidAttributes} from './fill_extrusion_attributes';
import {type Segment, SegmentVector} from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray} from '../index_array_type';
import {EXTENT} from '../extent';
import {VectorTileFeature} from '@mapbox/vector-tile';
import {classifyRings} from '@maplibre/maplibre-gl-style-spec';
const EARCUT_MAX_RINGS = 500;
import {register} from '../../util/web_worker_transfer';
import {hasPattern, addPatternDependencies} from './pattern_bucket_features';
import {loadGeometry} from '../load_geometry';
import {toEvaluationFeature} from '../evaluation_feature';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import type {CanonicalTileID} from '../../tile/tile_id';
import type {
Bucket,
BucketParameters,
BucketFeature,
IndexedFeature,
PopulateParameters
} from '../bucket';
import type {FillExtrusionStyleLayer} from '../../style/style_layer/fill_extrusion_style_layer';
import type {Context} from '../../gl/context';
import type {IndexBuffer} from '../../gl/index_buffer';
import type {VertexBuffer} from '../../gl/vertex_buffer';
import type Point from '@mapbox/point-geometry';
import type {FeatureStates} from '../../source/source_state';
import type {ImagePosition} from '../../render/image_atlas';
import {subdividePolygon, subdivideVertexLine} from '../../render/subdivision';
import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {fillLargeMeshArrays} from '../../render/fill_large_mesh_arrays';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
const FACTOR = Math.pow(2, 13);
function addVertex(vertexArray, x, y, nx, ny, nz, t, e) {
vertexArray.emplaceBack(
// a_pos
x,
y,
// a_normal_ed: 3-component normal and 1-component edgedistance
Math.floor(nx * FACTOR) * 2 + t,
ny * FACTOR * 2,
nz * FACTOR * 2,
// edgedistance (used for wrapping patterns around extrusion sides)
Math.round(e)
);
}
type CentroidAccumulator = {
x: number;
y: number;
sampleCount: number;
};
export class FillExtrusionBucket implements Bucket {
index: number;
zoom: number;
overscaling: number;
layers: Array<FillExtrusionStyleLayer>;
layerIds: Array<string>;
stateDependentLayers: Array<FillExtrusionStyleLayer>;
stateDependentLayerIds: Array<string>;
layoutVertexArray: FillExtrusionLayoutArray;
layoutVertexBuffer: VertexBuffer;
centroidVertexArray: PosArray;
centroidVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<FillExtrusionStyleLayer>;
segments: SegmentVector;
uploaded: boolean;
features: Array<BucketFeature>;
constructor(options: BucketParameters<FillExtrusionStyleLayer>) {
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.hasDependencies = false;
this.layoutVertexArray = new FillExtrusionLayoutArray();
this.centroidVertexArray = new PosArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.segments = new SegmentVector();
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
this.features = [];
this.hasDependencies = hasPattern('fill-extrusion', this.layers, options);
for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry);
if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue;
const bucketFeature: BucketFeature = {
id,
sourceLayerIndex,
index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature),
properties: feature.properties,
type: feature.type,
patterns: {}
};
if (this.hasDependencies) {
this.features.push(addPatternDependencies('fill-extrusion', this.layers, bucketFeature, {zoom: this.zoom}, options));
} else {
this.addFeature(bucketFeature, bucketFeature.geometry, index, canonical, {}, options.subdivisionGranularity);
}
options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, true);
}
}
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) {
for (const feature of this.features) {
const {geometry} = feature;
this.addFeature(feature, geometry, feature.index, canonical, imagePositions, options.subdivisionGranularity);
}
}
update(states: FeatureStates, vtLayer: VectorTileLayerLike, imagePositions: {[_: string]: ImagePosition}) {
if (!this.stateDependentLayers.length) return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, {
imagePositions
});
}
isEmpty() {
return this.layoutVertexArray.length === 0 && this.centroidVertexArray.length === 0;
}
uploadPending() {
return !this.uploaded || this.programConfigurations.needsUpload;
}
upload(context: Context) {
if (!this.uploaded) {
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes);
this.centroidVertexBuffer = context.createVertexBuffer(this.centroidVertexArray, centroidAttributes.members, true);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
}
this.programConfigurations.upload(context);
this.uploaded = true;
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
this.centroidVertexBuffer.destroy();
}
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, subdivisionGranularity: SubdivisionGranularitySetting) {
for (const polygon of classifyRings(geometry, EARCUT_MAX_RINGS)) {
// Compute polygon centroid to calculate elevation in GPU
const centroid: CentroidAccumulator = {x: 0, y: 0, sampleCount: 0};
const oldVertexCount = this.layoutVertexArray.length;
this.processPolygon(centroid, canonical, feature, polygon, subdivisionGranularity);
const addedVertices = this.layoutVertexArray.length - oldVertexCount;
const centroidX = Math.floor(centroid.x / centroid.sampleCount);
const centroidY = Math.floor(centroid.y / centroid.sampleCount);
for (let i = 0; i < addedVertices; i++) {
this.centroidVertexArray.emplaceBack(
centroidX,
centroidY
);
}
}
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {imagePositions, canonical});
}
private processPolygon(
centroid: CentroidAccumulator,
canonical: CanonicalTileID,
feature: BucketFeature,
polygon: Array<Array<Point>>,
subdivisionGranularity: SubdivisionGranularitySetting
): void {
if (polygon.length < 1) {
return;
}
if (isEntirelyOutside(polygon[0])) {
return;
}
// Only consider the un-subdivided polygon outer ring for centroid calculation
for (const ring of polygon) {
if (ring.length === 0) {
continue;
}
// Here we don't mind if a hole ring is entirely outside, unlike when generating geometry later.
accumulatePointsToCentroid(centroid, ring);
}
const segmentReference = {
segment: this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray)
};
const granularity = subdivisionGranularity.fill.getGranularityForZoomLevel(canonical.z);
const isPolygon = VectorTileFeature.types[feature.type] === 'Polygon';
for (const ring of polygon) {
if (ring.length === 0) {
continue;
}
if (isEntirelyOutside(ring)) {
continue;
}
const subdividedRing = subdivideVertexLine(ring, granularity, isPolygon);
this._generateSideFaces(subdividedRing, segmentReference);
}
// Only triangulate and draw the area of the feature if it is a polygon
// Other feature types (e.g. LineString) do not have area, so triangulation is pointless / undefined
if (!isPolygon)
return;
// Do not generate outlines, since outlines already got subdivided earlier.
const subdividedPolygon = subdividePolygon(polygon, canonical, granularity, false);
const vertexArray = this.layoutVertexArray;
fillLargeMeshArrays(
(x, y) => {
addVertex(vertexArray, x, y, 0, 0, 1, 1, 0);
},
this.segments,
this.layoutVertexArray,
this.indexArray,
subdividedPolygon.verticesFlattened,
subdividedPolygon.indicesTriangles
);
}
/**
* Generates side faces for the supplied geometry. Assumes `geometry` to be a line string, like the output of {@link subdivideVertexLine}.
* For rings, it is assumed that the first and last vertex of `geometry` are equal.
*/
private _generateSideFaces(geometry: Array<Point>, segmentReference: {segment: Segment}) {
let edgeDistance = 0;
for (let p = 1; p < geometry.length; p++) {
const p1 = geometry[p];
const p2 = geometry[p - 1];
if (isBoundaryEdge(p1, p2)) {
continue;
}
if (segmentReference.segment.vertexLength + 4 > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
segmentReference.segment = this.segments.prepareSegment(4, this.layoutVertexArray, this.indexArray);
}
const perp = p1.sub(p2)._perp()._unit();
const dist = p2.dist(p1);
if (edgeDistance + dist > 32768) edgeDistance = 0;
addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 0, edgeDistance);
addVertex(this.layoutVertexArray, p1.x, p1.y, perp.x, perp.y, 0, 1, edgeDistance);
edgeDistance += dist;
addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 0, edgeDistance);
addVertex(this.layoutVertexArray, p2.x, p2.y, perp.x, perp.y, 0, 1, edgeDistance);
const bottomRight = segmentReference.segment.vertexLength;
// ┌──────┐
// │ 0 1 │ Counter-clockwise winding order.
// │ │ Triangle 1: 0 => 2 => 1
// │ 2 3 │ Triangle 2: 1 => 2 => 3
// └──────┘
this.indexArray.emplaceBack(bottomRight, bottomRight + 2, bottomRight + 1);
this.indexArray.emplaceBack(bottomRight + 1, bottomRight + 2, bottomRight + 3);
segmentReference.segment.vertexLength += 4;
segmentReference.segment.primitiveLength += 2;
}
}
}
/**
* Accumulates geometry to centroid. Geometry can be either a polygon ring, a line string or a closed line string.
* In case of a polygon ring or line ring, the last vertex is ignored if it is the same as the first vertex.
*/
function accumulatePointsToCentroid(centroid: CentroidAccumulator, geometry: Array<Point>): void {
for (let i = 0; i < geometry.length; i++) {
const p = geometry[i];
if (i === geometry.length - 1 && geometry[0].x === p.x && geometry[0].y === p.y) {
continue;
}
centroid.x += p.x;
centroid.y += p.y;
centroid.sampleCount++;
}
}
register('FillExtrusionBucket', FillExtrusionBucket, {omit: ['layers', 'features']});
function isBoundaryEdge(p1, p2) {
return (p1.x === p2.x && (p1.x < 0 || p1.x > EXTENT)) ||
(p1.y === p2.y && (p1.y < 0 || p1.y > EXTENT));
}
function isEntirelyOutside(ring) {
return ring.every(p => p.x < 0) ||
ring.every(p => p.x > EXTENT) ||
ring.every(p => p.y < 0) ||
ring.every(p => p.y > EXTENT);
}

View File

@@ -0,0 +1,12 @@
import {CircleBucket} from './circle_bucket';
import {register} from '../../util/web_worker_transfer';
import type {HeatmapStyleLayer} from '../../style/style_layer/heatmap_style_layer';
export class HeatmapBucket extends CircleBucket<HeatmapStyleLayer> {
// Needed for flow to accept omit: ['layers'] below, due to
// https://github.com/facebook/flow/issues/4262
layers: Array<HeatmapStyleLayer>;
}
register('HeatmapBucket', HeatmapBucket, {omit: ['layers']});

View File

@@ -0,0 +1,8 @@
import {createLayout} from '../../util/struct_array';
export const lineLayoutAttributes = createLayout([
{name: 'a_pos_normal', components: 2, type: 'Int16'},
{name: 'a_data', components: 4, type: 'Uint8'}
], 4);
export const {members, size, alignment} = lineLayoutAttributes;

View File

@@ -0,0 +1,8 @@
import {createLayout} from '../../util/struct_array';
export const lineLayoutAttributesExt = createLayout([
{name: 'a_uv_x', components: 1, type: 'Float32'},
{name: 'a_split_index', components: 1, type: 'Float32'},
]);
export const {members, size, alignment} = lineLayoutAttributesExt;

View File

@@ -0,0 +1,191 @@
import {beforeAll, describe, test, expect, vi} from 'vitest';
import Point from '@mapbox/point-geometry';
import {SegmentVector} from '../segment';
import {LineBucket} from './line_bucket';
import {LineStyleLayer} from '../../style/style_layer/line_style_layer';
import {type LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
import {type EvaluationParameters} from '../../style/evaluation_parameters';
import {type ZoomHistory} from '../../../src/style/zoom_history';
import {type BucketFeature, type BucketParameters} from '../bucket';
import {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {type CreateBucketParameters, createPopulateOptions, getFeaturesFromLayer, loadVectorTile} from '../../../test/unit/lib/tile';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
const {noSubdivision} = SubdivisionGranularitySetting;
function createLine(numPoints) {
const points = [];
for (let i = 0; i < numPoints; i++) {
points.push(new Point(i / numPoints, i / numPoints));
}
return points;
}
function createLineBucket({id, layout, paint, globalState, availableImages}: CreateBucketParameters): LineBucket {
const layer = new LineStyleLayer({
id,
type: 'line',
layout,
paint
} as LayerSpecification, globalState);
layer.recalculate({zoom: 0, zoomHistory: {} as ZoomHistory} as EvaluationParameters,
availableImages as Array<string>);
return new LineBucket({layers: [layer]} as BucketParameters<LineStyleLayer>);
}
describe('LineBucket', () => {
let sourceLayer: VectorTileLayerLike;
beforeAll(() => {
// Load line features from fixture tile.
sourceLayer = loadVectorTile().layers.road;
});
test('LineBucket', () => {
expect(() => {
const bucket = createLineBucket({
id: 'test'
});
const line = {
type: 2,
properties: {}
} as BucketFeature;
const polygon = {
type: 3,
properties: {}
} as BucketFeature;
bucket.addLine([
new Point(0, 0)
], line, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0)
], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(0, 0)
], line, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(0, 0)
], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(10, 10),
new Point(0, 0)
], line, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(10, 10),
new Point(0, 0)
], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(10, 10),
new Point(10, 20)
], line, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(10, 10),
new Point(10, 20)
], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(10, 10),
new Point(10, 20),
new Point(0, 0)
], line, undefined, undefined, undefined, undefined, undefined, noSubdivision);
bucket.addLine([
new Point(0, 0),
new Point(10, 10),
new Point(10, 20),
new Point(0, 0)
], polygon, undefined, undefined, undefined, undefined, undefined, noSubdivision);
const feature = sourceLayer.feature(0);
bucket.addFeature(feature as any, feature.loadGeometry(), undefined, undefined, undefined, undefined, noSubdivision);
}).not.toThrow();
});
test('LineBucket segmentation', () => {
vi.spyOn(console, 'warn').mockImplementation(() => { });
// Stub MAX_VERTEX_ARRAY_LENGTH so we can test features
// breaking across array groups without tests taking a _long_ time.
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 256;
const bucket = createLineBucket({
id: 'test'
});
// first add an initial, small feature to make sure the next one starts at
// a non-zero offset
bucket.addFeature({} as BucketFeature, [createLine(10)], undefined, undefined, undefined, undefined, noSubdivision);
// add a feature that will break across the group boundary
bucket.addFeature({} as BucketFeature, [createLine(128)], undefined, undefined, undefined, undefined, noSubdivision);
// Each polygon must fit entirely within a segment, so we expect the
// first segment to include the first feature and the first polygon
// of the second feature, and the second segment to include the
// second polygon of the second feature.
expect(bucket.layoutVertexArray).toHaveLength(276);
expect(bucket.segments.get()).toEqual([{
vertexOffset: 0,
vertexLength: 20,
vaos: {},
primitiveOffset: 0,
primitiveLength: 18
}, {
vertexOffset: 20,
vertexLength: 256,
vaos: {},
primitiveOffset: 18,
primitiveLength: 254
}]);
expect(console.warn).toHaveBeenCalledTimes(1);
});
test('LineBucket line-pattern with global-state', () => {
const availableImages = [];
const bucket = createLineBucket({id: 'test',
paint: {'line-pattern': ['coalesce', ['get', 'pattern'], ['global-state', 'pattern']]},
globalState: {pattern: 'test-pattern'},
availableImages
});
bucket.populate(getFeaturesFromLayer(sourceLayer), createPopulateOptions(availableImages), undefined);
expect(bucket.patternFeatures.length).toBeGreaterThan(0);
expect(bucket.patternFeatures[0].patterns).toEqual({
test: {min: 'test-pattern', mid: 'test-pattern', max: 'test-pattern'}
});
});
test('LineBucket line-dasharray with global-state', () => {
const bucket = createLineBucket({id: 'test',
paint: {'line-dasharray': ['coalesce', ['get', 'dasharray'], ['global-state', 'dasharray']]},
globalState: {'dasharray': [3, 3]},
availableImages: []
});
bucket.populate(getFeaturesFromLayer(sourceLayer), createPopulateOptions([]), undefined);
expect(bucket.patternFeatures.length).toBeGreaterThan(0);
expect(bucket.patternFeatures[0].dashes).toEqual({
test: {min: '3,3,false', mid: '3,3,false', max: '3,3,false'}
});
});
});

652
node_modules/maplibre-gl/src/data/bucket/line_bucket.ts generated vendored Normal file
View File

@@ -0,0 +1,652 @@
import {LineLayoutArray, LineExtLayoutArray} from '../array_types.g';
import {GEOJSONVT_CLIP_END, GEOJSONVT_CLIP_START} from '@maplibre/geojson-vt';
import {members as layoutAttributes} from './line_attributes';
import {members as layoutAttributesExt} from './line_attributes_ext';
import {SegmentVector} from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray} from '../index_array_type';
import {EXTENT} from '../extent';
import {VectorTileFeature} from '@mapbox/vector-tile';
import {register} from '../../util/web_worker_transfer';
import {hasPattern, addPatternDependencies} from './pattern_bucket_features';
import {loadGeometry} from '../load_geometry';
import {toEvaluationFeature} from '../evaluation_feature';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import {subdivideVertexLine} from '../../render/subdivision';
import type {CanonicalTileID} from '../../tile/tile_id';
import type {
Bucket,
BucketParameters,
BucketFeature,
IndexedFeature,
PopulateParameters
} from '../bucket';
import type {LineStyleLayer} from '../../style/style_layer/line_style_layer';
import type Point from '@mapbox/point-geometry';
import type {Segment} from '../segment';
import type {RGBAImage} from '../../util/image';
import type {Context} from '../../gl/context';
import type {Texture} from '../../render/texture';
import type {IndexBuffer} from '../../gl/index_buffer';
import type {VertexBuffer} from '../../gl/vertex_buffer';
import type {FeatureStates} from '../../source/source_state';
import type {ImagePosition} from '../../render/image_atlas';
import type {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import type {DashEntry} from '../../render/line_atlas';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
// NOTE ON EXTRUDE SCALE:
// scale the extrusion vector so that the normal length is this value.
// contains the "texture" normals (-1..1). this is distinct from the extrude
// normals for line joins, because the x-value remains 0 for the texture
// normal array, while the extrude normal actually moves the vertex to create
// the acute/bevelled line join.
const EXTRUDE_SCALE = 63;
/*
* Sharp corners cause dashed lines to tilt because the distance along the line
* is the same at both the inner and outer corners. To improve the appearance of
* dashed lines we add extra points near sharp corners so that a smaller part
* of the line is tilted.
*
* COS_HALF_SHARP_CORNER controls how sharp a corner has to be for us to add an
* extra vertex. The default is 75 degrees.
*
* The newly created vertices are placed SHARP_CORNER_OFFSET pixels from the corner.
*/
const COS_HALF_SHARP_CORNER = Math.cos(75 / 2 * (Math.PI / 180));
const SHARP_CORNER_OFFSET = 15;
// Angle per triangle for approximating round line joins.
const DEG_PER_TRIANGLE = 20;
// The number of bits that is used to store the line distance in the buffer.
const LINE_DISTANCE_BUFFER_BITS = 15;
// We don't have enough bits for the line distance as we'd like to have, so
// use this value to scale the line distance (in tile units) down to a smaller
// value. This lets us store longer distances while sacrificing precision.
const LINE_DISTANCE_SCALE = 1 / 2;
// The maximum line distance, in tile units, that fits in the buffer.
const MAX_LINE_DISTANCE = Math.pow(2, LINE_DISTANCE_BUFFER_BITS - 1) / LINE_DISTANCE_SCALE;
type LineClips = {
start: number;
end: number;
};
type GradientTexture = {
texture?: Texture;
gradient?: RGBAImage;
version?: number;
};
/**
* @internal
* Line bucket class
*/
export class LineBucket implements Bucket {
distance: number;
totalDistance: number;
maxLineLength: number;
scaledDistance: number;
lineClips?: LineClips;
e1: number;
e2: number;
index: number;
zoom: number;
overscaling: number;
layers: Array<LineStyleLayer>;
layerIds: Array<string>;
gradients: {[x: string]: GradientTexture};
stateDependentLayers: Array<any>;
stateDependentLayerIds: Array<string>;
patternFeatures: Array<BucketFeature>;
lineClipsArray: Array<LineClips>;
layoutVertexArray: LineLayoutArray;
layoutVertexBuffer: VertexBuffer;
layoutVertexArray2: LineExtLayoutArray;
layoutVertexBuffer2: VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
hasDependencies: boolean;
programConfigurations: ProgramConfigurationSet<LineStyleLayer>;
segments: SegmentVector;
uploaded: boolean;
constructor(options: BucketParameters<LineStyleLayer>) {
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.hasDependencies = false;
this.patternFeatures = [];
this.lineClipsArray = [];
this.gradients = {};
this.layers.forEach(layer => {
this.gradients[layer.id] = {};
});
this.layoutVertexArray = new LineLayoutArray();
this.layoutVertexArray2 = new LineExtLayoutArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.segments = new SegmentVector();
this.maxLineLength = 0;
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
this.hasDependencies = hasPattern('line', this.layers, options) || this.hasLineDasharray(this.layers);
const lineSortKey = this.layers[0].layout.get('line-sort-key');
const sortFeaturesByKey = !lineSortKey.isConstant();
const bucketFeatures: BucketFeature[] = [];
for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = this.layers[0]._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry);
if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue;
const sortKey = sortFeaturesByKey ?
lineSortKey.evaluate(evaluationFeature, {}, canonical) :
undefined;
const bucketFeature: BucketFeature = {
id,
properties: feature.properties,
type: feature.type,
sourceLayerIndex,
index,
geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature),
patterns: {},
dashes: {},
sortKey
};
bucketFeatures.push(bucketFeature);
}
if (sortFeaturesByKey) {
bucketFeatures.sort((a, b) => {
return (a.sortKey) - (b.sortKey);
});
}
for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature;
if (this.hasDependencies) {
if (hasPattern('line', this.layers, options)) {
addPatternDependencies('line', this.layers, bucketFeature, {zoom: this.zoom}, options);
} else if (this.hasLineDasharray(this.layers)) {
this.addLineDashDependencies(this.layers, bucketFeature, this.zoom, options);
}
// pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures
this.patternFeatures.push(bucketFeature);
} else {
this.addFeature(bucketFeature, geometry, index, canonical, {}, {}, options.subdivisionGranularity);
}
const feature = features[index].feature;
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
}
}
update(states: FeatureStates, vtLayer: VectorTileLayerLike, imagePositions: {[_: string]: ImagePosition}, dashPositions: {[_: string]: DashEntry}) {
if (!this.stateDependentLayers.length) return;
this.programConfigurations.updatePaintArrays(states, vtLayer, this.stateDependentLayers, {
imagePositions,
dashPositions
});
}
addFeatures(options: PopulateParameters, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, dashPositions?: {[_: string]: DashEntry}) {
for (const feature of this.patternFeatures) {
this.addFeature(feature, feature.geometry, feature.index, canonical, imagePositions, dashPositions, options.subdivisionGranularity);
}
}
isEmpty() {
return this.layoutVertexArray.length === 0;
}
uploadPending() {
return !this.uploaded || this.programConfigurations.needsUpload;
}
upload(context: Context) {
if (!this.uploaded) {
if (this.layoutVertexArray2.length !== 0) {
this.layoutVertexBuffer2 = context.createVertexBuffer(this.layoutVertexArray2, layoutAttributesExt);
}
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
}
this.programConfigurations.upload(context);
this.uploaded = true;
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
}
lineFeatureClips(feature: BucketFeature): LineClips | undefined {
if (!!feature.properties && Object.prototype.hasOwnProperty.call(feature.properties, GEOJSONVT_CLIP_START) && Object.prototype.hasOwnProperty.call(feature.properties, GEOJSONVT_CLIP_END)) {
const start = +feature.properties[GEOJSONVT_CLIP_START];
const end = +feature.properties[GEOJSONVT_CLIP_END];
return {start, end};
}
}
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}, dashPositions: Record<string, DashEntry>, subdivisionGranularity: SubdivisionGranularitySetting) {
const layout = this.layers[0].layout;
const join = layout.get('line-join').evaluate(feature, {});
const cap = layout.get('line-cap').evaluate(feature, {});
const miterLimit = layout.get('line-miter-limit').evaluate(feature, {});
const roundLimit = layout.get('line-round-limit').evaluate(feature, {});
this.lineClips = this.lineFeatureClips(feature);
for (const line of geometry) {
this.addLine(line, feature, join, cap, miterLimit, roundLimit, canonical, subdivisionGranularity);
}
this.programConfigurations.populatePaintArrays(this.layoutVertexArray.length, feature, index, {imagePositions, dashPositions, canonical});
}
addLine(vertices: Array<Point>, feature: BucketFeature, join: string, cap: string, miterLimit: number, roundLimit: number, canonical: CanonicalTileID | undefined, subdivisionGranularity: SubdivisionGranularitySetting) {
this.distance = 0;
this.scaledDistance = 0;
this.totalDistance = 0;
// First, subdivide the line if needed (mostly for globe rendering)
const granularity = canonical ? subdivisionGranularity.line.getGranularityForZoomLevel(canonical.z) : 1;
vertices = subdivideVertexLine(vertices, granularity);
if (this.lineClips) {
this.lineClipsArray.push(this.lineClips);
// Calculate the total distance, in tile units, of this tiled line feature
for (let i = 0; i < vertices.length - 1; i++) {
this.totalDistance += vertices[i].dist(vertices[i + 1]);
}
this.updateScaledDistance();
this.maxLineLength = Math.max(this.maxLineLength, this.totalDistance);
}
const isPolygon = VectorTileFeature.types[feature.type] === 'Polygon';
// If the line has duplicate vertices at the ends, adjust start/length to remove them.
let len = vertices.length;
while (len >= 2 && vertices[len - 1].equals(vertices[len - 2])) {
len--;
}
let first = 0;
while (first < len - 1 && vertices[first].equals(vertices[first + 1])) {
first++;
}
// Ignore invalid geometry.
if (len < (isPolygon ? 3 : 2)) return;
if (join === 'bevel') miterLimit = 1.05;
const sharpCornerOffset = this.overscaling <= 16 ?
SHARP_CORNER_OFFSET * EXTENT / (512 * this.overscaling) :
0;
// we could be more precise, but it would only save a negligible amount of space
const segment = this.segments.prepareSegment(len * 10, this.layoutVertexArray, this.indexArray);
let currentVertex: Point;
let prevVertex: Point;
let nextVertex: Point;
let prevNormal: Point;
let nextNormal: Point;
// the last two vertices added
this.e1 = this.e2 = -1;
if (isPolygon) {
currentVertex = vertices[len - 2];
nextNormal = vertices[first].sub(currentVertex)._unit()._perp();
}
for (let i = first; i < len; i++) {
nextVertex = i === len - 1 ?
(isPolygon ? vertices[first + 1] : undefined) : // if it's a polygon, treat the last vertex like the first
vertices[i + 1]; // just the next vertex
// if two consecutive vertices exist, skip the current one
if (nextVertex && vertices[i].equals(nextVertex)) continue;
if (nextNormal) prevNormal = nextNormal;
if (currentVertex) prevVertex = currentVertex;
currentVertex = vertices[i];
// Calculate the normal towards the next vertex in this line. In case
// there is no next vertex, pretend that the line is continuing straight,
// meaning that we are just using the previous normal.
nextNormal = nextVertex ? nextVertex.sub(currentVertex)._unit()._perp() : prevNormal;
// If we still don't have a previous normal, this is the beginning of a
// non-closed line, so we're doing a straight "join".
prevNormal = prevNormal || nextNormal;
// Determine the normal of the join extrusion. It is the angle bisector
// of the segments between the previous line and the next line.
// In the case of 180° angles, the prev and next normals cancel each other out:
// prevNormal + nextNormal = (0, 0), its magnitude is 0, so the unit vector would be
// undefined. In that case, we're keeping the joinNormal at (0, 0), so that the cosHalfAngle
// below will also become 0 and miterLength will become Infinity.
let joinNormal = prevNormal.add(nextNormal);
if (joinNormal.x !== 0 || joinNormal.y !== 0) {
joinNormal._unit();
}
/* joinNormal prevNormal
* ↖ ↑
* .________. prevVertex
* |
* nextNormal ← | currentVertex
* |
* nextVertex !
*
*/
// calculate cosines of the angle (and its half) using dot product
const cosAngle = prevNormal.x * nextNormal.x + prevNormal.y * nextNormal.y;
const cosHalfAngle = joinNormal.x * nextNormal.x + joinNormal.y * nextNormal.y;
// Calculate the length of the miter (the ratio of the miter to the width)
// as the inverse of cosine of the angle between next and join normals
const miterLength = cosHalfAngle !== 0 ? 1 / cosHalfAngle : Infinity;
// approximate angle from cosine
const approxAngle = 2 * Math.sqrt(2 - 2 * cosHalfAngle);
const isSharpCorner = cosHalfAngle < COS_HALF_SHARP_CORNER && prevVertex && nextVertex;
const lineTurnsLeft = prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x > 0;
if (isSharpCorner && i > first) {
const prevSegmentLength = currentVertex.dist(prevVertex);
if (prevSegmentLength > 2 * sharpCornerOffset) {
const newPrevVertex = currentVertex.sub(currentVertex.sub(prevVertex)._mult(sharpCornerOffset / prevSegmentLength)._round());
this.updateDistance(prevVertex, newPrevVertex);
this.addCurrentVertex(newPrevVertex, prevNormal, 0, 0, segment);
prevVertex = newPrevVertex;
}
}
// The join if a middle vertex, otherwise the cap.
const middleVertex = prevVertex && nextVertex;
let currentJoin = middleVertex ? join : isPolygon ? 'butt' : cap;
if (middleVertex && currentJoin === 'round') {
if (miterLength < roundLimit) {
currentJoin = 'miter';
} else if (miterLength <= 2) {
currentJoin = 'fakeround';
}
}
if (currentJoin === 'miter' && miterLength > miterLimit) {
currentJoin = 'bevel';
}
if (currentJoin === 'bevel') {
// The maximum extrude length is 128 / 63 = 2 times the width of the line
// so if miterLength >= 2 we need to draw a different type of bevel here.
if (miterLength > 2) currentJoin = 'flipbevel';
// If the miterLength is really small and the line bevel wouldn't be visible,
// just draw a miter join to save a triangle.
if (miterLength < miterLimit) currentJoin = 'miter';
}
// Calculate how far along the line the currentVertex is
if (prevVertex) this.updateDistance(prevVertex, currentVertex);
if (currentJoin === 'miter') {
joinNormal._mult(miterLength);
this.addCurrentVertex(currentVertex, joinNormal, 0, 0, segment);
} else if (currentJoin === 'flipbevel') {
// miter is too big, flip the direction to make a beveled join
if (miterLength > 100) {
// Almost parallel lines
joinNormal = nextNormal.mult(-1);
} else {
const bevelLength = miterLength * prevNormal.add(nextNormal).mag() / prevNormal.sub(nextNormal).mag();
joinNormal._perp()._mult(bevelLength * (lineTurnsLeft ? -1 : 1));
}
this.addCurrentVertex(currentVertex, joinNormal, 0, 0, segment);
this.addCurrentVertex(currentVertex, joinNormal.mult(-1), 0, 0, segment);
} else if (currentJoin === 'bevel' || currentJoin === 'fakeround') {
const offset = -Math.sqrt(miterLength * miterLength - 1);
const offsetA = lineTurnsLeft ? offset : 0;
const offsetB = lineTurnsLeft ? 0 : offset;
// Close previous segment with a bevel
if (prevVertex) {
this.addCurrentVertex(currentVertex, prevNormal, offsetA, offsetB, segment);
}
if (currentJoin === 'fakeround') {
// The join angle is sharp enough that a round join would be visible.
// Bevel joins fill the gap between segments with a single pie slice triangle.
// Create a round join by adding multiple pie slices. The join isn't actually round, but
// it looks like it is at the sizes we render lines at.
// pick the number of triangles for approximating round join by based on the angle between normals
const n = Math.round((approxAngle * 180 / Math.PI) / DEG_PER_TRIANGLE);
for (let m = 1; m < n; m++) {
let t = m / n;
if (t !== 0.5) {
// approximate spherical interpolation https://observablehq.com/@mourner/approximating-geometric-slerp
const t2 = t - 0.5;
const A = 1.0904 + cosAngle * (-3.2452 + cosAngle * (3.55645 - cosAngle * 1.43519));
const B = 0.848013 + cosAngle * (-1.06021 + cosAngle * 0.215638);
t = t + t * t2 * (t - 1) * (A * t2 * t2 + B);
}
const extrude = nextNormal.sub(prevNormal)._mult(t)._add(prevNormal)._unit()._mult(lineTurnsLeft ? -1 : 1);
this.addHalfVertex(currentVertex, extrude.x, extrude.y, false, lineTurnsLeft, 0, segment);
}
}
if (nextVertex) {
// Start next segment
this.addCurrentVertex(currentVertex, nextNormal, -offsetA, -offsetB, segment);
}
} else if (currentJoin === 'butt') {
this.addCurrentVertex(currentVertex, joinNormal, 0, 0, segment); // butt cap
} else if (currentJoin === 'square') {
const offset = prevVertex ? 1 : -1; // closing or starting square cap
this.addCurrentVertex(currentVertex, joinNormal, offset, offset, segment);
} else if (currentJoin === 'round') {
if (prevVertex) {
// Close previous segment with butt
this.addCurrentVertex(currentVertex, prevNormal, 0, 0, segment);
// Add round cap or linejoin at end of segment
this.addCurrentVertex(currentVertex, prevNormal, 1, 1, segment, true);
}
if (nextVertex) {
// Add round cap before first segment
this.addCurrentVertex(currentVertex, nextNormal, -1, -1, segment, true);
// Start next segment with a butt
this.addCurrentVertex(currentVertex, nextNormal, 0, 0, segment);
}
}
if (isSharpCorner && i < len - 1) {
const nextSegmentLength = currentVertex.dist(nextVertex);
if (nextSegmentLength > 2 * sharpCornerOffset) {
const newCurrentVertex = currentVertex.add(nextVertex.sub(currentVertex)._mult(sharpCornerOffset / nextSegmentLength)._round());
this.updateDistance(currentVertex, newCurrentVertex);
this.addCurrentVertex(newCurrentVertex, nextNormal, 0, 0, segment);
currentVertex = newCurrentVertex;
}
}
}
}
/**
* Add two vertices to the buffers.
*
* @param p - the line vertex to add buffer vertices for
* @param normal - vertex normal
* @param endLeft - extrude to shift the left vertex along the line
* @param endRight - extrude to shift the left vertex along the line
* @param segment - the segment object to add the vertex to
* @param round - whether this is a round cap
*/
addCurrentVertex(p: Point, normal: Point, endLeft: number, endRight: number, segment: Segment, round: boolean = false) {
// left and right extrude vectors, perpendicularly shifted by endLeft/endRight
const leftX = normal.x + normal.y * endLeft;
const leftY = normal.y - normal.x * endLeft;
const rightX = -normal.x + normal.y * endRight;
const rightY = -normal.y - normal.x * endRight;
this.addHalfVertex(p, leftX, leftY, round, false, endLeft, segment);
this.addHalfVertex(p, rightX, rightY, round, true, -endRight, segment);
// There is a maximum "distance along the line" that we can store in the buffers.
// When we get close to the distance, reset it to zero and add the vertex again with
// a distance of zero. The max distance is determined by the number of bits we allocate
// to `linesofar`.
if (this.distance > MAX_LINE_DISTANCE / 2 && this.totalDistance === 0) {
this.distance = 0;
this.updateScaledDistance();
this.addCurrentVertex(p, normal, endLeft, endRight, segment, round);
}
}
addHalfVertex({x, y}: Point, extrudeX: number, extrudeY: number, round: boolean, up: boolean, dir: number, segment: Segment) {
const totalDistance = this.lineClips ? this.scaledDistance * (MAX_LINE_DISTANCE - 1) : this.scaledDistance;
// scale down so that we can store longer distances while sacrificing precision.
const linesofarScaled = totalDistance * LINE_DISTANCE_SCALE;
this.layoutVertexArray.emplaceBack(
// a_pos_normal
// Encode round/up the least significant bits
(x << 1) + (round ? 1 : 0),
(y << 1) + (up ? 1 : 0),
// a_data
// add 128 to store a byte in an unsigned byte
Math.round(EXTRUDE_SCALE * extrudeX) + 128,
Math.round(EXTRUDE_SCALE * extrudeY) + 128,
// Encode the -1/0/1 direction value into the first two bits of .z of a_data.
// Combine it with the lower 6 bits of `linesofarScaled` (shifted by 2 bits to make
// room for the direction value). The upper 8 bits of `linesofarScaled` are placed in
// the `w` component.
((dir === 0 ? 0 : (dir < 0 ? -1 : 1)) + 1) | ((linesofarScaled & 0x3F) << 2),
linesofarScaled >> 6);
// Constructs a second vertex buffer with higher precision line progress
if (this.lineClips) {
const progressRealigned = this.scaledDistance - this.lineClips.start;
const endClipRealigned = this.lineClips.end - this.lineClips.start;
const uvX = progressRealigned / endClipRealigned;
this.layoutVertexArray2.emplaceBack(uvX, this.lineClipsArray.length);
}
const e = segment.vertexLength++;
if (this.e1 >= 0 && this.e2 >= 0) {
this.indexArray.emplaceBack(this.e1, e, this.e2);
segment.primitiveLength++;
}
if (up) {
this.e2 = e;
} else {
this.e1 = e;
}
}
updateScaledDistance() {
// Knowing the ratio of the full linestring covered by this tiled feature, as well
// as the total distance (in tile units) of this tiled feature, and the distance
// (in tile units) of the current vertex, we can determine the relative distance
// of this vertex along the full linestring feature and scale it to [0, 2^15)
this.scaledDistance = this.lineClips ?
this.lineClips.start + (this.lineClips.end - this.lineClips.start) * this.distance / this.totalDistance :
this.distance;
}
updateDistance(prev: Point, next: Point) {
this.distance += prev.dist(next);
this.updateScaledDistance();
}
private hasLineDasharray(layers: Array<LineStyleLayer>): boolean {
for (const layer of layers) {
const dasharrayProperty = layer.paint.get('line-dasharray');
if (dasharrayProperty && !dasharrayProperty.isConstant()) {
return true;
}
}
return false;
}
private addLineDashDependencies(layers: Array<LineStyleLayer>, bucketFeature: BucketFeature, zoom: number, options: PopulateParameters) {
for (const layer of layers) {
const dasharrayProperty = layer.paint.get('line-dasharray');
if (!dasharrayProperty || dasharrayProperty.value.kind === 'constant') {
continue;
}
const round = layer.layout.get('line-cap').evaluate(bucketFeature, {}) === 'round';
const min = {
dasharray: dasharrayProperty.value.evaluate({zoom: zoom - 1}, bucketFeature, {}),
round
};
const mid = {
dasharray: dasharrayProperty.value.evaluate({zoom}, bucketFeature, {}),
round
};
const max = {
dasharray: dasharrayProperty.value.evaluate({zoom: zoom + 1}, bucketFeature, {}),
round
};
const minKey = `${min.dasharray.join(',')},${min.round}`;
const midKey = `${mid.dasharray.join(',')},${mid.round}`;
const maxKey = `${max.dasharray.join(',')},${max.round}`;
options.dashDependencies[minKey] = min;
options.dashDependencies[midKey] = mid;
options.dashDependencies[maxKey] = max;
bucketFeature.dashes[layer.id] = {min: minKey, mid: midKey, max: maxKey};
}
}
}
register('LineBucket', LineBucket, {omit: ['layers', 'patternFeatures']});

View File

@@ -0,0 +1,9 @@
import {createLayout} from '../../util/struct_array';
export const patternAttributes = createLayout([
// [tl.x, tl.y, br.x, br.y]
{name: 'a_pattern_from', components: 4, type: 'Uint16'},
{name: 'a_pattern_to', components: 4, type: 'Uint16'},
{name: 'a_pixel_ratio_from', components: 1, type: 'Uint16'},
{name: 'a_pixel_ratio_to', components: 1, type: 'Uint16'},
]);

View File

@@ -0,0 +1,58 @@
import type {FillStyleLayer} from '../../style/style_layer/fill_style_layer';
import type {FillExtrusionStyleLayer} from '../../style/style_layer/fill_extrusion_style_layer';
import type {LineStyleLayer} from '../../style/style_layer/line_style_layer';
import type {
BucketFeature,
PopulateParameters
} from '../bucket';
import {type PossiblyEvaluated} from '../../style/properties';
type PatternStyleLayers = Array<LineStyleLayer> | Array<FillStyleLayer> | Array<FillExtrusionStyleLayer>;
export function hasPattern(type: string, layers: PatternStyleLayers, options: PopulateParameters) {
const patterns = options.patternDependencies;
let hasPattern = false;
for (const layer of layers) {
const patternProperty = (layer.paint as PossiblyEvaluated<any, any>).get(`${type}-pattern`);
if (!patternProperty.isConstant()) {
hasPattern = true;
}
const constantPattern = patternProperty.constantOr(null);
if (constantPattern) {
hasPattern = true;
patterns[constantPattern.to] = true;
patterns[constantPattern.from] = true;
}
}
return hasPattern;
}
export function addPatternDependencies(type: string, layers: PatternStyleLayers, patternFeature: BucketFeature, parameters: { zoom: number }, options: PopulateParameters) {
const {zoom} = parameters;
const patterns = options.patternDependencies;
for (const layer of layers) {
const patternProperty = (layer.paint as PossiblyEvaluated<any, any>).get(`${type}-pattern`);
const patternPropertyValue = patternProperty.value;
if (patternPropertyValue.kind !== 'constant') {
let min = patternPropertyValue.evaluate({zoom: zoom - 1}, patternFeature, {}, options.availableImages);
let mid = patternPropertyValue.evaluate({zoom}, patternFeature, {}, options.availableImages);
let max = patternPropertyValue.evaluate({zoom: zoom + 1}, patternFeature, {}, options.availableImages);
min = min && min.name ? min.name : min;
mid = mid && mid.name ? mid.name : mid;
max = max && max.name ? max.name : max;
// add to patternDependencies
patterns[min] = true;
patterns[mid] = true;
patterns[max] = true;
// save for layout
patternFeature.patterns[layer.id] = {min, mid, max};
}
}
return patternFeature;
}

View File

@@ -0,0 +1,122 @@
import {createLayout} from '../../util/struct_array';
export const symbolLayoutAttributes = createLayout([
{name: 'a_pos_offset', components: 4, type: 'Int16'},
{name: 'a_data', components: 4, type: 'Uint16'},
{name: 'a_pixeloffset', components: 4, type: 'Int16'}
], 4);
export const dynamicLayoutAttributes = createLayout([
{name: 'a_projected_pos', components: 3, type: 'Float32'}
], 4);
export const placementOpacityAttributes = createLayout([
{name: 'a_fade_opacity', components: 1, type: 'Uint32'}
], 4);
export const collisionVertexAttributes = createLayout([
{name: 'a_placed', components: 2, type: 'Uint8'},
{name: 'a_shift', components: 2, type: 'Float32'},
{name: 'a_box_real', components: 2, type: 'Int16'},
]);
export const collisionBox = createLayout([
// the box is centered around the anchor point
{type: 'Int16', name: 'anchorPointX'},
{type: 'Int16', name: 'anchorPointY'},
// distances to the edges from the anchor
{type: 'Int16', name: 'x1'},
{type: 'Int16', name: 'y1'},
{type: 'Int16', name: 'x2'},
{type: 'Int16', name: 'y2'},
// the index of the feature in the original vectortile
{type: 'Uint32', name: 'featureIndex'},
// the source layer the feature appears in
{type: 'Uint16', name: 'sourceLayerIndex'},
// the bucket the feature appears in
{type: 'Uint16', name: 'bucketIndex'},
]);
export const collisionBoxLayout = createLayout([ // used to render collision boxes for debugging purposes
{name: 'a_pos', components: 2, type: 'Int16'},
{name: 'a_anchor_pos', components: 2, type: 'Int16'},
{name: 'a_extrude', components: 2, type: 'Int16'}
], 4);
export const collisionCircleLayout = createLayout([ // used to render collision circles for debugging purposes
{name: 'a_pos', components: 2, type: 'Float32'},
{name: 'a_radius', components: 1, type: 'Float32'},
{name: 'a_flags', components: 2, type: 'Int16'}
], 4);
export const quadTriangle = createLayout([
{name: 'triangle', components: 3, type: 'Uint16'},
]);
export const placement = createLayout([
{type: 'Int16', name: 'anchorX'},
{type: 'Int16', name: 'anchorY'},
{type: 'Uint16', name: 'glyphStartIndex'},
{type: 'Uint16', name: 'numGlyphs'},
{type: 'Uint32', name: 'vertexStartIndex'},
{type: 'Uint32', name: 'lineStartIndex'},
{type: 'Uint32', name: 'lineLength'},
{type: 'Uint16', name: 'segment'},
{type: 'Uint16', name: 'lowerSize'},
{type: 'Uint16', name: 'upperSize'},
{type: 'Float32', name: 'lineOffsetX'},
{type: 'Float32', name: 'lineOffsetY'},
{type: 'Uint8', name: 'writingMode'},
{type: 'Uint8', name: 'placedOrientation'},
{type: 'Uint8', name: 'hidden'},
{type: 'Uint32', name: 'crossTileID'},
{type: 'Int16', name: 'associatedIconIndex'}
]);
export const symbolInstance = createLayout([
{type: 'Int16', name: 'anchorX'},
{type: 'Int16', name: 'anchorY'},
{type: 'Int16', name: 'rightJustifiedTextSymbolIndex'},
{type: 'Int16', name: 'centerJustifiedTextSymbolIndex'},
{type: 'Int16', name: 'leftJustifiedTextSymbolIndex'},
{type: 'Int16', name: 'verticalPlacedTextSymbolIndex'},
{type: 'Int16', name: 'placedIconSymbolIndex'},
{type: 'Int16', name: 'verticalPlacedIconSymbolIndex'},
{type: 'Uint16', name: 'key'},
{type: 'Uint16', name: 'textBoxStartIndex'},
{type: 'Uint16', name: 'textBoxEndIndex'},
{type: 'Uint16', name: 'verticalTextBoxStartIndex'},
{type: 'Uint16', name: 'verticalTextBoxEndIndex'},
{type: 'Uint16', name: 'iconBoxStartIndex'},
{type: 'Uint16', name: 'iconBoxEndIndex'},
{type: 'Uint16', name: 'verticalIconBoxStartIndex'},
{type: 'Uint16', name: 'verticalIconBoxEndIndex'},
{type: 'Uint16', name: 'featureIndex'},
{type: 'Uint16', name: 'numHorizontalGlyphVertices'},
{type: 'Uint16', name: 'numVerticalGlyphVertices'},
{type: 'Uint16', name: 'numIconVertices'},
{type: 'Uint16', name: 'numVerticalIconVertices'},
{type: 'Uint16', name: 'useRuntimeCollisionCircles'},
{type: 'Uint32', name: 'crossTileID'},
{type: 'Float32', name: 'textBoxScale'},
{type: 'Float32', name: 'collisionCircleDiameter'},
{type: 'Uint16', name: 'textAnchorOffsetStartIndex'},
{type: 'Uint16', name: 'textAnchorOffsetEndIndex'}
]);
export const glyphOffset = createLayout([
{type: 'Float32', name: 'offsetX'}
]);
export const lineVertex = createLayout([
{type: 'Int16', name: 'x'},
{type: 'Int16', name: 'y'},
{type: 'Int16', name: 'tileUnitDistanceFromAnchor'}
]);
export const textAnchorOffset = createLayout([
{type: 'Uint16', name: 'textAnchor'},
{type: 'Float32', components: 2, name: 'textOffset'}
]);

View File

@@ -0,0 +1,244 @@
import {describe, test, expect, vi, beforeAll} from 'vitest';
import {SymbolBucket} from './symbol_bucket';
import {CollisionBoxArray} from '../../data/array_types.g';
import {performSymbolLayout} from '../../symbol/symbol_layout';
import {Placement} from '../../symbol/placement';
import {type CanonicalTileID, OverscaledTileID} from '../../tile/tile_id';
import {Tile} from '../../tile/tile';
import {CrossTileSymbolIndex} from '../../symbol/cross_tile_symbol_index';
import {FeatureIndex} from '../../data/feature_index';
import {createSymbolBucket, createSymbolIconBucket} from '../../../test/unit/lib/create_symbol_layer';
import {RGBAImage} from '../../util/image';
import {ImagePosition} from '../../render/image_atlas';
import {type IndexedFeature, type PopulateParameters} from '../bucket';
import {type StyleImage} from '../../style/style_image';
import glyphs from '../../../test/unit/assets/fontstack-glyphs.json' with {type: 'json'};
import {type StyleGlyph} from '../../style/style_glyph';
import {SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {MercatorTransform} from '../../geo/projection/mercator_transform';
import {createPopulateOptions, loadVectorTile} from '../../../test/unit/lib/tile';
const collisionBoxArray = new CollisionBoxArray();
const transform = new MercatorTransform();
transform.resize(100, 100);
const stacks = {'Test': glyphs} as any as {
[_: string]: {
[x: number]: StyleGlyph;
};
};
function bucketSetup(text = 'abcde') {
return createSymbolBucket('test', 'Test', text, collisionBoxArray);
}
function createIndexedFeature(id: number, index: number, iconId: string): IndexedFeature {
return {
feature: {
extent: 8192,
type: 1,
id,
properties: {
icon: iconId
},
loadGeometry() {
return [[{x: 0, y: 0}]];
}
},
id,
index,
sourceLayerIndex: 0
} as any as IndexedFeature;
}
describe('SymbolBucket', () => {
let features: IndexedFeature[];
beforeAll(() => {
// Load point features from fixture tile.
const sourceLayer = loadVectorTile().layers.place_label;
features = [{feature: sourceLayer.feature(10)} as unknown as IndexedFeature];
});
test('SymbolBucket', () => {
const bucketA = bucketSetup();
const bucketB = bucketSetup();
const options = createPopulateOptions([]);
const placement = new Placement(transform, undefined as any, 0, true);
const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
const crossTileSymbolIndex = new CrossTileSymbolIndex();
// add feature from bucket A
bucketA.populate(features, options, undefined as any);
performSymbolLayout(
{
bucket: bucketA,
glyphMap: stacks,
glyphPositions: {},
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);
const tileA = new Tile(tileID, 512);
tileA.latestFeatureIndex = new FeatureIndex(tileID);
tileA.buckets = {test: bucketA};
tileA.collisionBoxArray = collisionBoxArray;
// add same feature from bucket B
bucketB.populate(features, options, undefined as any);
performSymbolLayout({
bucket: bucketB, glyphMap: stacks, glyphPositions: {}, subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);
const tileB = new Tile(tileID, 512);
tileB.buckets = {test: bucketB};
tileB.collisionBoxArray = collisionBoxArray;
crossTileSymbolIndex.addLayer(bucketA.layers[0], [tileA, tileB], undefined as any);
const place = (layer, tile) => {
const parts = [];
placement.getBucketParts(parts, layer, tile, false);
for (const part of parts) {
placement.placeLayerBucketPart(part, {}, false);
}
};
const a = placement.collisionIndex.grid.keysLength();
place(bucketA.layers[0], tileA);
const b = placement.collisionIndex.grid.keysLength();
expect(a).not.toBe(b);
const a2 = placement.collisionIndex.grid.keysLength();
place(bucketB.layers[0], tileB);
const b2 = placement.collisionIndex.grid.keysLength();
expect(b2).toBe(a2);
});
test('SymbolBucket integer overflow', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => { });
SymbolBucket.MAX_GLYPHS = 5;
const bucket = bucketSetup();
const options = {iconDependencies: {}, glyphDependencies: {}} as PopulateParameters;
bucket.populate(features, options, undefined as any);
const fakeGlyph = {rect: {w: 10, h: 10}, metrics: {left: 10, top: 10, advance: 10}};
performSymbolLayout({
bucket,
glyphMap: stacks,
glyphPositions: {'Test': {97: fakeGlyph, 98: fakeGlyph, 99: fakeGlyph, 100: fakeGlyph, 101: fakeGlyph, 102: fakeGlyph} as any},
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].includes('Too many glyphs being rendered in a tile.')).toBeTruthy();
});
test('SymbolBucket image undefined sdf', () => {
const spy = vi.spyOn(console, 'warn').mockImplementation(() => { });
spy.mockReset();
const imageMap = {
a: {
data: new RGBAImage({width: 0, height: 0})
},
b: {
data: new RGBAImage({width: 0, height: 0}),
sdf: false
}
} as any as { [_: string]: StyleImage };
const imagePos = {
a: new ImagePosition({x: 0, y: 0, w: 10, h: 10}, 1 as any as StyleImage),
b: new ImagePosition({x: 10, y: 0, w: 10, h: 10}, 1 as any as StyleImage)
};
const bucket = createSymbolIconBucket('test', 'icon', collisionBoxArray);
const options = createPopulateOptions([]);
bucket.populate(
[
createIndexedFeature(0, 0, 'a'),
createIndexedFeature(1, 1, 'b'),
createIndexedFeature(2, 2, 'a')
] as any as IndexedFeature[],
options, undefined as any
);
const icons = options.iconDependencies as any;
expect(icons.a).toBe(true);
expect(icons.b).toBe(true);
performSymbolLayout({
bucket, imageMap, imagePositions: imagePos,
subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision
} as any);
// undefined SDF should be treated the same as false SDF - no warning raised
expect(spy).not.toHaveBeenCalledTimes(1);
});
test('SymbolBucket image mismatched sdf', () => {
const originalWarn = console.warn;
console.warn = vi.fn();
const imageMap = {
a: {
data: new RGBAImage({width: 0, height: 0}),
sdf: true
},
b: {
data: new RGBAImage({width: 0, height: 0}),
sdf: false
}
} as any as { [_: string]: StyleImage };
const imagePos = {
a: new ImagePosition({x: 0, y: 0, w: 10, h: 10}, 1 as any as StyleImage),
b: new ImagePosition({x: 10, y: 0, w: 10, h: 10}, 1 as any as StyleImage)
};
const bucket = createSymbolIconBucket('test', 'icon', collisionBoxArray);
const options = createPopulateOptions([]);
bucket.populate(
[
createIndexedFeature(0, 0, 'a'),
createIndexedFeature(1, 1, 'b'),
createIndexedFeature(2, 2, 'a')
] as any as IndexedFeature[],
options, undefined as unknown as CanonicalTileID
);
const icons = options.iconDependencies as any;
expect(icons.a).toBe(true);
expect(icons.b).toBe(true);
performSymbolLayout({bucket, imageMap, imagePositions: imagePos, subdivisionGranularity: SubdivisionGranularitySetting.noSubdivision} as any);
// true SDF and false SDF in same bucket should trigger warning
expect(console.warn).toHaveBeenCalledTimes(1);
console.warn = originalWarn;
});
test('SymbolBucket detects rtl text', () => {
const rtlBucket = bucketSetup('مرحبا');
const ltrBucket = bucketSetup('hello');
const options = createPopulateOptions([]);
rtlBucket.populate(features, options, undefined as any);
ltrBucket.populate(features, options, undefined as any);
expect(rtlBucket.hasRTLText).toBeTruthy();
expect(ltrBucket.hasRTLText).toBeFalsy();
});
// Test to prevent symbol bucket with rtl from text being culled by worker serialization.
test('SymbolBucket with rtl text is NOT empty even though no symbol instances are created', () => {
const rtlBucket = bucketSetup('مرحبا');
const options = createPopulateOptions([]);
rtlBucket.createArrays();
rtlBucket.populate(features, options, undefined as any);
expect(rtlBucket.isEmpty()).toBeFalsy();
expect(rtlBucket.symbolInstances).toHaveLength(0);
});
test('SymbolBucket detects rtl text mixed with ltr text', () => {
const mixedBucket = bucketSetup('مرحبا translates to hello');
const options = createPopulateOptions([]);
mixedBucket.populate(features, options, undefined as any);
expect(mixedBucket.hasRTLText).toBeTruthy();
});
});

View File

@@ -0,0 +1,973 @@
import {
symbolLayoutAttributes,
collisionVertexAttributes,
collisionBoxLayout,
dynamicLayoutAttributes,
} from './symbol_attributes';
import {SymbolLayoutArray,
SymbolDynamicLayoutArray,
SymbolOpacityArray,
CollisionBoxLayoutArray,
CollisionVertexArray,
PlacedSymbolArray,
SymbolInstanceArray,
GlyphOffsetArray,
SymbolLineVertexArray,
TextAnchorOffsetArray
} from '../array_types.g';
import Point from '@mapbox/point-geometry';
import {SegmentVector} from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray, LineIndexArray} from '../index_array_type';
import {transformText} from '../../symbol/transform_text';
import {mergeLines} from '../../symbol/merge_lines';
import {allowsVerticalWritingMode, stringContainsRTLText} from '../../util/script_detection';
import {WritingMode} from '../../symbol/shaping';
import {loadGeometry} from '../load_geometry';
import {toEvaluationFeature} from '../evaluation_feature';
import {VectorTileFeature} from '@mapbox/vector-tile';
import {verticalizedCharacterMap} from '../../util/verticalize_punctuation';
import {type Anchor} from '../../symbol/anchor';
import {getSizeData, MAX_PACKED_SIZE} from '../../symbol/symbol_size';
import {register} from '../../util/web_worker_transfer';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import {Formatted, ResolvedImage} from '@maplibre/maplibre-gl-style-spec';
import {rtlWorkerPlugin} from '../../source/rtl_text_plugin_worker';
import {getOverlapMode} from '../../style/style_layer/overlap_mode';
import type {CanonicalTileID} from '../../tile/tile_id';
import type {
Bucket,
BucketParameters,
IndexedFeature,
PopulateParameters
} from '../bucket';
import type {CollisionBoxArray, CollisionBox, SymbolInstance} from '../array_types.g';
import type {StructArray, StructArrayMember, ViewType} from '../../util/struct_array';
import type {SymbolStyleLayer} from '../../style/style_layer/symbol_style_layer';
import type {Context} from '../../gl/context';
import type {IndexBuffer} from '../../gl/index_buffer';
import type {VertexBuffer} from '../../gl/vertex_buffer';
import type {SymbolQuad} from '../../symbol/quads';
import type {SizeData} from '../../symbol/symbol_size';
import type {FeatureStates} from '../../source/source_state';
import type {ImagePosition} from '../../render/image_atlas';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
export type SingleCollisionBox = {
x1: number;
y1: number;
x2: number;
y2: number;
anchorPointX: number;
anchorPointY: number;
};
export type CollisionArrays = {
textBox?: SingleCollisionBox;
verticalTextBox?: SingleCollisionBox;
iconBox?: SingleCollisionBox;
verticalIconBox?: SingleCollisionBox;
textFeatureIndex?: number;
verticalTextFeatureIndex?: number;
iconFeatureIndex?: number;
verticalIconFeatureIndex?: number;
};
export type SymbolFeature = {
sortKey: number | void;
text: Formatted | void;
icon: ResolvedImage;
index: number;
sourceLayerIndex: number;
geometry: Array<Array<Point>>;
properties: any;
type: 'Unknown' | 'Point' | 'LineString' | 'Polygon';
id?: any;
};
export type SortKeyRange = {
sortKey: number;
symbolInstanceStart: number;
symbolInstanceEnd: number;
};
// Opacity arrays are frequently updated but don't contain a lot of information, so we pack them
// tight. Each Uint32 is actually four duplicate Uint8s for the four corners of a glyph
// 7 bits are for the current opacity, and the lowest bit is the target opacity
// actually defined in symbol_attributes.js
// const placementOpacityAttributes = [
// { name: 'a_fade_opacity', components: 1, type: 'Uint32' }
// ];
const shaderOpacityAttributes = [
{name: 'a_fade_opacity', components: 1, type: 'Uint8' as ViewType, offset: 0}
];
function addVertex(
array: StructArray,
anchorX: number,
anchorY: number,
ox: number,
oy: number,
tx: number,
ty: number,
sizeVertex: number,
isSDF: boolean,
pixelOffsetX: number,
pixelOffsetY: number,
minFontScaleX: number,
minFontScaleY: number
) {
const aSizeX = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[0])) : 0;
const aSizeY = sizeVertex ? Math.min(MAX_PACKED_SIZE, Math.round(sizeVertex[1])) : 0;
array.emplaceBack(
// a_pos_offset
anchorX,
anchorY,
Math.round(ox * 32),
Math.round(oy * 32),
// a_data
tx, // x coordinate of symbol on glyph atlas texture
ty, // y coordinate of symbol on glyph atlas texture
(aSizeX << 1) + (isSDF ? 1 : 0),
aSizeY,
pixelOffsetX * 16,
pixelOffsetY * 16,
minFontScaleX * 256,
minFontScaleY * 256
);
}
function addDynamicAttributes(dynamicLayoutVertexArray: StructArray, p: Point, angle: number) {
dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
dynamicLayoutVertexArray.emplaceBack(p.x, p.y, angle);
}
function containsRTLText(formattedText: Formatted): boolean {
for (const section of formattedText.sections) {
if (stringContainsRTLText(section.text)) {
return true;
}
}
return false;
}
export class SymbolBuffers {
layoutVertexArray: SymbolLayoutArray;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
programConfigurations: ProgramConfigurationSet<SymbolStyleLayer>;
segments: SegmentVector;
dynamicLayoutVertexArray: SymbolDynamicLayoutArray;
dynamicLayoutVertexBuffer: VertexBuffer;
opacityVertexArray: SymbolOpacityArray;
opacityVertexBuffer: VertexBuffer;
hasVisibleVertices: boolean;
collisionVertexArray: CollisionVertexArray;
collisionVertexBuffer: VertexBuffer;
placedSymbolArray: PlacedSymbolArray;
constructor(programConfigurations: ProgramConfigurationSet<SymbolStyleLayer>) {
this.layoutVertexArray = new SymbolLayoutArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = programConfigurations;
this.segments = new SegmentVector();
this.dynamicLayoutVertexArray = new SymbolDynamicLayoutArray();
this.opacityVertexArray = new SymbolOpacityArray();
this.hasVisibleVertices = false;
this.placedSymbolArray = new PlacedSymbolArray();
}
isEmpty() {
return this.layoutVertexArray.length === 0 &&
this.indexArray.length === 0 &&
this.dynamicLayoutVertexArray.length === 0 &&
this.opacityVertexArray.length === 0;
}
upload(context: Context, dynamicIndexBuffer: boolean, upload?: boolean, update?: boolean) {
if (this.isEmpty()) {
return;
}
if (upload) {
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, symbolLayoutAttributes.members);
this.indexBuffer = context.createIndexBuffer(this.indexArray, dynamicIndexBuffer);
this.dynamicLayoutVertexBuffer = context.createVertexBuffer(this.dynamicLayoutVertexArray, dynamicLayoutAttributes.members, true);
this.opacityVertexBuffer = context.createVertexBuffer(this.opacityVertexArray, shaderOpacityAttributes, true);
// This is a performance hack so that we can write to opacityVertexArray with uint32s
// even though the shaders read uint8s
this.opacityVertexBuffer.itemSize = 1;
}
if (upload || update) {
this.programConfigurations.upload(context);
}
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.programConfigurations.destroy();
this.segments.destroy();
this.dynamicLayoutVertexBuffer.destroy();
this.opacityVertexBuffer.destroy();
}
}
register('SymbolBuffers', SymbolBuffers);
class CollisionBuffers {
layoutVertexArray: StructArray;
layoutAttributes: Array<StructArrayMember>;
layoutVertexBuffer: VertexBuffer;
indexArray: TriangleIndexArray | LineIndexArray;
indexBuffer: IndexBuffer;
segments: SegmentVector;
collisionVertexArray: CollisionVertexArray;
collisionVertexBuffer: VertexBuffer;
constructor(LayoutArray: {
new (...args: any): StructArray;
},
layoutAttributes: Array<StructArrayMember>,
IndexArray: {
new (...args: any): TriangleIndexArray | LineIndexArray;
}) {
this.layoutVertexArray = new LayoutArray();
this.layoutAttributes = layoutAttributes;
this.indexArray = new IndexArray();
this.segments = new SegmentVector();
this.collisionVertexArray = new CollisionVertexArray();
}
upload(context: Context) {
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, this.layoutAttributes);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
this.collisionVertexBuffer = context.createVertexBuffer(this.collisionVertexArray, collisionVertexAttributes.members, true);
}
destroy() {
if (!this.layoutVertexBuffer) return;
this.layoutVertexBuffer.destroy();
this.indexBuffer.destroy();
this.segments.destroy();
this.collisionVertexBuffer.destroy();
}
}
register('CollisionBuffers', CollisionBuffers);
/**
* @internal
* Unlike other buckets, which simply implement `addFeature` with type-specific
* logic for (essentially) triangulating feature geometries, SymbolBucket
* requires specialized behavior:
*
* 1. WorkerTile.parse(), the logical owner of the bucket creation process,
* calls SymbolBucket.populate(), which resolves text and icon tokens on
* each feature, adds each glyphs and symbols needed to the passed-in
* collections options.glyphDependencies and options.iconDependencies, and
* stores the feature data for use in subsequent step (this.features).
*
* 2. WorkerTile asynchronously requests from the main thread all of the glyphs
* and icons needed (by this bucket and any others). When glyphs and icons
* have been received, the WorkerTile creates a CollisionIndex and invokes:
*
* 3. performSymbolLayout(bucket, stacks, icons) perform texts shaping and
* layout on a Symbol Bucket. This step populates:
* `this.symbolInstances`: metadata on generated symbols
* `this.collisionBoxArray`: collision data for use by foreground
* `this.text`: SymbolBuffers for text symbols
* `this.icons`: SymbolBuffers for icons
* `this.iconCollisionBox`: Debug SymbolBuffers for icon collision boxes
* `this.textCollisionBox`: Debug SymbolBuffers for text collision boxes
* The results are sent to the foreground for rendering
*
* 4. placement.ts is run on the foreground,
* and uses the CollisionIndex along with current camera settings to determine
* which symbols can actually show on the map. Collided symbols are hidden
* using a dynamic "OpacityVertexArray".
*/
export class SymbolBucket implements Bucket {
static MAX_GLYPHS: number;
static addDynamicAttributes: typeof addDynamicAttributes;
collisionBoxArray: CollisionBoxArray;
zoom: number;
overscaling: number;
layers: Array<SymbolStyleLayer>;
layerIds: Array<string>;
stateDependentLayers: Array<SymbolStyleLayer>;
stateDependentLayerIds: Array<string>;
index: number;
sdfIcons: boolean;
iconsInText: boolean;
iconsNeedLinear: boolean;
bucketInstanceId: number;
justReloaded: boolean;
hasDependencies: boolean;
textSizeData: SizeData;
iconSizeData: SizeData;
glyphOffsetArray: GlyphOffsetArray;
lineVertexArray: SymbolLineVertexArray;
features: Array<SymbolFeature>;
symbolInstances: SymbolInstanceArray;
textAnchorOffsets: TextAnchorOffsetArray;
collisionArrays: Array<CollisionArrays>;
sortKeyRanges: Array<SortKeyRange>;
pixelRatio: number;
tilePixelRatio: number;
compareText: {[_: string]: Array<Point>};
fadeStartTime: number;
sortFeaturesByKey: boolean;
sortFeaturesByY: boolean;
canOverlap: boolean;
sortedAngle: number;
featureSortOrder: Array<number>;
collisionCircleArray: Array<number>;
text: SymbolBuffers;
icon: SymbolBuffers;
textCollisionBox: CollisionBuffers;
iconCollisionBox: CollisionBuffers;
uploaded: boolean;
sourceLayerIndex: number;
sourceID: string;
symbolInstanceIndexes: Array<number>;
writingModes: WritingMode[];
allowVerticalPlacement: boolean;
hasRTLText: boolean;
constructor(options: BucketParameters<SymbolStyleLayer>) {
this.collisionBoxArray = options.collisionBoxArray;
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layers = options.layers;
this.layerIds = this.layers.map(layer => layer.id);
this.index = options.index;
this.pixelRatio = options.pixelRatio;
this.sourceLayerIndex = options.sourceLayerIndex;
this.hasDependencies = false;
this.hasRTLText = false;
this.sortKeyRanges = [];
this.collisionCircleArray = [];
const layer = this.layers[0];
const unevaluatedLayoutValues = layer._unevaluatedLayout._values;
this.textSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['text-size']);
this.iconSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['icon-size']);
const layout = this.layers[0].layout;
const sortKey = layout.get('symbol-sort-key');
const zOrder = layout.get('symbol-z-order');
this.canOverlap =
getOverlapMode(layout, 'text-overlap', 'text-allow-overlap') !== 'never' ||
getOverlapMode(layout, 'icon-overlap', 'icon-allow-overlap') !== 'never' ||
layout.get('text-ignore-placement') ||
layout.get('icon-ignore-placement');
this.sortFeaturesByKey = zOrder !== 'viewport-y' && !sortKey.isConstant();
const zOrderByViewportY = zOrder === 'viewport-y' || (zOrder === 'auto' && !this.sortFeaturesByKey);
this.sortFeaturesByY = zOrderByViewportY && this.canOverlap;
if (layout.get('symbol-placement') === 'point') {
this.writingModes = layout.get('text-writing-mode').map(wm => WritingMode[wm]);
}
this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
this.sourceID = options.sourceID;
}
createArrays() {
this.text = new SymbolBuffers(new ProgramConfigurationSet(this.layers, this.zoom, property => /^text/.test(property)));
this.icon = new SymbolBuffers(new ProgramConfigurationSet(this.layers, this.zoom, property => /^icon/.test(property)));
this.glyphOffsetArray = new GlyphOffsetArray();
this.lineVertexArray = new SymbolLineVertexArray();
this.symbolInstances = new SymbolInstanceArray();
this.textAnchorOffsets = new TextAnchorOffsetArray();
}
private calculateGlyphDependencies(
text: string,
stack: {[_: number]: boolean},
textAlongLine: boolean,
allowVerticalPlacement: boolean,
doesAllowVerticalWritingMode: boolean) {
for (const char of text) {
stack[char.codePointAt(0)] = true;
if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) {
const verticalChar = verticalizedCharacterMap[char];
if (verticalChar) {
stack[verticalChar.codePointAt(0)] = true;
}
}
}
}
populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID) {
const layer = this.layers[0];
const layout = layer.layout;
const textFont = layout.get('text-font');
const textField = layout.get('text-field');
const iconImage = layout.get('icon-image');
const hasText =
(textField.value.kind !== 'constant' ||
(textField.value.value instanceof Formatted && !textField.value.value.isEmpty()) ||
textField.value.value.toString().length > 0) &&
(textFont.value.kind !== 'constant' || textFont.value.value.length > 0);
// we should always resolve the icon-image value if the property was defined in the style
// this allows us to fire the styleimagemissing event if image evaluation returns null
// the only way to distinguish between null returned from a coalesce statement with no valid images
// and null returned because icon-image wasn't defined is to check whether or not iconImage.parameters is an empty object
const hasIcon = iconImage.value.kind !== 'constant' || !!iconImage.value.value || Object.keys(iconImage.parameters).length > 0;
const symbolSortKey = layout.get('symbol-sort-key');
this.features = [];
if (!hasText && !hasIcon) {
return;
}
const icons = options.iconDependencies;
const stacks = options.glyphDependencies;
const availableImages = options.availableImages;
const globalProperties = new EvaluationParameters(this.zoom);
for (const {feature, id, index, sourceLayerIndex} of features) {
const needGeometry = layer._featureFilter.needGeometry;
const evaluationFeature = toEvaluationFeature(feature, needGeometry);
if (!layer._featureFilter.filter(globalProperties, evaluationFeature, canonical)) {
continue;
}
if (!needGeometry) evaluationFeature.geometry = loadGeometry(feature);
let text: Formatted | void;
if (hasText) {
// Expression evaluation will automatically coerce to Formatted
// but plain string token evaluation skips that pathway so do the
// conversion here.
const resolvedTokens = layer.getValueAndResolveTokens('text-field', evaluationFeature, canonical, availableImages);
const formattedText = Formatted.factory(resolvedTokens);
// on this instance: if hasRTLText is already true, all future calls to containsRTLText can be skipped.
const bucketHasRTLText = this.hasRTLText = (this.hasRTLText || containsRTLText(formattedText));
if (
!bucketHasRTLText || // non-rtl text so can proceed safely
rtlWorkerPlugin.getRTLTextPluginStatus() === 'unavailable' || // We don't intend to lazy-load the rtl text plugin, so proceed with incorrect shaping
bucketHasRTLText && rtlWorkerPlugin.isParsed() // Use the rtlText plugin to shape text
) {
text = transformText(formattedText, layer, evaluationFeature);
}
}
let icon: ResolvedImage;
if (hasIcon) {
// Expression evaluation will automatically coerce to Image
// but plain string token evaluation skips that pathway so do the
// conversion here.
const resolvedTokens = layer.getValueAndResolveTokens('icon-image', evaluationFeature, canonical, availableImages);
if (resolvedTokens instanceof ResolvedImage) {
icon = resolvedTokens;
} else {
icon = ResolvedImage.fromString(resolvedTokens);
}
}
if (!text && !icon) {
continue;
}
const sortKey = this.sortFeaturesByKey ?
symbolSortKey.evaluate(evaluationFeature, {}, canonical) :
undefined;
const symbolFeature: SymbolFeature = {
id,
text,
icon,
index,
sourceLayerIndex,
geometry: evaluationFeature.geometry,
properties: feature.properties,
type: VectorTileFeature.types[feature.type],
sortKey
};
this.features.push(symbolFeature);
if (icon) {
icons[icon.name] = true;
}
if (text) {
const fontStack = textFont.evaluate(evaluationFeature, {}, canonical).join(',');
const textAlongLine = layout.get('text-rotation-alignment') !== 'viewport' && layout.get('symbol-placement') !== 'point';
this.allowVerticalPlacement = this.writingModes && this.writingModes.indexOf(WritingMode.vertical) >= 0;
for (const section of text.sections) {
if (!section.image) {
const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString());
const sectionFont = section.fontStack || fontStack;
const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {};
this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, this.allowVerticalPlacement, doesAllowVerticalWritingMode);
} else {
// Add section image to the list of dependencies.
icons[section.image.name] = true;
}
}
}
}
if (layout.get('symbol-placement') === 'line') {
// Merge adjacent lines with the same text to improve labelling.
// It's better to place labels on one long line than on many short segments.
this.features = mergeLines(this.features);
}
if (this.sortFeaturesByKey) {
this.features.sort((a, b) => {
// a.sortKey is always a number when sortFeaturesByKey is true
return (a.sortKey as number) - (b.sortKey as number);
});
}
}
update(states: FeatureStates, vtLayer: VectorTileLayerLike, imagePositions: {[_: string]: ImagePosition}) {
if (!this.stateDependentLayers.length) return;
this.text.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, {
imagePositions
});
this.icon.programConfigurations.updatePaintArrays(states, vtLayer, this.layers, {
imagePositions
});
}
isEmpty() {
// When the bucket encounters only rtl-text but the plugin isn't loaded, no symbol instances will be created.
// In order for the bucket to be serialized, and not discarded as an empty bucket both checks are necessary.
return this.symbolInstances.length === 0 && !this.hasRTLText;
}
uploadPending() {
return !this.uploaded || this.text.programConfigurations.needsUpload || this.icon.programConfigurations.needsUpload;
}
upload(context: Context) {
if (!this.uploaded && this.hasDebugData()) {
this.textCollisionBox.upload(context);
this.iconCollisionBox.upload(context);
}
this.text.upload(context, this.sortFeaturesByY, !this.uploaded, this.text.programConfigurations.needsUpload);
this.icon.upload(context, this.sortFeaturesByY, !this.uploaded, this.icon.programConfigurations.needsUpload);
this.uploaded = true;
}
destroyDebugData() {
this.textCollisionBox.destroy();
this.iconCollisionBox.destroy();
}
destroy() {
this.text.destroy();
this.icon.destroy();
if (this.hasDebugData()) {
this.destroyDebugData();
}
}
addToLineVertexArray(anchor: Anchor, line: Array<Point>) {
const lineStartIndex = this.lineVertexArray.length;
if (anchor.segment !== undefined) {
let sumForwardLength = anchor.dist(line[anchor.segment + 1]);
let sumBackwardLength = anchor.dist(line[anchor.segment]);
const vertices = {};
for (let i = anchor.segment + 1; i < line.length; i++) {
vertices[i] = {x: line[i].x, y: line[i].y, tileUnitDistanceFromAnchor: sumForwardLength};
if (i < line.length - 1) {
sumForwardLength += line[i + 1].dist(line[i]);
}
}
for (let i = anchor.segment || 0; i >= 0; i--) {
vertices[i] = {x: line[i].x, y: line[i].y, tileUnitDistanceFromAnchor: sumBackwardLength};
if (i > 0) {
sumBackwardLength += line[i - 1].dist(line[i]);
}
}
for (let i = 0; i < line.length; i++) {
const vertex = vertices[i];
this.lineVertexArray.emplaceBack(vertex.x, vertex.y, vertex.tileUnitDistanceFromAnchor);
}
}
return {
lineStartIndex,
lineLength: this.lineVertexArray.length - lineStartIndex
};
}
addSymbols(arrays: SymbolBuffers,
quads: Array<SymbolQuad>,
sizeVertex: any,
lineOffset: [number, number],
alongLine: boolean,
feature: SymbolFeature,
writingMode: WritingMode,
labelAnchor: Anchor,
lineStartIndex: number,
lineLength: number,
associatedIconIndex: number,
canonical: CanonicalTileID) {
const indexArray = arrays.indexArray;
const layoutVertexArray = arrays.layoutVertexArray;
const segment = arrays.segments.prepareSegment(4 * quads.length, layoutVertexArray, indexArray, this.canOverlap ? feature.sortKey as number : undefined);
const glyphOffsetArrayStart = this.glyphOffsetArray.length;
const vertexStartIndex = segment.vertexLength;
const angle = (this.allowVerticalPlacement && writingMode === WritingMode.vertical) ? Math.PI / 2 : 0;
const sections = feature.text && feature.text.sections;
for (let i = 0; i < quads.length; i++) {
const {tl, tr, bl, br, tex, pixelOffsetTL, pixelOffsetBR, minFontScaleX, minFontScaleY, glyphOffset, isSDF, sectionIndex} = quads[i];
const index = segment.vertexLength;
const y = glyphOffset[1];
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tl.x, y + tl.y, tex.x, tex.y, sizeVertex, isSDF, pixelOffsetTL.x, pixelOffsetTL.y, minFontScaleX, minFontScaleY);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, tr.x, y + tr.y, tex.x + tex.w, tex.y, sizeVertex, isSDF, pixelOffsetBR.x, pixelOffsetTL.y, minFontScaleX, minFontScaleY);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, bl.x, y + bl.y, tex.x, tex.y + tex.h, sizeVertex, isSDF, pixelOffsetTL.x, pixelOffsetBR.y, minFontScaleX, minFontScaleY);
addVertex(layoutVertexArray, labelAnchor.x, labelAnchor.y, br.x, y + br.y, tex.x + tex.w, tex.y + tex.h, sizeVertex, isSDF, pixelOffsetBR.x, pixelOffsetBR.y, minFontScaleX, minFontScaleY);
addDynamicAttributes(arrays.dynamicLayoutVertexArray, labelAnchor, angle);
indexArray.emplaceBack(index, index + 2, index + 1);
indexArray.emplaceBack(index + 1, index + 2, index + 3);
segment.vertexLength += 4;
segment.primitiveLength += 2;
this.glyphOffsetArray.emplaceBack(glyphOffset[0]);
if (i === quads.length - 1 || sectionIndex !== quads[i + 1].sectionIndex) {
arrays.programConfigurations.populatePaintArrays(layoutVertexArray.length, feature, feature.index, {imagePositions: {}, canonical, formattedSection: sections && sections[sectionIndex]});
}
}
arrays.placedSymbolArray.emplaceBack(
labelAnchor.x, labelAnchor.y,
glyphOffsetArrayStart,
this.glyphOffsetArray.length - glyphOffsetArrayStart,
vertexStartIndex,
lineStartIndex,
lineLength,
labelAnchor.segment,
sizeVertex ? sizeVertex[0] : 0,
sizeVertex ? sizeVertex[1] : 0,
lineOffset[0], lineOffset[1],
writingMode,
// placedOrientation is null initially; will be updated to horizontal(1)/vertical(2) if placed
0,
false as unknown as number,
// The crossTileID is only filled/used on the foreground for dynamic text anchors
0,
associatedIconIndex
);
}
_addCollisionDebugVertex(layoutVertexArray: StructArray, collisionVertexArray: StructArray, point: Point, anchorX: number, anchorY: number, extrude: Point) {
collisionVertexArray.emplaceBack(0, 0);
return layoutVertexArray.emplaceBack(
// pos
point.x,
point.y,
// a_anchor_pos
anchorX,
anchorY,
// extrude
Math.round(extrude.x),
Math.round(extrude.y));
}
addCollisionDebugVertices(x1: number, y1: number, x2: number, y2: number, arrays: CollisionBuffers, boxAnchorPoint: Point, symbolInstance: SymbolInstance) {
const segment = arrays.segments.prepareSegment(4, arrays.layoutVertexArray, arrays.indexArray);
const index = segment.vertexLength;
const layoutVertexArray = arrays.layoutVertexArray;
const collisionVertexArray = arrays.collisionVertexArray;
const anchorX = symbolInstance.anchorX;
const anchorY = symbolInstance.anchorY;
this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y1));
this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x2, y1));
this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x2, y2));
this._addCollisionDebugVertex(layoutVertexArray, collisionVertexArray, boxAnchorPoint, anchorX, anchorY, new Point(x1, y2));
segment.vertexLength += 4;
const indexArray = arrays.indexArray as LineIndexArray;
indexArray.emplaceBack(index, index + 1);
indexArray.emplaceBack(index + 1, index + 2);
indexArray.emplaceBack(index + 2, index + 3);
indexArray.emplaceBack(index + 3, index);
segment.primitiveLength += 4;
}
addDebugCollisionBoxes(startIndex: number, endIndex: number, symbolInstance: SymbolInstance, isText: boolean) {
for (let b = startIndex; b < endIndex; b++) {
const box: CollisionBox = this.collisionBoxArray.get(b);
const x1 = box.x1;
const y1 = box.y1;
const x2 = box.x2;
const y2 = box.y2;
this.addCollisionDebugVertices(x1, y1, x2, y2,
isText ? this.textCollisionBox : this.iconCollisionBox,
box.anchorPoint, symbolInstance);
}
}
generateCollisionDebugBuffers() {
if (this.hasDebugData()) {
this.destroyDebugData();
}
this.textCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray);
this.iconCollisionBox = new CollisionBuffers(CollisionBoxLayoutArray, collisionBoxLayout.members, LineIndexArray);
for (let i = 0; i < this.symbolInstances.length; i++) {
const symbolInstance = this.symbolInstances.get(i);
this.addDebugCollisionBoxes(symbolInstance.textBoxStartIndex, symbolInstance.textBoxEndIndex, symbolInstance, true);
this.addDebugCollisionBoxes(symbolInstance.verticalTextBoxStartIndex, symbolInstance.verticalTextBoxEndIndex, symbolInstance, true);
this.addDebugCollisionBoxes(symbolInstance.iconBoxStartIndex, symbolInstance.iconBoxEndIndex, symbolInstance, false);
this.addDebugCollisionBoxes(symbolInstance.verticalIconBoxStartIndex, symbolInstance.verticalIconBoxEndIndex, symbolInstance, false);
}
}
// These flat arrays are meant to be quicker to iterate over than the source
// CollisionBoxArray
_deserializeCollisionBoxesForSymbol(
collisionBoxArray: CollisionBoxArray,
textStartIndex: number,
textEndIndex: number,
verticalTextStartIndex: number,
verticalTextEndIndex: number,
iconStartIndex: number,
iconEndIndex: number,
verticalIconStartIndex: number,
verticalIconEndIndex: number
): CollisionArrays {
const collisionArrays = {} as CollisionArrays;
for (let k = textStartIndex; k < textEndIndex; k++) {
const box: CollisionBox = collisionBoxArray.get(k);
collisionArrays.textBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY};
collisionArrays.textFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
}
for (let k = verticalTextStartIndex; k < verticalTextEndIndex; k++) {
const box: CollisionBox = collisionBoxArray.get(k);
collisionArrays.verticalTextBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY};
collisionArrays.verticalTextFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
}
for (let k = iconStartIndex; k < iconEndIndex; k++) {
// An icon can only have one box now, so this indexing is a bit vestigial...
const box: CollisionBox = collisionBoxArray.get(k);
collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY};
collisionArrays.iconFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
}
for (let k = verticalIconStartIndex; k < verticalIconEndIndex; k++) {
// An icon can only have one box now, so this indexing is a bit vestigial...
const box: CollisionBox = collisionBoxArray.get(k);
collisionArrays.verticalIconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, anchorPointX: box.anchorPointX, anchorPointY: box.anchorPointY};
collisionArrays.verticalIconFeatureIndex = box.featureIndex;
break; // Only one box allowed per instance
}
return collisionArrays;
}
deserializeCollisionBoxes(collisionBoxArray: CollisionBoxArray) {
this.collisionArrays = [];
for (let i = 0; i < this.symbolInstances.length; i++) {
const symbolInstance = this.symbolInstances.get(i);
this.collisionArrays.push(this._deserializeCollisionBoxesForSymbol(
collisionBoxArray,
symbolInstance.textBoxStartIndex,
symbolInstance.textBoxEndIndex,
symbolInstance.verticalTextBoxStartIndex,
symbolInstance.verticalTextBoxEndIndex,
symbolInstance.iconBoxStartIndex,
symbolInstance.iconBoxEndIndex,
symbolInstance.verticalIconBoxStartIndex,
symbolInstance.verticalIconBoxEndIndex
));
}
}
hasTextData() {
return this.text.segments.get().length > 0;
}
hasIconData() {
return this.icon.segments.get().length > 0;
}
hasDebugData() {
return this.textCollisionBox && this.iconCollisionBox;
}
hasTextCollisionBoxData() {
return this.hasDebugData() && this.textCollisionBox.segments.get().length > 0;
}
hasIconCollisionBoxData() {
return this.hasDebugData() && this.iconCollisionBox.segments.get().length > 0;
}
addIndicesForPlacedSymbol(iconOrText: SymbolBuffers, placedSymbolIndex: number) {
const placedSymbol = iconOrText.placedSymbolArray.get(placedSymbolIndex);
const endIndex = placedSymbol.vertexStartIndex + placedSymbol.numGlyphs * 4;
for (let vertexIndex = placedSymbol.vertexStartIndex; vertexIndex < endIndex; vertexIndex += 4) {
iconOrText.indexArray.emplaceBack(vertexIndex, vertexIndex + 2, vertexIndex + 1);
iconOrText.indexArray.emplaceBack(vertexIndex + 1, vertexIndex + 2, vertexIndex + 3);
}
}
getSortedSymbolIndexes(angle: number) {
if (this.sortedAngle === angle && this.symbolInstanceIndexes !== undefined) {
return this.symbolInstanceIndexes;
}
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const rotatedYs = [];
const featureIndexes = [];
const result = [];
for (let i = 0; i < this.symbolInstances.length; ++i) {
result.push(i);
const symbolInstance = this.symbolInstances.get(i);
rotatedYs.push(Math.round(sin * symbolInstance.anchorX + cos * symbolInstance.anchorY) | 0);
featureIndexes.push(symbolInstance.featureIndex);
}
result.sort((aIndex, bIndex) => {
return (rotatedYs[aIndex] - rotatedYs[bIndex]) ||
(featureIndexes[bIndex] - featureIndexes[aIndex]);
});
return result;
}
addToSortKeyRanges(symbolInstanceIndex: number, sortKey: number) {
const last = this.sortKeyRanges[this.sortKeyRanges.length - 1];
if (last && last.sortKey === sortKey) {
last.symbolInstanceEnd = symbolInstanceIndex + 1;
} else {
this.sortKeyRanges.push({
sortKey,
symbolInstanceStart: symbolInstanceIndex,
symbolInstanceEnd: symbolInstanceIndex + 1
});
}
}
sortFeatures(angle: number) {
if (!this.sortFeaturesByY) return;
if (this.sortedAngle === angle) return;
// The current approach to sorting doesn't sort across segments so don't try.
// Sorting within segments separately seemed not to be worth the complexity.
if (this.text.segments.get().length > 1 || this.icon.segments.get().length > 1) return;
// If the symbols are allowed to overlap sort them by their vertical screen position.
// The index array buffer is rewritten to reference the (unchanged) vertices in the
// sorted order.
// To avoid sorting the actual symbolInstance array we sort an array of indexes.
this.symbolInstanceIndexes = this.getSortedSymbolIndexes(angle);
this.sortedAngle = angle;
this.text.indexArray.clear();
this.icon.indexArray.clear();
this.featureSortOrder = [];
for (const i of this.symbolInstanceIndexes) {
const symbolInstance = this.symbolInstances.get(i);
this.featureSortOrder.push(symbolInstance.featureIndex);
[
symbolInstance.rightJustifiedTextSymbolIndex,
symbolInstance.centerJustifiedTextSymbolIndex,
symbolInstance.leftJustifiedTextSymbolIndex
].forEach((index, i, array) => {
// Only add a given index the first time it shows up,
// to avoid duplicate opacity entries when multiple justifications
// share the same glyphs.
if (index >= 0 && array.indexOf(index) === i) {
this.addIndicesForPlacedSymbol(this.text, index);
}
});
if (symbolInstance.verticalPlacedTextSymbolIndex >= 0) {
this.addIndicesForPlacedSymbol(this.text, symbolInstance.verticalPlacedTextSymbolIndex);
}
if (symbolInstance.placedIconSymbolIndex >= 0) {
this.addIndicesForPlacedSymbol(this.icon, symbolInstance.placedIconSymbolIndex);
}
if (symbolInstance.verticalPlacedIconSymbolIndex >= 0) {
this.addIndicesForPlacedSymbol(this.icon, symbolInstance.verticalPlacedIconSymbolIndex);
}
}
if (this.text.indexBuffer) this.text.indexBuffer.updateData(this.text.indexArray);
if (this.icon.indexBuffer) this.icon.indexBuffer.updateData(this.icon.indexArray);
}
}
register('SymbolBucket', SymbolBucket, {
omit: ['layers', 'collisionBoxArray', 'features', 'compareText']
});
// this constant is based on the size of StructArray indexes used in a symbol
// bucket--namely, glyphOffsetArrayStart
// eg the max valid UInt16 is 65,535
// See https://github.com/mapbox/mapbox-gl-js/issues/2907 for motivation
// lineStartIndex and textBoxStartIndex could potentially be concerns
// but we expect there to be many fewer boxes/lines than glyphs
SymbolBucket.MAX_GLYPHS = 65535;
SymbolBucket.addDynamicAttributes = addDynamicAttributes;
export {addDynamicAttributes};

295
node_modules/maplibre-gl/src/data/dem_data.test.ts generated vendored Normal file
View File

@@ -0,0 +1,295 @@
import {describe, test, expect, vi} from 'vitest';
import {DEMData} from './dem_data';
import {RGBAImage} from '../util/image';
import {serialize, deserialize} from '../util/web_worker_transfer';
function createMockImage(height, width) {
// RGBAImage passed to constructor has uniform 1px padding on all sides.
height += 2;
width += 2;
const pixels = new Uint8Array(height * width * 4);
for (let i = 0; i < pixels.length; i++) {
pixels[i] = (i + 1) % 4 === 0 ? 1 : Math.floor(Math.random() * 256);
}
return new RGBAImage({height, width}, pixels);
}
function createMockClampImage(height, width) {
const pixels = new Uint8ClampedArray(height * width * 4);
for (let i = 0; i < pixels.length; i++) {
pixels[i] = (i + 1) % 4 === 0 ? 1 : Math.floor(Math.random() * 256);
}
return new RGBAImage({height, width}, pixels);
}
describe('DEMData', () => {
describe('constructor', () => {
test('Uint8Array', () => {
const imageData0 = createMockImage(4, 4);
const dem = new DEMData('0', imageData0, 'mapbox');
expect(dem.uid).toBe('0');
expect(dem.dim).toBe(4);
expect(dem.stride).toBe(6);
});
test('Uint8ClampedArray', () => {
const imageData0 = createMockClampImage(4, 4);
const dem = new DEMData('0', imageData0, 'mapbox');
expect(dem).not.toBeNull();
expect(dem['uid']).toBe('0');
expect(dem['dim']).toBe(2);
expect(dem['stride']).toBe(4);
});
test('otherEncoding', () => {
const spyOnWarnConsole = vi.spyOn(console, 'warn').mockImplementation(() => {});
const imageData0 = createMockImage(4, 4);
new DEMData('0', imageData0, 'otherEncoding' as any);
expect(spyOnWarnConsole).toHaveBeenCalledTimes(1);
expect(spyOnWarnConsole.mock.calls).toEqual([['\"otherEncoding\" is not a valid encoding type. Valid types include \"mapbox\", \"terrarium\" and \"custom\".']]);
});
});
});
function testDEMBorderRegion(dem: DEMData) {
return () => {
let nonempty = true;
for (let x = -1; x < 5; x++) {
for (let y = -1; y < 5; y++) {
if (dem.get(x, y) === -65536) {
nonempty = false;
break;
}
}
}
expect(nonempty).toBeTruthy();
let verticalBorderMatch = true;
for (const x of [-1, 4]) {
for (let y = 0; y < 4; y++) {
if (dem.get(x, y) !== dem.get(x < 0 ? x + 1 : x - 1, y)) {
verticalBorderMatch = false;
break;
}
}
}
expect(verticalBorderMatch).toBeTruthy();
// horizontal borders empty
let horizontalBorderMatch = true;
for (const y of [-1, 4]) {
for (let x = 0; x < 4; x++) {
if (dem.get(x, y) !== dem.get(x, y < 0 ? y + 1 : y - 1)) {
horizontalBorderMatch = false;
break;
}
}
}
expect(horizontalBorderMatch).toBeTruthy();
expect(dem.get(-1, 4) === dem.get(0, 3)).toBeTruthy();
expect(dem.get(4, 4) === dem.get(3, 3)).toBeTruthy();
expect(dem.get(-1, -1) === dem.get(0, 0)).toBeTruthy();
expect(dem.get(4, -1) === dem.get(3, 0)).toBeTruthy();
};
}
function testDEMBackfill(dem0: DEMData, dem1: DEMData) {
return () => {
dem0.backfillBorder(dem1, -1, 0);
for (let y = 0; y < 4; y++) {
// dx = -1, dy = 0, so the left edge of dem1 should equal the right edge of dem0
expect(dem0.get(-1, y) === dem1.get(3, y)).toBeTruthy();
}
dem0.backfillBorder(dem1, 0, -1);
for (let x = 0; x < 4; x++) {
expect(dem0.get(x, -1) === dem1.get(x, 3)).toBeTruthy();
}
dem0.backfillBorder(dem1, 1, 0);
for (let y = 0; y < 4; y++) {
expect(dem0.get(4, y) === dem1.get(0, y)).toBeTruthy();
}
dem0.backfillBorder(dem1, 0, 1);
for (let x = 0; x < 4; x++) {
expect(dem0.get(x, 4) === dem1.get(x, 0)).toBeTruthy();
}
dem0.backfillBorder(dem1, -1, 1);
expect(dem0.get(-1, 4) === dem1.get(3, 0)).toBeTruthy();
dem0.backfillBorder(dem1, 1, 1);
expect(dem0.get(4, 4) === dem1.get(0, 0)).toBeTruthy();
dem0.backfillBorder(dem1, -1, -1);
expect(dem0.get(-1, -1) === dem1.get(3, 3)).toBeTruthy();
dem0.backfillBorder(dem1, 1, -1);
expect(dem0.get(4, -1) === dem1.get(0, 3)).toBeTruthy();
};
}
describe('DEMData.backfillBorder with encoding', () => {
describe('mapbox encoding', () => {
const dem0 = new DEMData('0', createMockImage(4, 4), 'mapbox');
const dem1 = new DEMData('1', createMockImage(4, 4), 'mapbox');
test('border region is initially populated with neighboring data', testDEMBorderRegion(dem0));
test('backfillBorder correctly populates borders with neighboring data', testDEMBackfill(dem0, dem1));
});
describe('terrarium encoding', () => {
const dem0 = new DEMData('0', createMockImage(4, 4), 'terrarium');
const dem1 = new DEMData('1', createMockImage(4, 4), 'terrarium');
test('border region is initially populated with neighboring data', testDEMBorderRegion(dem0));
test('backfillBorder correctly populates borders with neighboring data', testDEMBackfill(dem0, dem1));
});
});
function testSerialization(dem0: DEMData, redFactor: number, greenFactor: number, blueFactor: number, baseShift: number) {
return () => {
const serialized = serialize(dem0);
// calculate min/max values
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 4; y++) {
const ele = dem0.get(x, y);
if (ele > max) max = ele;
if (ele < min) min = ele;
}
}
expect(serialized).toEqual({
$name: 'DEMData',
uid: '0',
dim: 4,
stride: 6,
data: dem0.data,
redFactor,
greenFactor,
blueFactor,
baseShift,
max,
min,
});
const transferrables = [];
serialize(dem0, transferrables);
expect(new Uint32Array(transferrables[0])).toEqual(dem0.data);
};
}
function testDeserialization(dem0: DEMData) {
return () => {
const serialized = serialize(dem0);
const deserialized = deserialize(serialized);
expect(deserialized).toEqual(dem0);
};
}
describe('DEMData is correctly serialized and deserialized', () => {
const mapboxDEM = new DEMData('0', createMockImage(4, 4), 'mapbox');
const terrariumDEM = new DEMData('0', createMockImage(4, 4), 'terrarium');
const customDEM = new DEMData('0', createMockImage(4, 4), 'custom', 1.0, 2.0, 3.0, 4.0);
test('serialized - mapbox', testSerialization(mapboxDEM, 6553.6, 25.6, 0.1, 10000));
test('serialized - terrarium', testSerialization(terrariumDEM, 256.0, 1.0, 1.0 / 256.0, 32768.0));
test('serialized - custom', testSerialization(customDEM, 1.0, 2.0, 3.0, 4.0));
test('deserialized - mapbox', testDeserialization(mapboxDEM));
test('deserialized - terrarium', testDeserialization(terrariumDEM));
test('deserialized - custom', testDeserialization(customDEM));
});
describe('UnpackVector is correctly returned', () => {
test('terrarium, mapbox and custom', () => {
const mapboxDEM = new DEMData('0', createMockImage(4, 4), 'mapbox');
const terrariumDEM = new DEMData('0', createMockImage(4, 4), 'terrarium');
const customDEM = new DEMData('0', createMockImage(4, 4), 'custom', 1.0, 2.0, 3.0, 4.0);
expect(terrariumDEM.getUnpackVector()).toEqual([256.0, 1.0, 1.0 / 256.0, 32768.0]);
expect(mapboxDEM.getUnpackVector()).toEqual([6553.6, 25.6, 0.1, 10000.0]);
expect(customDEM.getUnpackVector()).toEqual([1.0, 2.0, 3.0, 4.0]);
});
});
function testGetPixels(dem: DEMData, imageData: RGBAImage) {
return () => {
expect(dem.getPixels()).toEqual(imageData);
};
}
describe('DEMData.getImage', () => {
const imageData = createMockImage(4, 4);
const mapboxDEM = new DEMData('0', imageData, 'terrarium');
const terrariumDEM = new DEMData('0', imageData, 'terrarium');
const customDEM = new DEMData('0', imageData, 'terrarium');
test('Image is correctly returned - mapbox', testGetPixels(mapboxDEM, imageData));
test('Image is correctly returned - terrarium', testGetPixels(terrariumDEM, imageData));
test('Image is correctly returned - custom', testGetPixels(customDEM, imageData));
});
describe('DEMData pack and unpack', () => {
const imageData = createMockImage(4, 4);
test('mapbox', () => {
const dem = new DEMData('0', imageData, 'mapbox');
expect(dem.unpack(123, 177, 215)).toEqual(800645.5);
expect(dem.pack(800645.5)).toEqual({r: 123, g: 177, b: 215});
expect(dem.unpack(0, 0, 0)).toEqual(-10000);
expect(dem.pack(-10000)).toEqual({r: 0, g: 0, b: 0});
expect(dem.unpack(1, 1, 1)).toBeCloseTo(-3420.7);
expect(dem.pack(-3420.7)).toEqual({r: 1, g: 1, b: 1});
expect(dem.unpack(255, 255, 255)).toEqual(1667721.5);
expect(dem.pack(1667721.5)).toEqual({r: 255, g: 255, b: 255});
expect(dem.unpack(255, 0, 255)).toEqual(1661193.5);
expect(dem.pack(1661193.5)).toEqual({r: 255, g: 0, b: 255});
});
test('terrarium', () => {
const dem = new DEMData('0', imageData, 'terrarium');
expect(dem.unpack(123, 177, 215)).toEqual(-1102.16015625);
expect(dem.pack(-1102.16015625)).toEqual({r: 123, g: 177, b: 215});
expect(dem.unpack(0, 0, 0)).toEqual(-32768);
expect(dem.pack(-32768)).toEqual({r: 0, g: 0, b: 0});
expect(dem.unpack(1, 1, 1)).toEqual(-32510.99609375);
expect(dem.pack(-32510.99609375)).toEqual({r: 1, g: 1, b: 1});
expect(dem.unpack(255, 255, 255)).toEqual(32767.99609375);
expect(dem.pack(32767.99609375)).toEqual({r: 255, g: 255, b: 255});
expect(dem.unpack(255, 0, 255)).toEqual(32512.99609375);
expect(dem.pack(32512.99609375)).toEqual({r: 255, g: 0, b: 255});
});
test('custom', () => {
const dem = new DEMData('0', imageData, 'custom', 0.25, 64, 16384, 7000.0);
expect(dem.unpack(123, 177, 215)).toEqual(3526918.75);
expect(dem.pack(3526918.75)).toEqual({r: 123, g: 177, b: 215});
expect(dem.unpack(0, 0, 0)).toEqual(-7000);
expect(dem.pack(-7000)).toEqual({r: 0, g: 0, b: 0});
expect(dem.unpack(1, 1, 1)).toEqual(9448.25);
expect(dem.pack(9448.25)).toEqual({r: 1, g: 1, b: 1});
expect(dem.unpack(255, 255, 255)).toEqual(4187303.75);
expect(dem.pack(4187303.75)).toEqual({r: 255, g: 255, b: 255});
expect(dem.unpack(255, 0, 255)).toEqual(4170983.75);
expect(dem.pack(4170983.75)).toEqual({r: 255, g: 0, b: 255});
});
});

189
node_modules/maplibre-gl/src/data/dem_data.ts generated vendored Normal file
View File

@@ -0,0 +1,189 @@
import {RGBAImage} from '../util/image';
import {warnOnce} from '../util/util';
import {register} from '../util/web_worker_transfer';
/**
* The possible DEM encoding types
*/
export type DEMEncoding = 'mapbox' | 'terrarium' | 'custom';
/**
* DEMData is a data structure for decoding, backfilling, and storing elevation data for processing in the hillshade shaders
* data can be populated either from a png raw image tile or from serialized data sent back from a worker. When data is initially
* loaded from a image tile, we decode the pixel values using the appropriate decoding formula, but we store the
* elevation data as an Int32 value. we add 65536 (2^16) to eliminate negative values and enable the use of
* integer overflow when creating the texture used in the hillshadePrepare step.
*
* DEMData also handles the backfilling of data from a tile's neighboring tiles. This is necessary because we use a pixel's 8
* surrounding pixel values to compute the slope at that pixel, and we cannot accurately calculate the slope at pixels on a
* tile's edge without backfilling from neighboring tiles.
*/
export class DEMData {
uid: string | number;
data: Uint32Array;
stride: number;
dim: number;
min: number;
max: number;
redFactor: number;
greenFactor: number;
blueFactor: number;
baseShift: number;
/**
* Constructs a `DEMData` object
* @param uid - the tile's unique id
* @param data - RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride
// and dim is calculated as stride - 2.
* @param encoding - the encoding type of the data
* @param redFactor - the red channel factor used to unpack the data, used for `custom` encoding only
* @param greenFactor - the green channel factor used to unpack the data, used for `custom` encoding only
* @param blueFactor - the blue channel factor used to unpack the data, used for `custom` encoding only
* @param baseShift - the base shift used to unpack the data, used for `custom` encoding only
*/
constructor(uid: string | number, data: RGBAImage | ImageData, encoding: DEMEncoding, redFactor = 1.0, greenFactor = 1.0, blueFactor = 1.0, baseShift = 0.0) {
this.uid = uid;
if (data.height !== data.width) throw new RangeError('DEM tiles must be square');
if (encoding && !['mapbox', 'terrarium', 'custom'].includes(encoding)) {
warnOnce(`"${encoding}" is not a valid encoding type. Valid types include "mapbox", "terrarium" and "custom".`);
return;
}
this.stride = data.height;
const dim = this.dim = data.height - 2;
this.data = new Uint32Array(data.data.buffer);
switch (encoding) {
case 'terrarium':
// unpacking formula for mapzen terrarium:
// https://aws.amazon.com/public-datasets/terrain/
this.redFactor = 256.0;
this.greenFactor = 1.0;
this.blueFactor = 1.0 / 256.0;
this.baseShift = 32768.0;
break;
case 'custom':
this.redFactor = redFactor;
this.greenFactor = greenFactor;
this.blueFactor = blueFactor;
this.baseShift = baseShift;
break;
case 'mapbox':
default:
// unpacking formula for mapbox.terrain-rgb:
// https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb
this.redFactor = 6553.6;
this.greenFactor = 25.6;
this.blueFactor = 0.1;
this.baseShift = 10000.0;
break;
}
// in order to avoid flashing seams between tiles, here we are initially populating a 1px border of pixels around the image
// with the data of the nearest pixel from the image. this data is eventually replaced when the tile's neighboring
// tiles are loaded and the accurate data can be backfilled using DEMData#backfillBorder
for (let x = 0; x < dim; x++) {
// left vertical border
this.data[this._idx(-1, x)] = this.data[this._idx(0, x)];
// right vertical border
this.data[this._idx(dim, x)] = this.data[this._idx(dim - 1, x)];
// left horizontal border
this.data[this._idx(x, -1)] = this.data[this._idx(x, 0)];
// right horizontal border
this.data[this._idx(x, dim)] = this.data[this._idx(x, dim - 1)];
}
// corners
this.data[this._idx(-1, -1)] = this.data[this._idx(0, 0)];
this.data[this._idx(dim, -1)] = this.data[this._idx(dim - 1, 0)];
this.data[this._idx(-1, dim)] = this.data[this._idx(0, dim - 1)];
this.data[this._idx(dim, dim)] = this.data[this._idx(dim - 1, dim - 1)];
// calculate min/max values
this.min = Number.MAX_SAFE_INTEGER;
this.max = Number.MIN_SAFE_INTEGER;
for (let x = 0; x < dim; x++) {
for (let y = 0; y < dim; y++) {
const ele = this.get(x, y);
if (ele > this.max) this.max = ele;
if (ele < this.min) this.min = ele;
}
}
}
get(x: number, y: number) {
const pixels = new Uint8Array(this.data.buffer);
const index = this._idx(x, y) * 4;
return this.unpack(pixels[index], pixels[index + 1], pixels[index + 2]);
}
getUnpackVector() {
return [this.redFactor, this.greenFactor, this.blueFactor, this.baseShift];
}
_idx(x: number, y: number) {
if (x < -1 || x >= this.dim + 1 || y < -1 || y >= this.dim + 1) throw new RangeError(`Out of range source coordinates for DEM data. x: ${x}, y: ${y}, dim: ${this.dim}`);
return (y + 1) * this.stride + (x + 1);
}
unpack(r: number, g: number, b: number) {
return (r * this.redFactor + g * this.greenFactor + b * this.blueFactor - this.baseShift);
}
pack(v: number): {r: number; g: number; b: number} {
return packDEMData(v, this.getUnpackVector());
}
getPixels() {
return new RGBAImage({width: this.stride, height: this.stride}, new Uint8Array(this.data.buffer));
}
backfillBorder(borderTile: DEMData, dx: number, dy: number) {
if (this.dim !== borderTile.dim) throw new Error('dem dimension mismatch');
let xMin = dx * this.dim,
xMax = dx * this.dim + this.dim,
yMin = dy * this.dim,
yMax = dy * this.dim + this.dim;
switch (dx) {
case -1:
xMin = xMax - 1;
break;
case 1:
xMax = xMin + 1;
break;
}
switch (dy) {
case -1:
yMin = yMax - 1;
break;
case 1:
yMax = yMin + 1;
break;
}
const ox = -dx * this.dim;
const oy = -dy * this.dim;
for (let y = yMin; y < yMax; y++) {
for (let x = xMin; x < xMax; x++) {
this.data[this._idx(x, y)] = borderTile.data[this._idx(x + ox, y + oy)];
}
}
}
}
export function packDEMData(v: number, unpackVector: number[]): {r: number; g: number; b: number} {
const redFactor = unpackVector[0];
const greenFactor = unpackVector[1];
const blueFactor = unpackVector[2];
const baseShift = unpackVector[3];
const minScale = Math.min(redFactor, greenFactor, blueFactor);
const vScaled = Math.round((v + baseShift)/minScale);
return {
r: Math.floor(vScaled*minScale/redFactor) % 256,
g: Math.floor(vScaled*minScale/greenFactor) % 256,
b: Math.floor(vScaled*minScale/blueFactor) % 256
};
}
register('DEMData', DEMData);

View File

@@ -0,0 +1,18 @@
import {loadGeometry} from './load_geometry';
import type Point from '@mapbox/point-geometry';
import type {Feature} from '@maplibre/maplibre-gl-style-spec';
import type {VectorTileFeatureLike} from '@maplibre/vt-pbf';
type EvaluationFeature = Feature & { geometry: Array<Array<Point>> };
/**
* Construct a new feature based on a VectorTileFeatureLike for expression evaluation, the geometry of which
* will be loaded based on necessity.
* @param feature - the feature to evaluate
* @param needGeometry - if set to true this will load the geometry
*/
export function toEvaluationFeature(feature: VectorTileFeatureLike, needGeometry: boolean): EvaluationFeature {
return {type: feature.type,
id: feature.id,
properties: feature.properties,
geometry: needGeometry ? loadGeometry(feature) : []};
}

13
node_modules/maplibre-gl/src/data/extent.ts generated vendored Normal file
View File

@@ -0,0 +1,13 @@
/**
* The maximum value of a coordinate in the internal tile coordinate system. Coordinates of
* all source features normalized to this extent upon load.
*
* The value is a consequence of the following:
*
* * Vertex buffer store positions as signed 16 bit integers.
* * One bit is lost for signedness to support tile buffers.
* * One bit is lost because the line vertex buffer used to pack 1 bit of other data into the int.
* * One bit is lost to support features extending past the extent on the right edge of the tile.
* * This leaves us with 2^13 = 8192
*/
export const EXTENT = 8192;

8
node_modules/maplibre-gl/src/data/extent_bounds.ts generated vendored Normal file
View File

@@ -0,0 +1,8 @@
import Point from '@mapbox/point-geometry';
import {Bounds, type ReadOnlyBounds} from '../geo/bounds';
import {EXTENT} from './extent';
/**
* The bounding box covering the entire extent of a tile.
*/
export const EXTENT_BOUNDS = Bounds.fromPoints([new Point(0, 0), new Point(EXTENT, EXTENT)]) as ReadOnlyBounds;

101
node_modules/maplibre-gl/src/data/feature_index.test.ts generated vendored Normal file
View File

@@ -0,0 +1,101 @@
import path from 'path';
import {readFileSync} from 'fs';
import {describe, expect, test} from 'vitest';
import {FeatureIndex, GEOJSON_TILE_LAYER_NAME} from './feature_index';
import {type Feature, fromVectorTileJs, GeoJSONWrapper, type VectorTileFeatureLike} from '@maplibre/vt-pbf';
import {MercatorTransform} from '../geo/projection/mercator_transform';
import {OverscaledTileID} from '../tile/tile_id';
import {CircleStyleLayer} from '../style/style_layer/circle_style_layer';
import Point from '@mapbox/point-geometry';
import type {LayerSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {EvaluationParameters} from '../style/evaluation_parameters';
describe('FeatureIndex', () => {
describe('getId', () => {
const tileID = new OverscaledTileID(0, 0, 0, 0, 0);
test('uses cluster_id when cluster is true and id is undefined', () => {
const featureIndex = new FeatureIndex(tileID, 'someProperty');
const feature: VectorTileFeatureLike = {
id: 0,
properties: {
cluster: true,
cluster_id: '123',
promoteId: 'someProperty',
someProperty: undefined
},
extent: 4096,
type: 1,
loadGeometry: () => [],
};
expect(featureIndex.getId(feature, 'sourceLayer')).toBe(123); // cluster_id converted to number
});
});
describe('query', () => {
const tileID = new OverscaledTileID(3, 0, 2, 1, 2);
const transform = new MercatorTransform();
transform.resize(500, 500);
test('filter with global-state', () => {
const features = [
{
type: 1,
geometry: [0, 0],
tags: {cluster: true}
} as any as Feature
];
const geojsonWrapper = new GeoJSONWrapper(features);
geojsonWrapper.name = GEOJSON_TILE_LAYER_NAME;
const rawTileData = fromVectorTileJs({layers: {[GEOJSON_TILE_LAYER_NAME]: geojsonWrapper}});
const globalState = {isCluster: true};
const layer = new CircleStyleLayer({source: 'source', paint: {}} as LayerSpecification, globalState);
layer.recalculate({} as EvaluationParameters, []);
const featureIndex = new FeatureIndex(tileID);
featureIndex.rawTileData = rawTileData as any as ArrayBuffer;
featureIndex.bucketLayerIDs = [['layer']];
featureIndex.insert(geojsonWrapper.feature(0), [[new Point(1, 1)]], 0, 0, 0);
const result = featureIndex.query({
queryPadding: 0,
tileSize: 512,
scale: 1,
queryGeometry: [new Point(0, 0), new Point(10, 10)],
cameraQueryGeometry: [new Point(0, 0), new Point(10, 10)],
params: {
filter: ['==', ['get', 'cluster'], ['global-state', 'isCluster']],
globalState
},
transform
} as any, {
layer: layer,
}, [], undefined);
expect(result.layer[0].feature.properties).toEqual(features[0].tags);
});
test('query mlt tile', () => {
const layer = new CircleStyleLayer({source: 'source', paint: {}} as LayerSpecification, {});
layer.recalculate({} as EvaluationParameters, []);
const featureIndex = new FeatureIndex(tileID);
const mltRawData = readFileSync(path.join(__dirname, '../../test/integration/assets/tiles/mlt/5/17/10.mlt')).buffer.slice(0) as ArrayBuffer;
featureIndex.rawTileData = mltRawData;
featureIndex.encoding = 'mlt';
featureIndex.bucketLayerIDs = [['layer']];
featureIndex.insert({} as any, [[new Point(1, 1)]], 0, 0, 0);
const result = featureIndex.query({
queryPadding: 0,
tileSize: 512,
scale: 1,
queryGeometry: [new Point(0, 0), new Point(0, 2000), new Point(2000, 2000), new Point(2000, 0), new Point(0 ,0)],
cameraQueryGeometry: [new Point(0, 0), new Point(10, 10)],
params: {},
transform
} as any, {
layer: layer,
}, [], undefined);
expect(result.layer[0].feature.properties.admin_level).toBeDefined();
expect(result.layer[0].feature.geometry.type).toBe('LineString');
});
});
});

354
node_modules/maplibre-gl/src/data/feature_index.ts generated vendored Normal file
View File

@@ -0,0 +1,354 @@
import type Point from '@mapbox/point-geometry';
import {loadGeometry} from './load_geometry';
import {toEvaluationFeature} from './evaluation_feature';
import {EXTENT} from './extent';
import {featureFilter} from '@maplibre/maplibre-gl-style-spec';
import {TransferableGridIndex} from '../util/transferable_grid_index';
import {DictionaryCoder} from '../util/dictionary_coder';
import Protobuf from 'pbf';
import {GeoJSONFeature} from '../util/vectortile_to_geojson';
import {mapObject, extend} from '../util/util';
import {register} from '../util/web_worker_transfer';
import {EvaluationParameters} from '../style/evaluation_parameters';
import {polygonIntersectsBox} from '../util/intersection_tests';
import {PossiblyEvaluated} from '../style/properties';
import {FeatureIndexArray} from './array_types.g';
import {MLTVectorTile} from '../source/vector_tile_mlt';
import {Bounds} from '../geo/bounds';
import type {OverscaledTileID} from '../tile/tile_id';
import type {SourceFeatureState} from '../source/source_state';
import type {mat4} from 'gl-matrix';
import type {MapGeoJSONFeature} from '../util/vectortile_to_geojson';
import type {StyleLayer} from '../style/style_layer';
import type {FeatureFilter, FeatureState, FilterSpecification, PromoteIdSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {IReadonlyTransform} from '../geo/transform_interface';
import {type VectorTileFeatureLike, type VectorTileLayerLike, GEOJSON_TILE_LAYER_NAME} from '@maplibre/vt-pbf';
import {VectorTile} from '@mapbox/vector-tile';
export {GEOJSON_TILE_LAYER_NAME};
type QueryParameters = {
scale: number;
pixelPosMatrix: mat4;
transform: IReadonlyTransform;
tileSize: number;
queryGeometry: Array<Point>;
cameraQueryGeometry: Array<Point>;
queryPadding: number;
getElevation: undefined | ((x: number, y: number) => number);
params: {
filter?: FilterSpecification;
layers?: Set<string> | null;
availableImages?: Array<string>;
globalState?: Record<string, any>;
};
};
export type QueryResults = {
[_: string]: QueryResultsItem[];
};
export type QueryResultsItem = {
featureIndex: number;
feature: GeoJSONFeature;
intersectionZ?: boolean | number;
};
/**
* An in memory index class to allow fast interaction with features
*/
export class FeatureIndex {
tileID: OverscaledTileID;
x: number;
y: number;
z: number;
grid: TransferableGridIndex;
grid3D: TransferableGridIndex;
featureIndexArray: FeatureIndexArray;
promoteId?: PromoteIdSpecification;
encoding: string;
rawTileData: ArrayBuffer;
bucketLayerIDs: Array<Array<string>>;
vtLayers: {[_: string]: VectorTileLayerLike};
sourceLayerCoder: DictionaryCoder;
constructor(tileID: OverscaledTileID, promoteId?: PromoteIdSpecification | null) {
this.tileID = tileID;
this.x = tileID.canonical.x;
this.y = tileID.canonical.y;
this.z = tileID.canonical.z;
this.grid = new TransferableGridIndex(EXTENT, 16, 0);
this.grid3D = new TransferableGridIndex(EXTENT, 16, 0);
this.featureIndexArray = new FeatureIndexArray();
this.promoteId = promoteId;
}
insert(feature: VectorTileFeatureLike, geometry: Array<Array<Point>>, featureIndex: number, sourceLayerIndex: number, bucketIndex: number, is3D?: boolean) {
const key = this.featureIndexArray.length;
this.featureIndexArray.emplaceBack(featureIndex, sourceLayerIndex, bucketIndex);
const grid = is3D ? this.grid3D : this.grid;
for (let r = 0; r < geometry.length; r++) {
const ring = geometry[r];
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
for (let i = 0; i < ring.length; i++) {
const p = ring[i];
bbox[0] = Math.min(bbox[0], p.x);
bbox[1] = Math.min(bbox[1], p.y);
bbox[2] = Math.max(bbox[2], p.x);
bbox[3] = Math.max(bbox[3], p.y);
}
if (bbox[0] < EXTENT &&
bbox[1] < EXTENT &&
bbox[2] >= 0 &&
bbox[3] >= 0) {
grid.insert(key, bbox[0], bbox[1], bbox[2], bbox[3]);
}
}
}
loadVTLayers(): {[_: string]: VectorTileLayerLike} {
if (!this.vtLayers) {
this.vtLayers = this.encoding !== 'mlt'
? new VectorTile(new Protobuf(this.rawTileData)).layers
: new MLTVectorTile(this.rawTileData).layers;
this.sourceLayerCoder = new DictionaryCoder(this.vtLayers ? Object.keys(this.vtLayers).sort() : [GEOJSON_TILE_LAYER_NAME]);
}
return this.vtLayers;
}
// Finds non-symbol features in this tile at a particular position.
query(
args: QueryParameters,
styleLayers: {[_: string]: StyleLayer},
serializedLayers: {[_: string]: any},
sourceFeatureState: SourceFeatureState
): QueryResults {
this.loadVTLayers();
const params = args.params;
const pixelsToTileUnits = EXTENT / args.tileSize / args.scale;
const filter = featureFilter(params.filter, params.globalState);
const queryGeometry = args.queryGeometry;
const queryPadding = args.queryPadding * pixelsToTileUnits;
const bounds = Bounds.fromPoints(queryGeometry);
const matching = this.grid.query(bounds.minX - queryPadding, bounds.minY - queryPadding, bounds.maxX + queryPadding, bounds.maxY + queryPadding);
const cameraBounds = Bounds.fromPoints(args.cameraQueryGeometry).expandBy(queryPadding);
const matching3D = this.grid3D.query(
cameraBounds.minX, cameraBounds.minY, cameraBounds.maxX, cameraBounds.maxY,
(bx1, by1, bx2, by2) => {
return polygonIntersectsBox(args.cameraQueryGeometry, bx1 - queryPadding, by1 - queryPadding, bx2 + queryPadding, by2 + queryPadding);
});
for (const key of matching3D) {
matching.push(key);
}
matching.sort(topDownFeatureComparator);
const result: QueryResults = {};
let previousIndex;
for (let k = 0; k < matching.length; k++) {
const index = matching[k];
// don't check the same feature more than once
if (index === previousIndex) continue;
previousIndex = index;
const match = this.featureIndexArray.get(index);
let featureGeometry = null;
this.loadMatchingFeature(
result,
match.bucketIndex,
match.sourceLayerIndex,
match.featureIndex,
filter,
params.layers,
params.availableImages,
styleLayers,
serializedLayers,
sourceFeatureState,
(feature: VectorTileFeatureLike, styleLayer: StyleLayer, featureState: FeatureState) => {
if (!featureGeometry) {
featureGeometry = loadGeometry(feature);
}
return styleLayer.queryIntersectsFeature({
queryGeometry,
feature,
featureState,
geometry: featureGeometry,
zoom: this.z,
transform: args.transform,
pixelsToTileUnits,
pixelPosMatrix: args.pixelPosMatrix,
unwrappedTileID: this.tileID.toUnwrapped(),
getElevation: args.getElevation
});
}
);
}
return result;
}
loadMatchingFeature(
result: QueryResults,
bucketIndex: number,
sourceLayerIndex: number,
featureIndex: number,
filter: FeatureFilter,
filterLayerIDs: Set<string> | undefined,
availableImages: Array<string>,
styleLayers: {[_: string]: StyleLayer},
serializedLayers: {[_: string]: any},
sourceFeatureState?: SourceFeatureState,
intersectionTest?: (
feature: VectorTileFeatureLike,
styleLayer: StyleLayer,
featureState: any,
id: string | number | void
) => boolean | number) {
const layerIDs = this.bucketLayerIDs[bucketIndex];
if (filterLayerIDs && !layerIDs.some(id => filterLayerIDs.has(id)))
return;
const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex);
const sourceLayer = this.vtLayers[sourceLayerName];
const feature = sourceLayer.feature(featureIndex);
if (filter.needGeometry) {
const evaluationFeature = toEvaluationFeature(feature, true);
if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), evaluationFeature, this.tileID.canonical)) {
return;
}
} else if (!filter.filter(new EvaluationParameters(this.tileID.overscaledZ), feature)) {
return;
}
const id = this.getId(feature, sourceLayerName);
for (let l = 0; l < layerIDs.length; l++) {
const layerID = layerIDs[l];
if (filterLayerIDs && !filterLayerIDs.has(layerID)) {
continue;
}
const styleLayer = styleLayers[layerID];
if (!styleLayer) continue;
let featureState = {};
if (id && sourceFeatureState) {
// `feature-state` expression evaluation requires feature state to be available
featureState = sourceFeatureState.getState(styleLayer.sourceLayer || GEOJSON_TILE_LAYER_NAME, id);
}
const serializedLayer = extend({}, serializedLayers[layerID]);
serializedLayer.paint = evaluateProperties(serializedLayer.paint, styleLayer.paint, feature, featureState, availableImages);
serializedLayer.layout = evaluateProperties(serializedLayer.layout, styleLayer.layout, feature, featureState, availableImages);
const intersectionZ = !intersectionTest || intersectionTest(feature, styleLayer, featureState);
if (!intersectionZ) {
// Only applied for non-symbol features
continue;
}
const geojsonFeature = new GeoJSONFeature(feature, this.z, this.x, this.y, id) as MapGeoJSONFeature;
geojsonFeature.layer = serializedLayer;
let layerResult = result[layerID];
if (layerResult === undefined) {
layerResult = result[layerID] = [];
}
layerResult.push({featureIndex, feature: geojsonFeature, intersectionZ});
}
}
// Given a set of symbol indexes that have already been looked up,
// return a matching set of GeoJSONFeatures
lookupSymbolFeatures(symbolFeatureIndexes: Array<number>,
serializedLayers: {[_: string]: StyleLayer},
bucketIndex: number,
sourceLayerIndex: number,
filterParams: {
filterSpec: FilterSpecification;
globalState: Record<string, any>;
},
filterLayerIDs: Set<string> | null,
availableImages: Array<string>,
styleLayers: {[_: string]: StyleLayer}): QueryResults {
const result: QueryResults = {};
this.loadVTLayers();
const filter = featureFilter(filterParams.filterSpec, filterParams.globalState);
for (const symbolFeatureIndex of symbolFeatureIndexes) {
this.loadMatchingFeature(
result,
bucketIndex,
sourceLayerIndex,
symbolFeatureIndex,
filter,
filterLayerIDs,
availableImages,
styleLayers,
serializedLayers
);
}
return result;
}
hasLayer(id: string) {
for (const layerIDs of this.bucketLayerIDs) {
for (const layerID of layerIDs) {
if (id === layerID) return true;
}
}
return false;
}
getId(feature: VectorTileFeatureLike, sourceLayerId: string): string | number {
let id: string | number = feature.id;
if (this.promoteId) {
const propName = typeof this.promoteId === 'string' ? this.promoteId : this.promoteId[sourceLayerId];
id = feature.properties[propName] as string | number;
if (typeof id === 'boolean') id = Number(id);
// When cluster is true, the id is the cluster_id even though promoteId is set
if (id === undefined && feature.properties?.cluster && this.promoteId) {
id = Number(feature.properties.cluster_id);
}
}
return id;
}
}
register(
'FeatureIndex',
FeatureIndex,
{omit: ['rawTileData', 'sourceLayerCoder']}
);
function evaluateProperties(serializedProperties, styleLayerProperties, feature, featureState, availableImages) {
return mapObject(serializedProperties, (property, key) => {
const prop = styleLayerProperties instanceof PossiblyEvaluated ? styleLayerProperties.get(key) : null;
return prop && prop.evaluate ? prop.evaluate(feature, featureState, availableImages) : prop;
});
}
function topDownFeatureComparator(a, b) {
return b - a;
}

View File

@@ -0,0 +1,34 @@
import {describe, test, expect} from 'vitest';
import {FeaturePositionMap} from './feature_position_map';
import {serialize, deserialize} from '../util/web_worker_transfer';
describe('FeaturePositionMap', () => {
test('Can be queried after serialization/deserialization', () => {
const featureMap = new FeaturePositionMap();
featureMap.add(7, 1, 0, 1);
featureMap.add(3, 2, 1, 2);
featureMap.add(7, 3, 2, 3);
featureMap.add(4, 4, 3, 4);
featureMap.add(2, 5, 4, 5);
featureMap.add(7, 6, 5, 7);
const featureMap2 = deserialize(serialize(featureMap, [])) as FeaturePositionMap;
const compareIndex = (a, b) => a.index - b.index;
expect(featureMap2.getPositions(7).sort(compareIndex)).toEqual([
{index: 1, start: 0, end: 1},
{index: 3, start: 2, end: 3},
{index: 6, start: 5, end: 7}
].sort(compareIndex));
});
test('Can not be queried before serialization/deserialization', () => {
const featureMap = new FeaturePositionMap();
featureMap.add(0, 1, 2, 3);
expect(() => {
featureMap.getPositions(0);
}).toThrow();
});
});

View File

@@ -0,0 +1,126 @@
import murmur3 from 'murmurhash-js';
import {register} from '../util/web_worker_transfer';
type SerializedFeaturePositionMap = {
ids: Float64Array;
positions: Uint32Array;
};
type FeaturePosition = {
index: number;
start: number;
end: number;
};
// A transferable data structure that maps feature ids to their indices and buffer offsets
export class FeaturePositionMap {
ids: Array<number>;
positions: Array<number>;
indexed: boolean;
constructor() {
this.ids = [];
this.positions = [];
this.indexed = false;
}
add(id: unknown, index: number, start: number, end: number) {
this.ids.push(getNumericId(id));
this.positions.push(index, start, end);
}
getPositions(id: unknown): Array<FeaturePosition> {
if (!this.indexed) throw new Error('Trying to get index, but feature positions are not indexed');
const intId = getNumericId(id);
// binary search for the first occurrence of id in this.ids;
// relies on ids/positions being sorted by id, which happens in serialization
let i = 0;
let j = this.ids.length - 1;
while (i < j) {
const m = (i + j) >> 1;
if (this.ids[m] >= intId) {
j = m;
} else {
i = m + 1;
}
}
const positions = [];
while (this.ids[i] === intId) {
const index = this.positions[3 * i];
const start = this.positions[3 * i + 1];
const end = this.positions[3 * i + 2];
positions.push({index, start, end});
i++;
}
return positions;
}
static serialize(map: FeaturePositionMap, transferables: Array<ArrayBuffer>): SerializedFeaturePositionMap {
const ids = new Float64Array(map.ids);
const positions = new Uint32Array(map.positions);
sort(ids, positions, 0, ids.length - 1);
if (transferables) {
transferables.push(ids.buffer, positions.buffer);
}
return {ids, positions};
}
static deserialize(obj: SerializedFeaturePositionMap): FeaturePositionMap {
const map = new FeaturePositionMap();
// after transferring, we only use these arrays statically (no pushes),
// so TypedArray vs Array distinction that flow points out doesn't matter
map.ids = (obj.ids as any);
map.positions = (obj.positions as any);
map.indexed = true;
return map;
}
}
function getNumericId(value: unknown) {
const numValue = +value;
if (!isNaN(numValue) && numValue <= Number.MAX_SAFE_INTEGER) {
return numValue;
}
return murmur3(String(value));
}
// custom quicksort that sorts ids, indices and offsets together (by ids)
// uses Hoare partitioning & manual tail call optimization to avoid worst case scenarios
function sort(ids, positions, left, right) {
while (left < right) {
const pivot = ids[(left + right) >> 1];
let i = left - 1;
let j = right + 1;
while (true) {
do i++; while (ids[i] < pivot);
do j--; while (ids[j] > pivot);
if (i >= j) break;
swap(ids, i, j);
swap(positions, 3 * i, 3 * j);
swap(positions, 3 * i + 1, 3 * j + 1);
swap(positions, 3 * i + 2, 3 * j + 2);
}
if (j - left < right - j) {
sort(ids, positions, left, j);
left = j + 1;
} else {
sort(ids, positions, j + 1, right);
right = j;
}
}
}
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
register('FeaturePositionMap', FeaturePositionMap);

View File

@@ -0,0 +1,9 @@
import {LineIndexArray, TriangleIndexArray, LineStripIndexArray} from './array_types.g';
/**
* An index array stores Uint16 indices of vertexes in a corresponding vertex array. We use
* three kinds of index arrays: arrays storing groups of three indices, forming triangles;
* arrays storing pairs of indices, forming line segments; and arrays storing single indices,
* forming a line strip.
*/
export {LineIndexArray, TriangleIndexArray, LineStripIndexArray};

View File

@@ -0,0 +1,51 @@
import {describe, test, expect, beforeAll} from 'vitest';
import {loadGeometry} from './load_geometry';
import {loadVectorTile} from '../../test/unit/lib/tile';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
describe('loadGeometry', () => {
let sourceLayer: VectorTileLayerLike;
beforeAll(() => {
// Load line features from fixture tile.
sourceLayer = loadVectorTile().layers.road;
});
test('loadGeometry', () => {
const feature = sourceLayer.feature(0);
const originalGeometry = feature.loadGeometry();
const scaledGeometry = loadGeometry(feature);
expect(scaledGeometry[0][0].x).toBe(originalGeometry[0][0].x * 2);
expect(scaledGeometry[0][0].y).toBe(originalGeometry[0][0].y * 2);
});
test('loadGeometry warns and clamps when exceeding extent', () => {
const feature = sourceLayer.feature(0);
feature.extent = 2048;
let numWarnings = 0;
// Use a custom console.warn to count warnings
const warn = console.warn;
console.warn = (warning) => {
if (warning.match(/Geometry exceeds allowed extent, reduce your vector tile buffer size/)) {
numWarnings++;
}
};
const lines = loadGeometry(feature);
expect(numWarnings).toBe(1);
let maxValue = -Infinity;
for (const line of lines) {
for (const {x, y} of line) {
maxValue = Math.max(x, y, maxValue);
}
}
expect(maxValue).toBe(16383);
// Put it back
console.warn = warn;
});
});

44
node_modules/maplibre-gl/src/data/load_geometry.ts generated vendored Normal file
View File

@@ -0,0 +1,44 @@
import {warnOnce, clamp} from '../util/util';
import {EXTENT} from './extent';
import type Point from '@mapbox/point-geometry';
import type {VectorTileFeatureLike} from '@maplibre/vt-pbf';
// These bounds define the minimum and maximum supported coordinate values.
// While visible coordinates are within [0, EXTENT], tiles may theoretically
// contain coordinates within [-Infinity, Infinity]. Our range is limited by the
// number of bits used to represent the coordinate.
const BITS = 15;
const MAX = Math.pow(2, BITS - 1) - 1;
const MIN = -MAX - 1;
/**
* Loads a geometry from a VectorTileFeatureLike and scales it to the common extent
* used internally.
* @param feature - the vector tile feature to load
*/
export function loadGeometry(feature: VectorTileFeatureLike): Array<Array<Point>> {
const scale = EXTENT / feature.extent;
const geometry = feature.loadGeometry();
for (let r = 0; r < geometry.length; r++) {
const ring = geometry[r];
for (let p = 0; p < ring.length; p++) {
const point = ring[p];
// round here because mapbox-gl-native uses integers to represent
// points and we need to do the same to avoid rendering differences.
const x = Math.round(point.x * scale);
const y = Math.round(point.y * scale);
point.x = clamp(x, MIN, MAX);
point.y = clamp(y, MIN, MAX);
if (x < point.x || x > point.x + 1 || y < point.y || y > point.y + 1) {
// warn when exceeding allowed extent except for the 1-px-off case
// https://github.com/mapbox/mapbox-gl-js/issues/8992
warnOnce('Geometry exceeds allowed extent, reduce your vector tile buffer size');
}
}
}
return geometry;
}

View File

@@ -0,0 +1,5 @@
import {createLayout} from '../util/struct_array';
export default createLayout([
{name: 'a_pos3d', type: 'Int16', components: 3}
]);

5
node_modules/maplibre-gl/src/data/pos_attributes.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
import {createLayout} from '../util/struct_array';
export default createLayout([
{name: 'a_pos', type: 'Int16', components: 2}
]);

View File

@@ -0,0 +1,824 @@
import {packUint8ToFloat} from '../shaders/encode_attribute';
import {type Color, supportsPropertyExpression} from '@maplibre/maplibre-gl-style-spec';
import {register} from '../util/web_worker_transfer';
import {PossiblyEvaluatedPropertyValue} from '../style/properties';
import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray, DashLayoutArray} from './array_types.g';
import {clamp} from '../util/util';
import {patternAttributes} from './bucket/pattern_attributes';
import {dashAttributes} from './bucket/dash_attributes';
import {EvaluationParameters} from '../style/evaluation_parameters';
import {FeaturePositionMap} from './feature_position_map';
import {type Uniform, Uniform1f, UniformColor, Uniform4f} from '../render/uniform_binding';
import type {UniformLocations} from '../render/uniform_binding';
import type {CanonicalTileID} from '../tile/tile_id';
import type {Context} from '../gl/context';
import type {TypedStyleLayer} from '../style/style_layer/typed_style_layer';
import type {CrossfadeParameters} from '../style/evaluation_parameters';
import type {StructArray, StructArrayMember} from '../util/struct_array';
import type {VertexBuffer} from '../gl/vertex_buffer';
import type {ImagePosition} from '../render/image_atlas';
import type {
Feature,
FeatureState,
GlobalProperties,
SourceExpression,
CompositeExpression,
FormattedSection
} from '@maplibre/maplibre-gl-style-spec';
import type {FeatureStates} from '../source/source_state';
import type {DashEntry} from '../render/line_atlas';
import type {VectorTileLayerLike} from '@maplibre/vt-pbf';
export type BinderUniform = {
name: string;
property: string;
binding: Uniform<any>;
};
function packColor(color: Color): [number, number] {
return [
packUint8ToFloat(255 * color.r, 255 * color.g),
packUint8ToFloat(255 * color.b, 255 * color.a)
];
}
type PaintOptions = {
imagePositions: {
[_: string]: ImagePosition;
};
dashPositions?: {
[_: string]: DashEntry;
};
canonical?: CanonicalTileID;
formattedSection?: FormattedSection;
globalState?: Record<string, any>;
};
/**
* `Binder` is the interface definition for the strategies for constructing,
* uploading, and binding paint property data as GLSL attributes. Most style-
* spec properties have a 1:1 relationship to shader attribute/uniforms, but
* some require multiple values per feature to be passed to the GPU, and in
* those cases we bind multiple attributes/uniforms.
*
* It has three implementations, one for each of the three strategies we use:
*
* * For _constant_ properties -- those whose value is a constant, or the constant
* result of evaluating a camera expression at a particular camera position -- we
* don't need a vertex attribute buffer, and instead use a uniform.
* * For data expressions, we use a vertex buffer with a single attribute value,
* the evaluated result of the source function for the given feature.
* * For composite expressions, we use a vertex buffer with two attributes: min and
* max values covering the range of zooms at which we expect the tile to be
* displayed. These values are calculated by evaluating the composite expression for
* the given feature at strategically chosen zoom levels. In addition to this
* attribute data, we also use a uniform value which the shader uses to interpolate
* between the min and max value at the final displayed zoom level. The use of a
* uniform allows us to cheaply update the value on every frame.
*
* Note that the shader source varies depending on whether we're using a uniform or
* attribute. We dynamically compile shaders at runtime to accommodate this.
*/
interface AttributeBinder {
populatePaintArray(
length: number,
feature: Feature,
options: PaintOptions
): void;
updatePaintArray(
start: number,
length: number,
feature: Feature,
featureState: FeatureState,
options: PaintOptions
): void;
upload(a: Context): void;
destroy(): void;
}
interface UniformBinder {
uniformNames: Array<string>;
setUniform(
uniform: Uniform<any>,
globals: GlobalProperties,
currentValue: PossiblyEvaluatedPropertyValue<any>,
uniformName: string
): void;
getBinding(context: Context, location: WebGLUniformLocation, name: string): Partial<Uniform<any>>;
}
class ConstantBinder implements UniformBinder {
value: unknown;
type: string;
uniformNames: Array<string>;
constructor(value: unknown, names: Array<string>, type: string) {
this.value = value;
this.uniformNames = names.map(name => `u_${name}`);
this.type = type;
}
setUniform(
uniform: Uniform<any>,
globals: GlobalProperties,
currentValue: PossiblyEvaluatedPropertyValue<unknown>
): void {
uniform.set(currentValue.constantOr(this.value));
}
getBinding(context: Context, location: WebGLUniformLocation, _: string): Partial<Uniform<any>> {
return (this.type === 'color') ?
new UniformColor(context, location) :
new Uniform1f(context, location);
}
}
class CrossFadedConstantBinder implements UniformBinder {
uniformNames: Array<string>;
patternFrom: Array<number>;
patternTo: Array<number>;
dashFrom: Array<number>;
dashTo: Array<number>;
pixelRatioFrom: number;
pixelRatioTo: number;
constructor(value: unknown, names: Array<string>) {
this.uniformNames = names.map(name => `u_${name}`);
this.patternFrom = null;
this.patternTo = null;
this.pixelRatioFrom = 1.0;
this.pixelRatioTo = 1.0;
}
setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) {
this.pixelRatioFrom = posFrom.pixelRatio;
this.pixelRatioTo = posTo.pixelRatio;
this.patternFrom = posFrom.tlbr;
this.patternTo = posTo.tlbr;
}
setConstantDashPositions(dashTo: DashEntry, dashFrom: DashEntry) {
this.dashTo = [0, dashTo.y, dashTo.height, dashTo.width];
this.dashFrom = [0, dashFrom.y, dashFrom.height, dashFrom.width];
}
setUniform(uniform: Uniform<any>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue<unknown>, uniformName: string) {
let value = null;
if (uniformName === 'u_pattern_to') {
value = this.patternTo;
} else if (uniformName === 'u_pattern_from') {
value = this.patternFrom;
} else if (uniformName === 'u_dasharray_to') {
value = this.dashTo;
} else if (uniformName === 'u_dasharray_from') {
value = this.dashFrom;
} else if (uniformName === 'u_pixel_ratio_to') {
value = this.pixelRatioTo;
} else if (uniformName === 'u_pixel_ratio_from') {
value = this.pixelRatioFrom;
}
if (value !== null) {
uniform.set(value);
}
}
getBinding(context: Context, location: WebGLUniformLocation, name: string): Partial<Uniform<any>> {
return (name.startsWith('u_pattern') || name.startsWith('u_dasharray_')) ?
new Uniform4f(context, location) :
new Uniform1f(context, location);
}
}
class SourceExpressionBinder implements AttributeBinder {
expression: SourceExpression;
type: string;
maxValue: number;
paintVertexArray: StructArray;
paintVertexAttributes: Array<StructArrayMember>;
paintVertexBuffer: VertexBuffer;
constructor(expression: SourceExpression, names: Array<string>, type: string, PaintVertexArray: {
new (...args: any): StructArray;
}) {
this.expression = expression;
this.type = type;
this.maxValue = 0;
this.paintVertexAttributes = names.map((name) => ({
name: `a_${name}`,
type: 'Float32',
components: type === 'color' ? 2 : 1,
offset: 0
}));
this.paintVertexArray = new PaintVertexArray();
}
populatePaintArray(newLength: number, feature: Feature, options: PaintOptions) {
const start = this.paintVertexArray.length;
const value = this.expression.evaluate(new EvaluationParameters(0, options), feature, {}, options.canonical, [], options.formattedSection);
this.paintVertexArray.resize(newLength);
this._setPaintValue(start, newLength, value);
}
updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) {
const value = this.expression.evaluate(new EvaluationParameters(0, options), feature, featureState);
this._setPaintValue(start, end, value);
}
_setPaintValue(start, end, value) {
if (this.type === 'color') {
const color = packColor(value);
for (let i = start; i < end; i++) {
this.paintVertexArray.emplace(i, color[0], color[1]);
}
} else {
for (let i = start; i < end; i++) {
this.paintVertexArray.emplace(i, value);
}
this.maxValue = Math.max(this.maxValue, Math.abs(value));
}
}
upload(context: Context) {
if (this.paintVertexArray?.arrayBuffer.byteLength) {
if (this.paintVertexBuffer && this.paintVertexBuffer.buffer) {
this.paintVertexBuffer.updateData(this.paintVertexArray);
} else {
this.paintVertexBuffer = context.createVertexBuffer(this.paintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent);
}
}
}
destroy() {
if (this.paintVertexBuffer) {
this.paintVertexBuffer.destroy();
}
}
}
class CompositeExpressionBinder implements AttributeBinder, UniformBinder {
expression: CompositeExpression;
uniformNames: Array<string>;
type: string;
useIntegerZoom: boolean;
zoom: number;
maxValue: number;
paintVertexArray: StructArray;
paintVertexAttributes: Array<StructArrayMember>;
paintVertexBuffer: VertexBuffer;
constructor(expression: CompositeExpression, names: Array<string>, type: string, useIntegerZoom: boolean, zoom: number, PaintVertexArray: {
new (...args: any): StructArray;
}) {
this.expression = expression;
this.uniformNames = names.map(name => `u_${name}_t`);
this.type = type;
this.useIntegerZoom = useIntegerZoom;
this.zoom = zoom;
this.maxValue = 0;
this.paintVertexAttributes = names.map((name) => ({
name: `a_${name}`,
type: 'Float32',
components: type === 'color' ? 4 : 2,
offset: 0
}));
this.paintVertexArray = new PaintVertexArray();
}
populatePaintArray(newLength: number, feature: Feature, options: PaintOptions) {
const min = this.expression.evaluate(new EvaluationParameters(this.zoom, options), feature, {}, options.canonical, [], options.formattedSection);
const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1, options), feature, {}, options.canonical, [], options.formattedSection);
const start = this.paintVertexArray.length;
this.paintVertexArray.resize(newLength);
this._setPaintValue(start, newLength, min, max);
}
updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) {
const min = this.expression.evaluate(new EvaluationParameters(this.zoom, options), feature, featureState);
const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1, options), feature, featureState);
this._setPaintValue(start, end, min, max);
}
_setPaintValue(start, end, min, max) {
if (this.type === 'color') {
const minColor = packColor(min);
const maxColor = packColor(max);
for (let i = start; i < end; i++) {
this.paintVertexArray.emplace(i, minColor[0], minColor[1], maxColor[0], maxColor[1]);
}
} else {
for (let i = start; i < end; i++) {
this.paintVertexArray.emplace(i, min, max);
}
this.maxValue = Math.max(this.maxValue, Math.abs(min), Math.abs(max));
}
}
upload(context: Context) {
if (this.paintVertexArray?.arrayBuffer.byteLength) {
if (this.paintVertexBuffer && this.paintVertexBuffer.buffer) {
this.paintVertexBuffer.updateData(this.paintVertexArray);
} else {
this.paintVertexBuffer = context.createVertexBuffer(this.paintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent);
}
}
}
destroy() {
if (this.paintVertexBuffer) {
this.paintVertexBuffer.destroy();
}
}
setUniform(uniform: Uniform<any>, globals: GlobalProperties): void {
const currentZoom = this.useIntegerZoom ? Math.floor(globals.zoom) : globals.zoom;
const factor = clamp(this.expression.interpolationFactor(currentZoom, this.zoom, this.zoom + 1), 0, 1);
uniform.set(factor);
}
getBinding(context: Context, location: WebGLUniformLocation, _: string): Uniform1f {
return new Uniform1f(context, location);
}
}
abstract class CrossFadedBinder<T> implements AttributeBinder {
expression: CompositeExpression;
type: string;
useIntegerZoom: boolean;
zoom: number;
layerId: string;
zoomInPaintVertexArray: StructArray;
zoomOutPaintVertexArray: StructArray;
zoomInPaintVertexBuffer: VertexBuffer;
zoomOutPaintVertexBuffer: VertexBuffer;
paintVertexAttributes: Array<StructArrayMember>;
constructor(expression: CompositeExpression, type: string, useIntegerZoom: boolean, zoom: number, PaintVertexArray: {
new (...args: any): StructArray;
}, layerId: string) {
this.expression = expression;
this.type = type;
this.useIntegerZoom = useIntegerZoom;
this.zoom = zoom;
this.layerId = layerId;
this.zoomInPaintVertexArray = new PaintVertexArray();
this.zoomOutPaintVertexArray = new PaintVertexArray();
}
populatePaintArray(length: number, feature: Feature, options: PaintOptions) {
const start = this.zoomInPaintVertexArray.length;
this.zoomInPaintVertexArray.resize(length);
this.zoomOutPaintVertexArray.resize(length);
this._setPaintValues(start, length, this.getPositionIds(feature), options);
}
updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, options: PaintOptions) {
this._setPaintValues(start, end, this.getPositionIds(feature), options);
}
abstract getVertexAttributes(): Array<StructArrayMember>;
protected abstract getPositionIds(feature: Feature): {min: string; mid: string; max: string};
protected abstract getPositions(options: PaintOptions): {[_: string]: T};
protected abstract emplace(array: StructArray, index: number, midPos: T, minMaxPos: T): void;
protected _setPaintValues(start: number, end: number, positionIds: {min: string; mid: string; max: string}, options: PaintOptions) {
const positions = this.getPositions(options);
if (!positions || !positionIds) return;
const min = positions[positionIds.min];
const mid = positions[positionIds.mid];
const max = positions[positionIds.max];
if (!min || !mid || !max) return;
// We populate two paint arrays because, for cross-faded properties, we don't know which direction
// we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass
// unnecessary vertex data to the shaders, we determine which to upload at draw time.
for (let i = start; i < end; i++) {
this.emplace(this.zoomInPaintVertexArray, i, mid, min);
this.emplace(this.zoomOutPaintVertexArray, i, mid, max);
}
}
upload(context: Context) {
if (this.zoomInPaintVertexArray?.arrayBuffer.byteLength && this.zoomOutPaintVertexArray?.arrayBuffer.byteLength) {
const attributes = this.getVertexAttributes();
this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, attributes, this.expression.isStateDependent);
this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, attributes, this.expression.isStateDependent);
}
}
destroy() {
if (this.zoomOutPaintVertexBuffer) this.zoomOutPaintVertexBuffer.destroy();
if (this.zoomInPaintVertexBuffer) this.zoomInPaintVertexBuffer.destroy();
}
}
class CrossFadedPatternBinder extends CrossFadedBinder<ImagePosition> {
protected getPositions(options: PaintOptions): {[_: string]: ImagePosition} {
return options.imagePositions;
}
protected getPositionIds(feature: Feature) {
return feature.patterns && feature.patterns[this.layerId];
}
getVertexAttributes(): Array<StructArrayMember> {
return patternAttributes.members;
}
protected emplace(array: StructArray, index: number, midPos: ImagePosition, minMaxPos: ImagePosition): void {
array.emplace(index,
midPos.tlbr[0], midPos.tlbr[1], midPos.tlbr[2], midPos.tlbr[3],
minMaxPos.tlbr[0], minMaxPos.tlbr[1], minMaxPos.tlbr[2], minMaxPos.tlbr[3],
midPos.pixelRatio,
minMaxPos.pixelRatio,
);
}
}
class CrossFadedDasharrayBinder extends CrossFadedBinder<DashEntry> {
protected getPositions(options: PaintOptions): {[_: string]: DashEntry} {
return options.dashPositions;
}
protected getPositionIds(feature: Feature) {
return feature.dashes && feature.dashes[this.layerId];
}
getVertexAttributes(): Array<StructArrayMember> {
return dashAttributes.members;
}
protected emplace(array: StructArray, index: number, midPos: DashEntry, minMaxPos: DashEntry): void {
array.emplace(index,
0, midPos.y, midPos.height, midPos.width,
0, minMaxPos.y, minMaxPos.height, minMaxPos.width,
);
}
}
/**
* @internal
* ProgramConfiguration contains the logic for binding style layer properties and tile
* layer feature data into GL program uniforms and vertex attributes.
*
* Non-data-driven property values are bound to shader uniforms. Data-driven property
* values are bound to vertex attributes. In order to support a uniform GLSL syntax over
* both, [Mapbox GL Shaders](https://github.com/mapbox/mapbox-gl-shaders) defines a `#pragma`
* abstraction, which ProgramConfiguration is responsible for implementing. At runtime,
* it examines the attributes of a particular layer, combines this with fixed knowledge
* about how layers of the particular type are implemented, and determines which uniforms
* and vertex attributes will be required. It can then substitute the appropriate text
* into the shader source code, create and link a program, and bind the uniforms and
* vertex attributes in preparation for drawing.
*
* When a vector tile is parsed, this same configuration information is used to
* populate the attribute buffers needed for data-driven styling using the zoom
* level and feature property data.
*/
export class ProgramConfiguration {
binders: {[_: string]: AttributeBinder | UniformBinder};
cacheKey: string;
_buffers: Array<VertexBuffer>;
constructor(layer: TypedStyleLayer, zoom: number, filterProperties: (_: string) => boolean) {
this.binders = {};
this._buffers = [];
const keys = [];
for (const property in layer.paint._values) {
if (!filterProperties(property)) continue;
const value = (layer.paint as any).get(property);
if (!(value instanceof PossiblyEvaluatedPropertyValue) || !supportsPropertyExpression(value.property.specification)) {
continue;
}
const names = paintAttributeNames(property, layer.type);
const expression = value.value;
const type = value.property.specification.type;
const useIntegerZoom = (value.property as any).useIntegerZoom;
const propType = value.property.specification['property-type'];
const isCrossFaded = propType === 'cross-faded' || propType === 'cross-faded-data-driven';
if (expression.kind === 'constant') {
this.binders[property] = isCrossFaded ?
new CrossFadedConstantBinder(expression.value, names) :
new ConstantBinder(expression.value, names, type);
keys.push(`/u_${property}`);
} else if (expression.kind === 'source' || isCrossFaded) {
const StructArrayLayout = layoutType(property, type, 'source');
this.binders[property] = isCrossFaded ?
property === 'line-dasharray' ?
new CrossFadedDasharrayBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
new CrossFadedPatternBinder(expression as CompositeExpression, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
new SourceExpressionBinder(expression as SourceExpression, names, type, StructArrayLayout);
keys.push(`/a_${property}`);
} else {
const StructArrayLayout = layoutType(property, type, 'composite');
this.binders[property] = new CompositeExpressionBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout);
keys.push(`/z_${property}`);
}
}
this.cacheKey = keys.sort().join('');
}
getMaxValue(property: string): number {
const binder = this.binders[property];
return binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ? binder.maxValue : 0;
}
populatePaintArrays(newLength: number, feature: Feature, options: PaintOptions) {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.populatePaintArray(newLength, feature, options);
}
}
setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof CrossFadedConstantBinder)
binder.setConstantPatternPositions(posTo, posFrom);
}
}
setConstantDashPositions(dashTo: DashEntry, dashFrom: DashEntry) {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof CrossFadedConstantBinder)
binder.setConstantDashPositions(dashTo, dashFrom);
}
}
updatePaintArrays(
featureStates: FeatureStates,
featureMap: FeaturePositionMap,
vtLayer: VectorTileLayerLike,
layer: TypedStyleLayer,
options: PaintOptions
): boolean {
let dirty: boolean = false;
for (const id in featureStates) {
const positions = featureMap.getPositions(id);
for (const pos of positions) {
const feature = vtLayer.feature(pos.index);
for (const property in this.binders) {
const binder = this.binders[property];
if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ||
binder instanceof CrossFadedBinder) && binder.expression.isStateDependent === true) {
//AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255
const value = (layer.paint as any).get(property);
binder.expression = value.value;
binder.updatePaintArray(pos.start, pos.end, feature, featureStates[id], options);
dirty = true;
}
}
}
}
return dirty;
}
defines(): Array<string> {
const result = [];
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof ConstantBinder || binder instanceof CrossFadedConstantBinder) {
result.push(...binder.uniformNames.map(name => `#define HAS_UNIFORM_${name}`));
}
}
return result;
}
getBinderAttributes(): Array<string> {
const result = [];
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder) {
for (let i = 0; i < binder.paintVertexAttributes.length; i++) {
result.push(binder.paintVertexAttributes[i].name);
}
} else if (binder instanceof CrossFadedBinder) {
const attributes = binder.getVertexAttributes();
for (const attribute of attributes) {
result.push(attribute.name);
}
}
}
return result;
}
getBinderUniforms(): Array<string> {
const uniforms = [];
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof ConstantBinder || binder instanceof CrossFadedConstantBinder || binder instanceof CompositeExpressionBinder) {
for (const uniformName of binder.uniformNames) {
uniforms.push(uniformName);
}
}
}
return uniforms;
}
getPaintVertexBuffers(): Array<VertexBuffer> {
return this._buffers;
}
getUniforms(context: Context, locations: UniformLocations): Array<BinderUniform> {
const uniforms = [];
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof ConstantBinder || binder instanceof CrossFadedConstantBinder || binder instanceof CompositeExpressionBinder) {
for (const name of binder.uniformNames) {
if (locations[name]) {
const binding = binder.getBinding(context, locations[name], name);
uniforms.push({name, property, binding});
}
}
}
}
return uniforms;
}
setUniforms(
context: Context,
binderUniforms: Array<BinderUniform>,
properties: any,
globals: GlobalProperties
) {
// Uniform state bindings are owned by the Program, but we set them
// from within the ProgramConfiguration's binder members.
for (const {name, property, binding} of binderUniforms) {
(this.binders[property] as any).setUniform(binding, globals, properties.get(property), name);
}
}
updatePaintBuffers(crossfade?: CrossfadeParameters) {
this._buffers = [];
for (const property in this.binders) {
const binder = this.binders[property];
if (crossfade && binder instanceof CrossFadedBinder) {
const patternVertexBuffer = crossfade.fromScale === 2 ? binder.zoomInPaintVertexBuffer : binder.zoomOutPaintVertexBuffer;
if (patternVertexBuffer) this._buffers.push(patternVertexBuffer);
} else if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder) && binder.paintVertexBuffer) {
this._buffers.push(binder.paintVertexBuffer);
}
}
}
upload(context: Context) {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.upload(context);
}
this.updatePaintBuffers();
}
destroy() {
for (const property in this.binders) {
const binder = this.binders[property];
if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedBinder)
binder.destroy();
}
}
}
export class ProgramConfigurationSet<Layer extends TypedStyleLayer> {
programConfigurations: {[_: string]: ProgramConfiguration};
needsUpload: boolean;
_featureMap: FeaturePositionMap;
_bufferOffset: number;
constructor(layers: ReadonlyArray<Layer>, zoom: number, filterProperties: (_: string) => boolean = () => true) {
this.programConfigurations = {};
for (const layer of layers) {
this.programConfigurations[layer.id] = new ProgramConfiguration(layer, zoom, filterProperties);
}
this.needsUpload = false;
this._featureMap = new FeaturePositionMap();
this._bufferOffset = 0;
}
populatePaintArrays(length: number, feature: Feature, index: number, options: PaintOptions) {
for (const key in this.programConfigurations) {
this.programConfigurations[key].populatePaintArrays(length, feature, options);
}
if (feature.id !== undefined) {
this._featureMap.add(feature.id, index, this._bufferOffset, length);
}
this._bufferOffset = length;
this.needsUpload = true;
}
updatePaintArrays(featureStates: FeatureStates, vtLayer: VectorTileLayerLike, layers: ReadonlyArray<TypedStyleLayer>, options: PaintOptions) {
for (const layer of layers) {
this.needsUpload = this.programConfigurations[layer.id].updatePaintArrays(featureStates, this._featureMap, vtLayer, layer, options) || this.needsUpload;
}
}
get(layerId: string) {
return this.programConfigurations[layerId];
}
upload(context: Context) {
if (!this.needsUpload) return;
for (const layerId in this.programConfigurations) {
this.programConfigurations[layerId].upload(context);
}
this.needsUpload = false;
}
destroy() {
for (const layerId in this.programConfigurations) {
this.programConfigurations[layerId].destroy();
}
}
}
function paintAttributeNames(property: string, type: string) {
const attributeNameExceptions = {
'text-opacity': ['opacity'],
'icon-opacity': ['opacity'],
'text-color': ['fill_color'],
'icon-color': ['fill_color'],
'text-halo-color': ['halo_color'],
'icon-halo-color': ['halo_color'],
'text-halo-blur': ['halo_blur'],
'icon-halo-blur': ['halo_blur'],
'text-halo-width': ['halo_width'],
'icon-halo-width': ['halo_width'],
'line-gap-width': ['gapwidth'],
'line-dasharray': ['dasharray_to', 'dasharray_from'],
'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
};
return attributeNameExceptions[property] || [property.replace(`${type}-`, '').replace(/-/g, '_')];
}
function getLayoutException(property: string) {
const propertyExceptions = {
'line-pattern': {
'source': PatternLayoutArray,
'composite': PatternLayoutArray
},
'fill-pattern': {
'source': PatternLayoutArray,
'composite': PatternLayoutArray
},
'fill-extrusion-pattern': {
'source': PatternLayoutArray,
'composite': PatternLayoutArray
},
'line-dasharray': {
'source': DashLayoutArray,
'composite': DashLayoutArray
},
};
return propertyExceptions[property];
}
function layoutType(property: string, type: string, binderType: string) {
const defaultLayouts = {
'color': {
'source': StructArrayLayout2f8,
'composite': StructArrayLayout4f16
},
'number': {
'source': StructArrayLayout1f4,
'composite': StructArrayLayout2f8
}
};
const layoutException = getLayoutException(property);
return layoutException && layoutException[binderType] || defaultLayouts[type][binderType];
}
register('ConstantBinder', ConstantBinder);
register('CrossFadedConstantBinder', CrossFadedConstantBinder);
register('SourceExpressionBinder', SourceExpressionBinder);
register('CrossFadedPatternBinder', CrossFadedPatternBinder);
register('CrossFadedDasharrayBinder', CrossFadedDasharrayBinder);
register('CompositeExpressionBinder', CompositeExpressionBinder);
register('ProgramConfiguration', ProgramConfiguration, {omit: ['_buffers']});
register('ProgramConfigurationSet', ProgramConfigurationSet);

View File

@@ -0,0 +1,6 @@
import {createLayout} from '../util/struct_array';
export default createLayout([
{name: 'a_pos', type: 'Int16', components: 2},
{name: 'a_texture_pos', type: 'Int16', components: 2}
]);

221
node_modules/maplibre-gl/src/data/segment.test.ts generated vendored Normal file
View File

@@ -0,0 +1,221 @@
import {describe, expect, test} from 'vitest';
import {FillLayoutArray, TriangleIndexArray} from './array_types.g';
import {SegmentVector} from './segment';
describe('SegmentVector', () => {
test('constructor', () => {
expect(new SegmentVector() instanceof SegmentVector).toBeTruthy();
});
test('simpleSegment', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const segmentVector = SegmentVector.simpleSegment(0, 0, 10, 0);
expect(segmentVector instanceof SegmentVector).toBeTruthy();
expect(segmentVector.segments).toHaveLength(1);
expect(segmentVector.segments[0].vertexLength).toBe(10);
});
test('prepareSegment returns a segment', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const result = segmentVector.prepareSegment(10, vertexBuffer, indexBuffer);
expect(result).toBeTruthy();
expect(result.vertexLength).toBe(0);
});
test('prepareSegment handles vertex overflow', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 10);
const second = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 10);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(first === second).toBe(false);
expect(first.vertexLength).toBe(10);
expect(second.vertexLength).toBe(10);
expect(segmentVector.segments).toHaveLength(2);
});
test('prepareSegment reuses segments', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
const second = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(first === second).toBe(true);
expect(first.vertexLength).toBe(10);
});
test('createNewSegment returns a new segment', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
const second = segmentVector.createNewSegment(vertexBuffer, indexBuffer);
second.vertexLength += 5;
addVertices(vertexBuffer, 5);
const third = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(third).toBeTruthy();
expect(first === second).toBe(false);
expect(second === third).toBe(true);
expect(first.vertexLength).toBe(5);
expect(third.vertexLength).toBe(10);
});
test('createNewSegment returns a new segment and resets invalidateLast', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
segmentVector.forceNewSegmentOnNextPrepare();
const second = segmentVector.createNewSegment(vertexBuffer, indexBuffer);
second.vertexLength += 5;
addVertices(vertexBuffer, 5);
const third = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(third).toBeTruthy();
expect(first === second).toBe(false);
expect(second === third).toBe(true);
expect(first.vertexLength).toBe(5);
expect(third.vertexLength).toBe(10);
});
test('getOrCreateLatestSegment creates a new segment if SegmentVector was empty', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = segmentVector.getOrCreateLatestSegment(vertexBuffer, indexBuffer);
expect(first).toBeTruthy();
expect(segmentVector.segments).toHaveLength(1);
});
test('getOrCreateLatestSegment returns the last segment if invalidateLast=false', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
const second = segmentVector.getOrCreateLatestSegment(vertexBuffer, indexBuffer);
second.vertexLength += 5;
addVertices(vertexBuffer, 5);
const third = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(third).toBeTruthy();
expect(first === second).toBe(true);
expect(second === third).toBe(true);
expect(first.vertexLength).toBe(15);
});
test('getOrCreateLatestSegment respects invalidateLast and returns a new segment', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
segmentVector.forceNewSegmentOnNextPrepare();
const second = segmentVector.getOrCreateLatestSegment(vertexBuffer, indexBuffer);
second.vertexLength += 5;
addVertices(vertexBuffer, 5);
const third = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(third).toBeTruthy();
expect(first === second).toBe(false);
expect(second === third).toBe(true);
expect(first.vertexLength).toBe(5);
expect(third.vertexLength).toBe(10);
});
test('prepareSegment respects invalidateLast', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
segmentVector.forceNewSegmentOnNextPrepare();
const second = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
const third = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(third).toBeTruthy();
expect(first === second).toBe(false);
expect(second === third).toBe(true);
expect(first.vertexLength).toBe(5);
expect(second.vertexLength).toBe(10);
expect(segmentVector.segments).toHaveLength(2);
});
test('invalidateLast called twice has no effect', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
segmentVector.forceNewSegmentOnNextPrepare();
segmentVector.forceNewSegmentOnNextPrepare();
const second = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(first === second).toBe(false);
expect(first.vertexLength).toBe(5);
expect(second.vertexLength).toBe(5);
expect(segmentVector.segments).toHaveLength(2);
});
test('invalidateLast called on an empty SegmentVector has no effect', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
segmentVector.forceNewSegmentOnNextPrepare();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5);
expect(first).toBeTruthy();
expect(first.vertexLength).toBe(5);
expect(segmentVector.segments).toHaveLength(1);
});
test('prepareSegment respects different sortKey', () => {
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = 16;
const vertexBuffer = new FillLayoutArray();
const indexBuffer = new TriangleIndexArray();
const segmentVector = new SegmentVector();
const first = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5, 1);
const second = mockUseSegment(segmentVector, vertexBuffer, indexBuffer, 5, 2);
expect(first).toBeTruthy();
expect(second).toBeTruthy();
expect(first === second).toBe(false);
expect(first.vertexLength).toBe(5);
expect(second.vertexLength).toBe(5);
expect(segmentVector.segments).toHaveLength(2);
});
});
/**
* Mocks the usage of a segment from SegmentVector. Returns the used segment.
*/
function mockUseSegment(segmentVector: SegmentVector, vertexBuffer: FillLayoutArray, indexBuffer: TriangleIndexArray, numVertices: number, sortKey?: number) {
const seg = segmentVector.prepareSegment(numVertices, vertexBuffer, indexBuffer, sortKey);
seg.vertexLength += numVertices;
addVertices(vertexBuffer, numVertices);
return seg;
}
function addVertices(array: FillLayoutArray, count: number) {
for (let i = 0; i < count; i++) {
array.emplaceBack(0, 0);
}
}

138
node_modules/maplibre-gl/src/data/segment.ts generated vendored Normal file
View File

@@ -0,0 +1,138 @@
import {warnOnce} from '../util/util';
import {register} from '../util/web_worker_transfer';
import type {VertexArrayObject} from '../render/vertex_array_object';
import type {StructArray} from '../util/struct_array';
/**
* @internal
* A single segment of a vector
*/
export type Segment = {
sortKey?: number;
vertexOffset: number;
primitiveOffset: number;
vertexLength: number;
primitiveLength: number;
vaos: {[_: string]: VertexArrayObject};
};
/**
* @internal
* Used for calculations on vector segments
*/
export class SegmentVector {
static MAX_VERTEX_ARRAY_LENGTH: number;
segments: Array<Segment>;
private _forceNewSegmentOnNextPrepare: boolean = false;
constructor(segments: Array<Segment> = []) {
this.segments = segments;
}
/**
* Returns the last segment if `numVertices` fits into it.
* If there are no segments yet or `numVertices` doesn't fit into the last one, creates a new empty segment and returns it.
*/
prepareSegment(
numVertices: number,
layoutVertexArray: StructArray,
indexArray: StructArray,
sortKey?: number
): Segment {
const lastSegment: Segment = this.segments[this.segments.length - 1];
if (numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH) {
warnOnce(`Max vertices per segment is ${SegmentVector.MAX_VERTEX_ARRAY_LENGTH}: bucket requested ${numVertices}. Consider using the \`fillLargeMeshArrays\` function if you require meshes with more than ${SegmentVector.MAX_VERTEX_ARRAY_LENGTH} vertices.`);
}
if (this._forceNewSegmentOnNextPrepare || !lastSegment || lastSegment.vertexLength + numVertices > SegmentVector.MAX_VERTEX_ARRAY_LENGTH || lastSegment.sortKey !== sortKey) {
return this.createNewSegment(layoutVertexArray, indexArray, sortKey);
} else {
return lastSegment;
}
}
/**
* Creates a new empty segment and returns it.
*/
createNewSegment(
layoutVertexArray: StructArray,
indexArray: StructArray,
sortKey?: number
): Segment {
const segment: Segment = {
vertexOffset: layoutVertexArray.length,
primitiveOffset: indexArray.length,
vertexLength: 0,
primitiveLength: 0,
vaos: {}
};
if (sortKey !== undefined) {
segment.sortKey = sortKey;
}
// If this was set, we have no need to create a new segment on next prepareSegment call,
// since this function already created a new, empty segment.
this._forceNewSegmentOnNextPrepare = false;
this.segments.push(segment);
return segment;
}
/**
* Returns the last segment, or creates a new segments if there are no segments yet.
*/
getOrCreateLatestSegment(
layoutVertexArray: StructArray,
indexArray: StructArray,
sortKey?: number
): Segment {
return this.prepareSegment(0, layoutVertexArray, indexArray, sortKey);
}
/**
* Causes the next call to {@link prepareSegment} to always return a new segment,
* not reusing the current segment even if the new geometry would fit it.
*/
forceNewSegmentOnNextPrepare() {
this._forceNewSegmentOnNextPrepare = true;
}
get() {
return this.segments;
}
destroy() {
for (const segment of this.segments) {
for (const k in segment.vaos) {
segment.vaos[k].destroy();
}
}
}
static simpleSegment(
vertexOffset: number,
primitiveOffset: number,
vertexLength: number,
primitiveLength: number
): SegmentVector {
return new SegmentVector([{
vertexOffset,
primitiveOffset,
vertexLength,
primitiveLength,
vaos: {},
sortKey: 0
}]);
}
}
/**
* The maximum size of a vertex array. This limit is imposed by WebGL's 16 bit
* addressing of vertex buffers.
*/
SegmentVector.MAX_VERTEX_ARRAY_LENGTH = Math.pow(2, 16) - 1;
register('SegmentVector', SegmentVector);

159
node_modules/maplibre-gl/src/geo/bounds.test.ts generated vendored Normal file
View File

@@ -0,0 +1,159 @@
import {describe, test, expect} from 'vitest';
import {Bounds} from './bounds';
import Point from '@mapbox/point-geometry';
function bounds(minX: number, minY: number, maxX: number, maxY: number): Bounds {
return Bounds.fromPoints([
new Point(minX, minY),
new Point(maxX, maxY),
]);
}
describe('Bounds', () => {
test('empty bounding box', () => {
const empty = new Bounds();
expect(empty).toBeInstanceOf(Bounds);
expect(empty.contains(new Point(0, 0))).toBeFalsy();
expect(empty.empty()).toBeTruthy();
});
test('add single point', () => {
const bounds = new Bounds();
bounds.extend(new Point(1, 2));
expect(bounds.empty()).toBeFalsy();
expect(bounds.height()).toEqual(0);
expect(bounds.width()).toEqual(0);
expect(bounds.contains(new Point(1, 2))).toBeTruthy();
expect(bounds.contains(new Point(2, 2))).toBeFalsy();
expect(bounds.contains(new Point(-1, 2))).toBeFalsy();
expect(bounds.contains(new Point(1, 1))).toBeFalsy();
expect(bounds.contains(new Point(1, 3))).toBeFalsy();
});
test('add multiple points', () => {
const bounds = new Bounds();
bounds.extend(new Point(1, 2));
bounds.extend(new Point(3, 5));
expect(bounds.empty()).toBeFalsy();
expect(bounds.width()).toEqual(2);
expect(bounds.height()).toEqual(3);
expect(bounds.contains(new Point(1, 2))).toBeTruthy();
expect(bounds.contains(new Point(3, 2))).toBeTruthy();
expect(bounds.contains(new Point(3, 5))).toBeTruthy();
expect(bounds.contains(new Point(1, 5))).toBeTruthy();
expect(bounds.contains(new Point(0.9, 1.9))).toBeFalsy();
expect(bounds.contains(new Point(3.1, 1.9))).toBeFalsy();
expect(bounds.contains(new Point(3.1, 5.1))).toBeFalsy();
expect(bounds.contains(new Point(2, 5.1))).toBeFalsy();
});
test('fromPoints', () => {
const bounds = Bounds.fromPoints([new Point(1, 2), new Point(3, 4)]);
expect(bounds).toMatchObject({
minX: 1,
maxX: 3,
minY: 2,
maxY: 4,
});
});
test('expandBy positive', () => {
const bounds = new Bounds();
bounds.extend(new Point(0, 0));
bounds.expandBy(1);
expect(bounds).toMatchObject({
minX: -1,
maxX: 1,
minY: -1,
maxY: 1,
});
});
test('expandBy negative', () => {
const bounds = new Bounds();
bounds.extend(new Point(0, 0));
bounds.expandBy(2);
bounds.expandBy(-1);
expect(bounds.empty()).toBeFalsy();
expect(bounds).toMatchObject({
minX: -1,
maxX: 1,
minY: -1,
maxY: 1,
});
});
test('shrinkBy', () => {
const bounds = new Bounds();
bounds.extend(new Point(0, 0));
bounds.expandBy(2);
bounds.shrinkBy(1);
expect(bounds.empty()).toBeFalsy();
expect(bounds).toMatchObject({
minX: -1,
maxX: 1,
minY: -1,
maxY: 1,
});
});
test('expandBy collapse', () => {
const bounds = new Bounds();
bounds.extend(new Point(0, 0));
bounds.expandBy(2);
bounds.expandBy(-3);
expect(bounds.empty()).toBeTruthy();
});
test('map', () => {
const bounds = new Bounds();
bounds.extend(new Point(1, 2));
bounds.extend(new Point(3, 4));
expect(bounds.map(point => new Point(-point.y, -point.x))).toEqual({
minX: -4,
minY: -3,
maxX: -2,
maxY: -1,
});
});
test('covers', () => {
const e = 0.1;
const box = bounds(1, 2, 3, 4);
expect(box.covers(box)).toBeTruthy();
expect(box.covers(bounds(1-e, 2, 3, 4))).toBeFalsy();
expect(box.covers(bounds(1, 2-e, 3, 4))).toBeFalsy();
expect(box.covers(bounds(1, 2, 3+e, 4))).toBeFalsy();
expect(box.covers(bounds(1, 2, 3, 4+e))).toBeFalsy();
expect(box.covers(bounds(1+e, 2, 3, 4))).toBeTruthy();
expect(box.covers(bounds(1, 2+e, 3, 4))).toBeTruthy();
expect(box.covers(bounds(1, 2, 3-e, 4))).toBeTruthy();
expect(box.covers(bounds(1, 2, 3, 4-e))).toBeTruthy();
});
test('intersects', () => {
const e = 0.1;
const box = bounds(1, 2, 3, 4);
expect(box.intersects(box)).toBeTruthy();
// bottom-left corner
expect(box.intersects(bounds(0, 0, 1, 2))).toBeTruthy();
expect(box.intersects(bounds(0, 0, 1-e, 2))).toBeFalsy();
expect(box.intersects(bounds(0, 0, 1, 2-e))).toBeFalsy();
// bottom-right corner
expect(box.intersects(bounds(3, 0, 10, 2))).toBeTruthy();
expect(box.intersects(bounds(3+e, 0, 10, 2))).toBeFalsy();
expect(box.intersects(bounds(3, 0, 10, 2-e))).toBeFalsy();
// top-left corner
expect(box.intersects(bounds(0, 4, 1, 8))).toBeTruthy();
expect(box.intersects(bounds(0, 4+e, 1, 8))).toBeFalsy();
expect(box.intersects(bounds(0, 4, 1-e, 8))).toBeFalsy();
// top-right corner
expect(box.intersects(bounds(3, 4, 10, 10))).toBeTruthy();
expect(box.intersects(bounds(3+e, 4, 10, 10))).toBeFalsy();
expect(box.intersects(bounds(3, 4+e, 10, 10))).toBeFalsy();
});
});

169
node_modules/maplibre-gl/src/geo/bounds.ts generated vendored Normal file
View File

@@ -0,0 +1,169 @@
import Point from '@mapbox/point-geometry';
import {type Point2D} from '@maplibre/maplibre-gl-style-spec';
export interface ReadOnlyBounds {
readonly minX: number;
readonly maxX: number;
readonly minY: number;
readonly maxY: number;
/**
* Returns whether this bounding box contains a point
*
* @param point - The point to check
* @returns True if this bounding box contains point, false otherwise.
*/
contains(point: Point2D): boolean;
/**
* Returns true if this bounding box contains no points
*
* @returns True if this bounding box contains no points.
*/
empty(): boolean;
/**
* Returns the width of this bounding box.
*
* @returns `maxX - minX`.
*/
width(): number;
/**
* Returns the height of this bounding box.
*
* @returns `maxY - minY`.
*/
height(): number;
/**
* Returns true if this bounding box completely covers `other`.
*
* @param other - The other bounding box
* @returns True if this bounding box completely encloses `other`
*/
covers(other: ReadOnlyBounds): boolean;
/**
* Returns true if this bounding box touches any part of `other`.
*
* @param other - The other bounding box
* @returns True if this bounding box touches any part of `other`.
*/
intersects(other: ReadOnlyBounds): boolean;
}
/** A 2-d bounding box covering an X and Y range. */
export class Bounds implements ReadOnlyBounds {
minX: number = Infinity;
maxX: number = -Infinity;
minY: number = Infinity;
maxY: number = -Infinity;
/**
* Expands this bounding box to include point.
*
* @param point - The point to include in this bounding box
* @returns This mutated bounding box
*/
extend(point: Point2D): this {
this.minX = Math.min(this.minX, point.x);
this.minY = Math.min(this.minY, point.y);
this.maxX = Math.max(this.maxX, point.x);
this.maxY = Math.max(this.maxY, point.y);
return this;
}
/**
* Expands this bounding box by a fixed amount in each direction.
*
* @param amount - The amount to expand the box by, or contract if negative
* @returns This mutated bounding box
*/
expandBy(amount: number): this {
this.minX -= amount;
this.minY -= amount;
this.maxX += amount;
this.maxY += amount;
// check if bounds collapsed in either dimension
if (this.minX > this.maxX || this.minY > this.maxY) {
this.minX = Infinity;
this.maxX = -Infinity;
this.minY = Infinity;
this.maxY = -Infinity;
}
return this;
}
/**
* Shrinks this bounding box by a fixed amount in each direction.
*
* @param amount - The amount to shrink the box by
* @returns This mutated bounding box
*/
shrinkBy(amount: number): this {
return this.expandBy(-amount);
}
/**
* Returns a new bounding box that contains all of the corners of this bounding
* box with a transform applied. Does not modify this bounding box.
*
* @param fn - The function to apply to each corner
* @returns A new bounding box containing all of the mapped points.
*/
map(fn: (point: Point2D) => Point2D) {
const result = new Bounds();
result.extend(fn(new Point(this.minX, this.minY)));
result.extend(fn(new Point(this.maxX, this.minY)));
result.extend(fn(new Point(this.minX, this.maxY)));
result.extend(fn(new Point(this.maxX, this.maxY)));
return result;
}
/**
* Creates a new bounding box that includes all points provided.
*
* @param points - The points to include inside the bounding box
* @returns The new bounding box
*/
static fromPoints(points: Point2D[]): Bounds {
const result = new Bounds();
for (const p of points) {
result.extend(p);
}
return result;
}
contains(point: Point2D): boolean {
return point.x >= this.minX && point.x <= this.maxX && point.y >= this.minY && point.y <= this.maxY;
}
empty(): boolean {
return this.minX > this.maxX;
}
width(): number {
return this.maxX - this.minX;
}
height(): number {
return this.maxY - this.minY;
}
covers(other: ReadOnlyBounds) {
return !this.empty() && !other.empty() &&
other.minX >= this.minX &&
other.maxX <= this.maxX &&
other.minY >= this.minY &&
other.maxY <= this.maxY;
}
intersects(other: ReadOnlyBounds) {
return !this.empty() && !other.empty() &&
other.minX <= this.maxX &&
other.maxX >= this.minX &&
other.minY <= this.maxY &&
other.maxY >= this.minY;
}
}

83
node_modules/maplibre-gl/src/geo/edge_insets.test.ts generated vendored Normal file
View File

@@ -0,0 +1,83 @@
import {describe, test, expect} from 'vitest';
import {EdgeInsets} from '../geo/edge_insets';
describe('EdgeInsets', () => {
describe('constructor', () => {
test('creates an object with default values', () => {
expect(new EdgeInsets() instanceof EdgeInsets).toBeTruthy();
});
test('invalid initialization', () => {
expect(() => {
new EdgeInsets(NaN, 10);
}).toThrow('Invalid value for edge-insets, top, bottom, left and right must all be numbers');
expect(() => {
new EdgeInsets(-10, 10, 20, 10);
}).toThrow('Invalid value for edge-insets, top, bottom, left and right must all be numbers');
});
test('valid initialization', () => {
const top = 10;
const bottom = 15;
const left = 26;
const right = 19;
const inset = new EdgeInsets(top, bottom, left, right);
expect(inset.top).toBe(top);
expect(inset.bottom).toBe(bottom);
expect(inset.left).toBe(left);
expect(inset.right).toBe(right);
});
});
describe('getCenter', () => {
test('valid input', () => {
const inset = new EdgeInsets(10, 15, 50, 10);
const center = inset.getCenter(600, 400);
expect(center.x).toBe(320);
expect(center.y).toBe(197.5);
});
test('center clamping', () => {
const inset = new EdgeInsets(300, 200, 500, 200);
const center = inset.getCenter(600, 400);
// Midpoint of the overlap when padding overlaps
expect(center.x).toBe(450);
expect(center.y).toBe(250);
});
});
describe('interpolate', () => {
test('it works', () => {
const inset1 = new EdgeInsets(10, 15, 50, 10);
const inset2 = new EdgeInsets(20, 30, 100, 10);
const inset3 = inset1.interpolate(inset1, inset2, 0.5);
// inset1 is mutated in-place
expect(inset3).toBe(inset1);
expect(inset3.top).toBe(15);
expect(inset3.bottom).toBe(22.5);
expect(inset3.left).toBe(75);
expect(inset3.right).toBe(10);
});
});
test('equals', () => {
const inset1 = new EdgeInsets(10, 15, 50, 10);
const inset2 = new EdgeInsets(10, 15, 50, 10);
const inset3 = new EdgeInsets(10, 15, 50, 11);
expect(inset1.equals(inset2)).toBeTruthy();
expect(inset2.equals(inset3)).toBeFalsy();
});
test('clone', () => {
const inset1 = new EdgeInsets(10, 15, 50, 10);
const inset2 = inset1.clone();
expect(inset2 === inset1).toBeFalsy();
expect(inset1.equals(inset2)).toBeTruthy();
});
});

146
node_modules/maplibre-gl/src/geo/edge_insets.ts generated vendored Normal file
View File

@@ -0,0 +1,146 @@
import {interpolates} from '@maplibre/maplibre-gl-style-spec';
import Point from '@mapbox/point-geometry';
import {clamp, type Complete, type RequireAtLeastOne} from '../util/util';
/**
* An `EdgeInset` object represents screen space padding applied to the edges of the viewport.
* This shifts the apparent center or the vanishing point of the map. This is useful for adding floating UI elements
* on top of the map and having the vanishing point shift as UI elements resize.
*
* @group Geography and Geometry
*/
export class EdgeInsets {
/**
* @defaultValue 0
*/
top: number;
/**
* @defaultValue 0
*/
bottom: number;
/**
* @defaultValue 0
*/
left: number;
/**
* @defaultValue 0
*/
right: number;
constructor(top: number = 0, bottom: number = 0, left: number = 0, right: number = 0) {
if (isNaN(top) || top < 0 ||
isNaN(bottom) || bottom < 0 ||
isNaN(left) || left < 0 ||
isNaN(right) || right < 0
) {
throw new Error('Invalid value for edge-insets, top, bottom, left and right must all be numbers');
}
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
}
/**
* Interpolates the inset in-place.
* This maintains the current inset value for any inset not present in `target`.
* @param start - interpolation start
* @param target - interpolation target
* @param t - interpolation step/weight
* @returns the insets
*/
interpolate(start: PaddingOptions | EdgeInsets, target: PaddingOptions, t: number): EdgeInsets {
if (target.top != null && start.top != null) this.top = interpolates.number(start.top, target.top, t);
if (target.bottom != null && start.bottom != null) this.bottom = interpolates.number(start.bottom, target.bottom, t);
if (target.left != null && start.left != null) this.left = interpolates.number(start.left, target.left, t);
if (target.right != null && start.right != null) this.right = interpolates.number(start.right, target.right, t);
return this;
}
/**
* Utility method that computes the new apparent center or vanishing point after applying insets.
* This is in pixels and with the top left being (0.0) and +y being downwards.
*
* @param width - the width
* @param height - the height
* @returns the point
*/
getCenter(width: number, height: number): Point {
// Clamp insets so they never overflow width/height and always calculate a valid center
const x = clamp((this.left + width - this.right) / 2, 0, width);
const y = clamp((this.top + height - this.bottom) / 2, 0, height);
return new Point(x, y);
}
equals(other: PaddingOptions): boolean {
return this.top === other.top &&
this.bottom === other.bottom &&
this.left === other.left &&
this.right === other.right;
}
clone(): EdgeInsets {
return new EdgeInsets(this.top, this.bottom, this.left, this.right);
}
/**
* Returns the current state as json, useful when you want to have a
* read-only representation of the inset.
*
* @returns state as json
*/
toJSON(): Complete<PaddingOptions> {
return {
top: this.top,
bottom: this.bottom,
left: this.left,
right: this.right
};
}
}
/**
* Options for setting padding on calls to methods such as {@link Map.fitBounds}, {@link Map.fitScreenCoordinates}, and {@link Map.setPadding}. Adjust these options to set the amount of padding in pixels added to the edges of the canvas. Set a uniform padding on all edges or individual values for each edge. All properties of this object must be
* non-negative integers.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let bbox = [[-79, 43], [-73, 45]];
* map.fitBounds(bbox, {
* padding: {top: 10, bottom:25, left: 15, right: 5}
* });
* ```
*
* @example
* ```ts
* let bbox = [[-79, 43], [-73, 45]];
* map.fitBounds(bbox, {
* padding: 20
* });
* ```
* @see [Fit to the bounds of a LineString](https://maplibre.org/maplibre-gl-js/docs/examples/zoomto-linestring/)
* @see [Fit a map to a bounding box](https://maplibre.org/maplibre-gl-js/docs/examples/fitbounds/)
*/
export type PaddingOptions = RequireAtLeastOne<{
/**
* Padding in pixels from the top of the map canvas.
*/
top: number;
/**
* Padding in pixels from the bottom of the map canvas.
*/
bottom: number;
/**
* Padding in pixels from the left of the map canvas.
*/
right: number;
/**
* Padding in pixels from the right of the map canvas.
*/
left: number;
}>;

64
node_modules/maplibre-gl/src/geo/lng_lat.test.ts generated vendored Normal file
View File

@@ -0,0 +1,64 @@
import {describe, test, expect} from 'vitest';
import {LngLat} from '../geo/lng_lat';
describe('LngLat', () => {
test('constructor', () => {
expect(new LngLat(0, 0) instanceof LngLat).toBeTruthy();
expect(() => {
new LngLat(0, -91);
}).toThrow('Invalid LngLat latitude value: must be between -90 and 90');
expect(() => {
new LngLat(0, 91);
}).toThrow('Invalid LngLat latitude value: must be between -90 and 90');
});
test('convert', () => {
expect(LngLat.convert([0, 10]) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({lng: 0, lat: 10}) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({lng: 0, lat: 0}) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({lon: 0, lat: 10}) instanceof LngLat).toBeTruthy();
expect(LngLat.convert({lon: 0, lat: 0}) instanceof LngLat).toBeTruthy();
expect(LngLat.convert(new LngLat(0, 0)) instanceof LngLat).toBeTruthy();
});
test('wrap', () => {
expect(new LngLat(0, 0).wrap()).toEqual({lng: 0, lat: 0});
expect(new LngLat(10, 20).wrap()).toEqual({lng: 10, lat: 20});
expect(new LngLat(360, 0).wrap()).toEqual({lng: 0, lat: 0});
expect(new LngLat(190, 0).wrap()).toEqual({lng: -170, lat: 0});
});
test('toArray', () => {
expect(new LngLat(10, 20).toArray()).toEqual([10, 20]);
});
test('toString', () => {
expect(new LngLat(10, 20).toString()).toBe('LngLat(10, 20)');
});
test('distanceTo', () => {
const newYork = new LngLat(-74.0060, 40.7128);
const losAngeles = new LngLat(-118.2437, 34.0522);
const d = newYork.distanceTo(losAngeles); // 3935751.690893987, "true distance" is 3966km
expect(d > 3935750).toBeTruthy();
expect(d < 3935752).toBeTruthy();
});
test('distanceTo to pole', () => {
const newYork = new LngLat(-74.0060, 40.7128);
const northPole = new LngLat(-135, 90);
const d = newYork.distanceTo(northPole); // 5480494.158486183 , "true distance" is 5499km
expect(d > 5480493).toBeTruthy();
expect(d < 5480495).toBeTruthy();
});
test('distanceTo to Null Island', () => {
const newYork = new LngLat(-74.0060, 40.7128);
const nullIsland = new LngLat(0, 0);
const d = newYork.distanceTo(nullIsland); // 8667080.125666846 , "true distance" is 8661km
expect(d > 8667079).toBeTruthy();
expect(d < 8667081).toBeTruthy();
});
});

176
node_modules/maplibre-gl/src/geo/lng_lat.ts generated vendored Normal file
View File

@@ -0,0 +1,176 @@
import {wrap} from '../util/util';
/*
* Approximate radius of the earth in meters.
* Uses the WGS-84 approximation. The radius at the equator is ~6378137 and at the poles is ~6356752. https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84
* 6371008.8 is one published "average radius" see https://en.wikipedia.org/wiki/Earth_radius#Mean_radius, or ftp://athena.fsv.cvut.cz/ZFG/grs80-Moritz.pdf p.4
*/
export const earthRadius = 6371008.8;
/**
* A {@link LngLat} object, an array of two numbers representing longitude and latitude,
* or an object with `lng` and `lat` or `lon` and `lat` properties.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let v1 = new LngLat(-122.420679, 37.772537);
* let v2 = [-122.420679, 37.772537];
* let v3 = {lon: -122.420679, lat: 37.772537};
* ```
*/
export type LngLatLike = LngLat | {
lng: number;
lat: number;
} | {
lon: number;
lat: number;
} | [number, number];
/**
* A `LngLat` object represents a given longitude and latitude coordinate, measured in degrees.
* These coordinates are based on the [WGS84 (EPSG:4326) standard](https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84).
*
* MapLibre GL JS uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match the
* [GeoJSON specification](https://tools.ietf.org/html/rfc7946).
*
* Note that any MapLibre GL JS method that accepts a `LngLat` object as an argument or option
* can also accept an `Array` of two numbers and will perform an implicit conversion.
* This flexible type is documented as {@link LngLatLike}.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let ll = new LngLat(-123.9749, 40.7736);
* ll.lng; // = -123.9749
* ```
* @see [Get coordinates of the mouse pointer](https://maplibre.org/maplibre-gl-js/docs/examples/mouse-position/)
* @see [Display a popup](https://maplibre.org/maplibre-gl-js/docs/examples/popup/)
* @see [Create a timeline animation](https://maplibre.org/maplibre-gl-js/docs/examples/timeline-animation/)
*/
export class LngLat {
/**
* Longitude, measured in degrees.
*/
lng: number;
/**
* Latitude, measured in degrees.
*/
lat: number;
/**
* @param lng - Longitude, measured in degrees.
* @param lat - Latitude, measured in degrees.
*/
constructor(lng: number, lat: number) {
if (isNaN(lng) || isNaN(lat)) {
throw new Error(`Invalid LngLat object: (${lng}, ${lat})`);
}
this.lng = +lng;
this.lat = +lat;
if (this.lat > 90 || this.lat < -90) {
throw new Error('Invalid LngLat latitude value: must be between -90 and 90');
}
}
/**
* Returns a new `LngLat` object whose longitude is wrapped to the range (-180, 180).
*
* @returns The wrapped `LngLat` object.
* @example
* ```ts
* let ll = new LngLat(286.0251, 40.7736);
* let wrapped = ll.wrap();
* wrapped.lng; // = -73.9749
* ```
*/
wrap() {
return new LngLat(wrap(this.lng, -180, 180), this.lat);
}
/**
* Returns the coordinates represented as an array of two numbers.
*
* @returns The coordinates represented as an array of longitude and latitude.
* @example
* ```ts
* let ll = new LngLat(-73.9749, 40.7736);
* ll.toArray(); // = [-73.9749, 40.7736]
* ```
*/
toArray(): [number, number] {
return [this.lng, this.lat];
}
/**
* Returns the coordinates represent as a string.
*
* @returns The coordinates represented as a string of the format `'LngLat(lng, lat)'`.
* @example
* ```ts
* let ll = new LngLat(-73.9749, 40.7736);
* ll.toString(); // = "LngLat(-73.9749, 40.7736)"
* ```
*/
toString(): string {
return `LngLat(${this.lng}, ${this.lat})`;
}
/**
* Returns the approximate distance between a pair of coordinates in meters
* Uses the Haversine Formula (from R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope, vol. 68, no. 2, 1984, p. 159)
*
* @param lngLat - coordinates to compute the distance to
* @returns Distance in meters between the two coordinates.
* @example
* ```ts
* let new_york = new LngLat(-74.0060, 40.7128);
* let los_angeles = new LngLat(-118.2437, 34.0522);
* new_york.distanceTo(los_angeles); // = 3935751.690893987, "true distance" using a non-spherical approximation is ~3966km
* ```
*/
distanceTo(lngLat: LngLat): number {
const rad = Math.PI / 180;
const lat1 = this.lat * rad;
const lat2 = lngLat.lat * rad;
const a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((lngLat.lng - this.lng) * rad);
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
return maxMeters;
}
/**
* Converts an array of two numbers or an object with `lng` and `lat` or `lon` and `lat` properties
* to a `LngLat` object.
*
* If a `LngLat` object is passed in, the function returns it unchanged.
*
* @param input - An array of two numbers or object to convert, or a `LngLat` object to return.
* @returns A new `LngLat` object, if a conversion occurred, or the original `LngLat` object.
* @example
* ```ts
* let arr = [-73.9749, 40.7736];
* let ll = LngLat.convert(arr);
* ll; // = LngLat {lng: -73.9749, lat: 40.7736}
* ```
*/
static convert(input: LngLatLike): LngLat {
if (input instanceof LngLat) {
return input;
}
if (Array.isArray(input) && (input.length === 2 || input.length === 3)) {
return new LngLat(Number(input[0]), Number(input[1]));
}
if (!Array.isArray(input) && typeof input === 'object' && input !== null) {
return new LngLat(
// flow can't refine this to have one of lng or lat, so we have to cast to any
Number('lng' in input ? (input as any).lng : (input as any).lon),
Number(input.lat)
);
}
throw new Error('`LngLatLike` argument must be specified as a LngLat instance, an object {lng: <lng>, lat: <lat>}, an object {lon: <lng>, lat: <lat>}, or an array of [<lng>, <lat>]');
}
}

444
node_modules/maplibre-gl/src/geo/lng_lat_bounds.test.ts generated vendored Normal file
View File

@@ -0,0 +1,444 @@
import {describe, test, expect} from 'vitest';
import {LngLat} from './lng_lat';
import {LngLatBounds} from './lng_lat_bounds';
import {tileIdToLngLatBounds} from '../tile/tile_id_to_lng_lat_bounds';
import {CanonicalTileID} from '../tile/tile_id';
import {EXTENT} from '../data/extent';
describe('LngLatBounds', () => {
test('constructor', () => {
const sw = new LngLat(0, 0);
const ne = new LngLat(-10, 10);
const bounds = new LngLatBounds(sw, ne);
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(0);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(-10);
});
test('constructor across dateline', () => {
const sw = new LngLat(170, 0);
const ne = new LngLat(-170, 10);
const bounds = new LngLatBounds(sw, ne);
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(170);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(-170);
});
test('constructor across pole', () => {
const sw = new LngLat(0, 85);
const ne = new LngLat(-10, -85);
const bounds = new LngLatBounds(sw, ne);
expect(bounds.getSouth()).toBe(85);
expect(bounds.getWest()).toBe(0);
expect(bounds.getNorth()).toBe(-85);
expect(bounds.getEast()).toBe(-10);
});
test('constructor no args', () => {
const bounds = new LngLatBounds();
const t1 = () => {
bounds.getCenter();
};
expect(t1).toThrow();
});
test('extend with coordinate', () => {
const bounds = new LngLatBounds([0, 0], [10, 10]);
bounds.extend([-10, -10]);
expect(bounds.getSouth()).toBe(-10);
expect(bounds.getWest()).toBe(-10);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(10);
bounds.extend(new LngLat(-15, -15));
expect(bounds.getSouth()).toBe(-15);
expect(bounds.getWest()).toBe(-15);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(10);
bounds.extend([-80, -80, 80, 80]);
expect(bounds.getSouth()).toBe(-80);
expect(bounds.getWest()).toBe(-80);
expect(bounds.getNorth()).toBe(80);
expect(bounds.getEast()).toBe(80);
bounds.extend({lng: -90, lat: -90});
expect(bounds.getSouth()).toBe(-90);
expect(bounds.getWest()).toBe(-90);
expect(bounds.getNorth()).toBe(80);
expect(bounds.getEast()).toBe(80);
bounds.extend({lon: 90, lat: 90});
expect(bounds.getSouth()).toBe(-90);
expect(bounds.getWest()).toBe(-90);
expect(bounds.getNorth()).toBe(90);
expect(bounds.getEast()).toBe(90);
});
test('extend with bounds', () => {
const bounds1 = new LngLatBounds([0, 0], [10, 10]);
const bounds2 = new LngLatBounds([-10, -10], [10, 10]);
bounds1.extend(bounds2);
expect(bounds1.getSouth()).toBe(-10);
expect(bounds1.getWest()).toBe(-10);
expect(bounds1.getNorth()).toBe(10);
expect(bounds1.getEast()).toBe(10);
const bounds4 = new LngLatBounds([-20, -20, 20, 20]);
bounds1.extend(bounds4);
expect(bounds1.getSouth()).toBe(-20);
expect(bounds1.getWest()).toBe(-20);
expect(bounds1.getNorth()).toBe(20);
expect(bounds1.getEast()).toBe(20);
const bounds5 = new LngLatBounds();
bounds1.extend(bounds5);
expect(bounds1.getSouth()).toBe(-20);
expect(bounds1.getWest()).toBe(-20);
expect(bounds1.getNorth()).toBe(20);
expect(bounds1.getEast()).toBe(20);
});
test('extend with null', () => {
const bounds = new LngLatBounds([0, 0], [10, 10]);
bounds.extend(null);
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(0);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(10);
});
test('extend undefined bounding box', () => {
const bounds1 = new LngLatBounds(undefined, undefined);
const bounds2 = new LngLatBounds([-10, -10], [10, 10]);
bounds1.extend(bounds2);
expect(bounds1.getSouth()).toBe(-10);
expect(bounds1.getWest()).toBe(-10);
expect(bounds1.getNorth()).toBe(10);
expect(bounds1.getEast()).toBe(10);
});
test('extend same LngLat instance', () => {
const point = new LngLat(0, 0);
const bounds = new LngLatBounds(point, point);
bounds.extend(new LngLat(15, 15));
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(0);
expect(bounds.getNorth()).toBe(15);
expect(bounds.getEast()).toBe(15);
});
test('accessors', () => {
const sw = new LngLat(0, 0);
const ne = new LngLat(-10, -20);
const bounds = new LngLatBounds(sw, ne);
expect(bounds.getCenter()).toEqual(new LngLat(-5, -10));
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(0);
expect(bounds.getNorth()).toBe(-20);
expect(bounds.getEast()).toBe(-10);
expect(bounds.getSouthWest()).toEqual(new LngLat(0, 0));
expect(bounds.getSouthEast()).toEqual(new LngLat(-10, 0));
expect(bounds.getNorthEast()).toEqual(new LngLat(-10, -20));
expect(bounds.getNorthWest()).toEqual(new LngLat(0, -20));
});
test('convert', () => {
const sw = new LngLat(0, 0);
const ne = new LngLat(-10, 10);
const bounds = new LngLatBounds(sw, ne);
expect(LngLatBounds.convert(undefined)).toBeUndefined();
expect(LngLatBounds.convert(bounds)).toEqual(bounds);
expect(LngLatBounds.convert([sw, ne])).toEqual(bounds);
expect(
LngLatBounds.convert([bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()])
).toEqual(bounds);
});
test('toArray', () => {
const llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
expect(llb.toArray()).toEqual([[-73.9876, 40.7661], [-73.9397, 40.8002]]);
});
test('toString', () => {
const llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
expect(llb.toString()).toBe('LngLatBounds(LngLat(-73.9876, 40.7661), LngLat(-73.9397, 40.8002))');
});
test('isEmpty', () => {
const nullBounds = new LngLatBounds();
expect(nullBounds.isEmpty()).toBe(true);
const sw = new LngLat(0, 0);
const ne = new LngLat(-10, 10);
const bounds = new LngLatBounds(sw, ne);
expect(bounds.isEmpty()).toBe(false);
});
test('fromLngLat', () => {
const center0 = new LngLat(0, 0);
const center1 = new LngLat(-73.9749, 40.7736);
const center0Radius10 = LngLatBounds.fromLngLat(center0, 10);
const center1Radius10 = LngLatBounds.fromLngLat(center1, 10);
const center1Radius0 = LngLatBounds.fromLngLat(center1);
expect(center0Radius10.toArray()).toEqual(
[[-0.00008983152770714982, -0.00008983152770714982], [0.00008983152770714982, 0.00008983152770714982]]
);
expect(center1Radius10.toArray()).toEqual(
[[-73.97501862141328, 40.77351016847229], [-73.97478137858673, 40.77368983152771]]
);
expect(center1Radius0.toArray()).toEqual([[-73.9749, 40.7736], [-73.9749, 40.7736]]);
});
describe('LngLatBounds adjustAntiMeridian tests', () => {
test('kenya', () => {
const sw = new LngLat(32.958984, -5.353521);
const ne = new LngLat(43.50585, 5.615985);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-5.353521);
expect(bounds.getWest()).toBe(32.958984);
expect(bounds.getNorth()).toBe(5.615985);
expect(bounds.getEast()).toBe(43.50585);
});
test('normal cross (crossing antimeridian)', () => {
const sw = new LngLat(170, 0);
const ne = new LngLat(-170, 10);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(170);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(190);
});
test('exactly meridian (crossing antimeridian)', () => {
const sw = new LngLat(180, -20);
const ne = new LngLat(-180, 20);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-20);
expect(bounds.getWest()).toBe(180);
expect(bounds.getNorth()).toBe(20);
expect(bounds.getEast()).toBe(180);
});
test('small cross (crossing antimeridian)', () => {
const sw = new LngLat(179, -5);
const ne = new LngLat(-179, 5);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-5);
expect(bounds.getWest()).toBe(179);
expect(bounds.getNorth()).toBe(5);
expect(bounds.getEast()).toBe(181);
});
test('large cross (crossing antimeridian)', () => {
const sw = new LngLat(100, -30);
const ne = new LngLat(-100, 30);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-30);
expect(bounds.getWest()).toBe(100);
expect(bounds.getNorth()).toBe(30);
expect(bounds.getEast()).toBe(260);
});
test('reverse cross (crossing antimeridian)', () => {
const sw = new LngLat(-170, 0);
const ne = new LngLat(170, 10);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(-170);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(170);
});
test('reverse not cross', () => {
const sw = new LngLat(150, 0);
const ne = new LngLat(170, 10);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(150);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(170);
});
test('same longitude', () => {
const sw = new LngLat(175, -10);
const ne = new LngLat(175, 10);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-10);
expect(bounds.getWest()).toBe(175);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(175);
});
test('full world', () => {
const sw = new LngLat(-180, -90);
const ne = new LngLat(180, 90);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-90);
expect(bounds.getWest()).toBe(-180);
expect(bounds.getNorth()).toBe(90);
expect(bounds.getEast()).toBe(180);
});
test('across pole', () => {
const sw = new LngLat(0, 85);
const ne = new LngLat(-10, -85);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(85);
expect(bounds.getWest()).toBe(0);
expect(bounds.getNorth()).toBe(-85);
expect(bounds.getEast()).toBe(350);
});
test('across pole reverse', () => {
const sw = new LngLat(-10, -85);
const ne = new LngLat(0, 85);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(-85);
expect(bounds.getWest()).toBe(-10);
expect(bounds.getNorth()).toBe(85);
expect(bounds.getEast()).toBe(0);
});
test('across dateline', () => {
const sw = new LngLat(170, 0);
const ne = new LngLat(-170, 10);
const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
expect(bounds.getSouth()).toBe(0);
expect(bounds.getWest()).toBe(170);
expect(bounds.getNorth()).toBe(10);
expect(bounds.getEast()).toBe(190);
});
});
describe('contains', () => {
describe('point', () => {
test('point is in bounds', () => {
const llb = new LngLatBounds([-1, -1], [1, 1]);
const ll = {lng: 0, lat: 0};
expect(llb.contains(ll)).toBeTruthy();
});
test('point is not in bounds', () => {
const llb = new LngLatBounds([-1, -1], [1, 1]);
const ll = {lng: 3, lat: 3};
expect(llb.contains(ll)).toBeFalsy();
});
test('point is in bounds that spans dateline', () => {
const llb = new LngLatBounds([190, -10], [170, 10]);
const ll = {lng: 180, lat: 0};
expect(llb.contains(ll)).toBeTruthy();
});
test('point is not in bounds that spans dateline', () => {
const llb = new LngLatBounds([190, -10], [170, 10]);
const ll = {lng: 0, lat: 0};
expect(llb.contains(ll)).toBeFalsy();
});
});
});
describe('intersects', () => {
test('bounds intersect', () => {
const bounds1 = new LngLatBounds([0, 0], [10, 10]);
const bounds2 = new LngLatBounds([5, 5], [15, 15]);
expect(bounds1.intersects(bounds2)).toBe(true);
});
test('bounds do not intersect', () => {
const bounds1 = new LngLatBounds([0, 0], [10, 10]);
const bounds2 = new LngLatBounds([20, 20], [30, 30]);
expect(bounds1.intersects(bounds2)).toBe(false);
});
describe('dateline crossing', () => {
test('both bounds wrap around dateline - always intersect', () => {
const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
const bounds2 = new LngLatBounds([160, 5], [-160, 15]);
expect(bounds1.intersects(bounds2)).toBe(true);
});
test('only first bounds wraps - intersects on east side', () => {
const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
const bounds2 = new LngLatBounds([165, 0], [175, 10]);
expect(bounds1.intersects(bounds2)).toBe(true);
});
test('only first bounds wraps - intersects on west side', () => {
const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
const bounds2 = new LngLatBounds([-175, 0], [-165, 10]);
expect(bounds1.intersects(bounds2)).toBe(true);
});
test('only first bounds wraps - does not intersect (in gap)', () => {
const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
const bounds2 = new LngLatBounds([0, 0], [10, 10]);
expect(bounds1.intersects(bounds2)).toBe(false);
});
test('only second bounds wraps - intersects on east side', () => {
const bounds1 = new LngLatBounds([165, 0], [175, 10]);
const bounds2 = new LngLatBounds([170, 0], [-170, 10]);
expect(bounds1.intersects(bounds2)).toBe(true);
});
test('only second bounds wraps - intersects on west side', () => {
const bounds1 = new LngLatBounds([-175, 0], [-165, 10]);
const bounds2 = new LngLatBounds([170, 0], [-170, 10]);
expect(bounds1.intersects(bounds2)).toBe(true);
});
test('only second bounds wraps - does not intersect (in gap)', () => {
const bounds1 = new LngLatBounds([0, 0], [10, 10]);
const bounds2 = new LngLatBounds([170, 0], [-170, 10]);
expect(bounds1.intersects(bounds2)).toBe(false);
});
test('wrapping bounds with no latitude overlap', () => {
const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
const bounds2 = new LngLatBounds([160, 20], [-160, 30]);
expect(bounds1.intersects(bounds2)).toBe(false);
});
test('wrapping tile bounds at dateline intersects with negative longitude bounds', () => {
const tileBounds = new LngLatBounds([170, 0], [-170, 10]);
const bounds = new LngLatBounds([-180, 5], [-175, 10]);
expect(tileBounds.intersects(bounds)).toBe(true);
});
test('entire worlds tile should return true', () => {
const tileBounds = tileIdToLngLatBounds(new CanonicalTileID(0, 0, 0), 2048 / EXTENT);
const bounds = new LngLatBounds([[-8.290589217651302, 44.47966524518165], [20.566067150212803, 50.98693819014929]]);
expect(tileBounds.intersects(bounds)).toBe(true);
});
test('point feature outside bounds does not intersect', () => {
const bounds = new LngLatBounds([0, 0], [10, 10]);
const point = new LngLatBounds([20, 5], [20, 5]);
expect(bounds.intersects(point)).toBe(false);
});
});
});
});

414
node_modules/maplibre-gl/src/geo/lng_lat_bounds.ts generated vendored Normal file
View File

@@ -0,0 +1,414 @@
import {LngLat} from './lng_lat';
import type {LngLatLike} from './lng_lat';
import {wrap} from '../util/util';
/**
* A {@link LngLatBounds} object, an array of {@link LngLatLike} objects in [sw, ne] order,
* or an array of numbers in [west, south, east, north] order.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let v1 = new LngLatBounds(
* new LngLat(-73.9876, 40.7661),
* new LngLat(-73.9397, 40.8002)
* );
* let v2 = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002])
* let v3 = [[-73.9876, 40.7661], [-73.9397, 40.8002]];
* ```
*/
export type LngLatBoundsLike = LngLatBounds | [LngLatLike, LngLatLike] | [number, number, number, number];
/**
* A `LngLatBounds` object represents a geographical bounding box,
* defined by its southwest and northeast points in longitude and latitude.
*
* If no arguments are provided to the constructor, a `null` bounding box is created.
*
* Note that any Mapbox GL method that accepts a `LngLatBounds` object as an argument or option
* can also accept an `Array` of two {@link LngLatLike} constructs and will perform an implicit conversion.
* This flexible type is documented as {@link LngLatBoundsLike}.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let sw = new LngLat(-73.9876, 40.7661);
* let ne = new LngLat(-73.9397, 40.8002);
* let llb = new LngLatBounds(sw, ne);
* ```
*/
export class LngLatBounds {
_ne: LngLat;
_sw: LngLat;
/**
* @param sw - The southwest corner of the bounding box.
* OR array of 4 numbers in the order of west, south, east, north
* OR array of 2 LngLatLike: [sw,ne]
* @param ne - The northeast corner of the bounding box.
* @example
* ```ts
* let sw = new LngLat(-73.9876, 40.7661);
* let ne = new LngLat(-73.9397, 40.8002);
* let llb = new LngLatBounds(sw, ne);
* ```
* OR
* ```ts
* let llb = new LngLatBounds([-73.9876, 40.7661, -73.9397, 40.8002]);
* ```
* OR
* ```ts
* let llb = new LngLatBounds([sw, ne]);
* ```
*/
constructor(sw?: LngLatLike | [number, number, number, number] | [LngLatLike, LngLatLike], ne?: LngLatLike) {
if (!sw) {
// noop
} else if (ne) {
this.setSouthWest(<LngLatLike>sw).setNorthEast(ne);
} else if (Array.isArray(sw)) {
if (sw.length === 4) {
// 4 element array: west, south, east, north
this.setSouthWest([sw[0], sw[1]]).setNorthEast([sw[2], sw[3]]);
} else {
this.setSouthWest(sw[0] as LngLatLike).setNorthEast(sw[1] as LngLatLike);
}
}
}
/**
* Set the northeast corner of the bounding box
*
* @param ne - a {@link LngLatLike} object describing the northeast corner of the bounding box.
*/
setNorthEast(ne: LngLatLike): this {
this._ne = ne instanceof LngLat ? new LngLat(ne.lng, ne.lat) : LngLat.convert(ne);
return this;
}
/**
* Set the southwest corner of the bounding box
*
* @param sw - a {@link LngLatLike} object describing the southwest corner of the bounding box.
*/
setSouthWest(sw: LngLatLike): this {
this._sw = sw instanceof LngLat ? new LngLat(sw.lng, sw.lat) : LngLat.convert(sw);
return this;
}
/**
* Extend the bounds to include a given LngLatLike or LngLatBoundsLike.
*
* @param obj - object to extend to
*/
extend(obj: LngLatLike | LngLatBoundsLike): this {
const sw = this._sw,
ne = this._ne;
let sw2, ne2;
if (obj instanceof LngLat) {
sw2 = obj;
ne2 = obj;
} else if (obj instanceof LngLatBounds) {
sw2 = obj._sw;
ne2 = obj._ne;
if (!sw2 || !ne2) return this;
} else {
if (Array.isArray(obj)) {
if (obj.length === 4 || (obj as any[]).every(Array.isArray)) {
const lngLatBoundsObj = (obj as any as LngLatBoundsLike);
return this.extend(LngLatBounds.convert(lngLatBoundsObj));
} else {
const lngLatObj = (obj as any as LngLatLike);
return this.extend(LngLat.convert(lngLatObj));
}
} else if (obj && ('lng' in obj || 'lon' in obj) && 'lat' in obj) {
return this.extend(LngLat.convert(obj));
}
return this;
}
if (!sw && !ne) {
this._sw = new LngLat(sw2.lng, sw2.lat);
this._ne = new LngLat(ne2.lng, ne2.lat);
} else {
sw.lng = Math.min(sw2.lng, sw.lng);
sw.lat = Math.min(sw2.lat, sw.lat);
ne.lng = Math.max(ne2.lng, ne.lng);
ne.lat = Math.max(ne2.lat, ne.lat);
}
return this;
}
/**
* Returns the geographical coordinate equidistant from the bounding box's corners.
*
* @returns The bounding box's center.
* @example
* ```ts
* let llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
* llb.getCenter(); // = LngLat {lng: -73.96365, lat: 40.78315}
* ```
*/
getCenter(): LngLat {
return new LngLat((this._sw.lng + this._ne.lng) / 2, (this._sw.lat + this._ne.lat) / 2);
}
/**
* Returns the southwest corner of the bounding box.
*
* @returns The southwest corner of the bounding box.
*/
getSouthWest(): LngLat { return this._sw; }
/**
* Returns the northeast corner of the bounding box.
*
* @returns The northeast corner of the bounding box.
*/
getNorthEast(): LngLat { return this._ne; }
/**
* Returns the northwest corner of the bounding box.
*
* @returns The northwest corner of the bounding box.
*/
getNorthWest(): LngLat { return new LngLat(this.getWest(), this.getNorth()); }
/**
* Returns the southeast corner of the bounding box.
*
* @returns The southeast corner of the bounding box.
*/
getSouthEast(): LngLat { return new LngLat(this.getEast(), this.getSouth()); }
/**
* Returns the west edge of the bounding box.
*
* @returns The west edge of the bounding box.
*/
getWest(): number { return this._sw.lng; }
/**
* Returns the south edge of the bounding box.
*
* @returns The south edge of the bounding box.
*/
getSouth(): number { return this._sw.lat; }
/**
* Returns the east edge of the bounding box.
*
* @returns The east edge of the bounding box.
*/
getEast(): number { return this._ne.lng; }
/**
* Returns the north edge of the bounding box.
*
* @returns The north edge of the bounding box.
*/
getNorth(): number { return this._ne.lat; }
/**
* Returns the bounding box represented as an array.
*
* @returns The bounding box represented as an array, consisting of the
* southwest and northeast coordinates of the bounding represented as arrays of numbers.
* @example
* ```ts
* let llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
* llb.toArray(); // = [[-73.9876, 40.7661], [-73.9397, 40.8002]]
* ```
*/
toArray(): [[number, number], [number, number]] {
return [this._sw.toArray(), this._ne.toArray()];
}
/**
* Return the bounding box represented as a string.
*
* @returns The bounding box represents as a string of the format
* `'LngLatBounds(LngLat(lng, lat), LngLat(lng, lat))'`.
* @example
* ```ts
* let llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
* llb.toString(); // = "LngLatBounds(LngLat(-73.9876, 40.7661), LngLat(-73.9397, 40.8002))"
* ```
*/
toString() {
return `LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`;
}
/**
* Check if the bounding box is an empty/`null`-type box.
*
* @returns True if bounds have been defined, otherwise false.
*/
isEmpty() {
return !(this._sw && this._ne);
}
/**
* Check if the point is within the bounding box.
*
* @param lnglat - geographic point to check against.
* @returns `true` if the point is within the bounding box.
* @example
* ```ts
* let llb = new LngLatBounds(
* new LngLat(-73.9876, 40.7661),
* new LngLat(-73.9397, 40.8002)
* );
*
* let ll = new LngLat(-73.9567, 40.7789);
*
* console.log(llb.contains(ll)); // = true
* ```
*/
contains(lnglat: LngLatLike) {
const {lng, lat} = LngLat.convert(lnglat);
const containsLatitude = this._sw.lat <= lat && lat <= this._ne.lat;
let containsLongitude = this._sw.lng <= lng && lng <= this._ne.lng;
if (this._sw.lng > this._ne.lng) { // wrapped coordinates
containsLongitude = this._sw.lng >= lng && lng >= this._ne.lng;
}
return containsLatitude && containsLongitude;
}
/**
* Checks if this bounding box intersects with another bounding box.
*
* Returns true if the bounding boxes share any area, including cases where
* they only touch along an edge or at a corner.
*
* This method properly handles cases where either or both bounding boxes cross
* the antimeridian (date line).
*/
intersects(other: LngLatBoundsLike): boolean {
other = LngLatBounds.convert(other);
const latIntersects =
other.getNorth() >= this.getSouth() &&
other.getSouth() <= this.getNorth();
if (!latIntersects) return false;
// Check if either bound covers the full world (|span| >= 360°)
// This must be done before wrapping to preserve the span information
const thisSpan = Math.abs(this.getEast() - this.getWest());
const otherSpan = Math.abs(other.getEast() - other.getWest());
if (thisSpan >= 360 || otherSpan >= 360) {
return true;
}
// Normalize longitudes to [-180, 180] range
const thisWest = wrap(this.getWest(), -180, 180);
const thisEast = wrap(this.getEast(), -180, 180);
const otherWest = wrap(other.getWest(), -180, 180);
const otherEast = wrap(other.getEast(), -180, 180);
// Check if either bounds wraps around the antimeridian
// Use strict inequality: equal values indicate zero-width bounds (e.g., a point), not wrapping
const thisWraps = thisWest > thisEast;
const otherWraps = otherWest > otherEast;
// Both wrap: they always intersect
if (thisWraps && otherWraps) {
return true;
}
// Only this wraps: intersects if other is outside the gap
if (thisWraps) {
return otherEast >= thisWest || otherWest <= thisEast;
}
if (otherWraps) {
// Only other wraps: intersects if this is outside the gap
return thisEast >= otherWest || thisWest <= otherEast;
}
// Neither wraps: standard intersection check
return otherWest <= thisEast && otherEast >= thisWest;
}
/**
* Converts an array to a `LngLatBounds` object.
*
* If a `LngLatBounds` object is passed in, the function returns it unchanged.
*
* Internally, the function calls {@link LngLat.convert} to convert arrays to `LngLat` values.
*
* @param input - An array of two coordinates to convert, or a `LngLatBounds` object to return.
* @returns A new `LngLatBounds` object, if a conversion occurred, or the original `LngLatBounds` object.
* @example
* ```ts
* let arr = [[-73.9876, 40.7661], [-73.9397, 40.8002]];
* let llb = LngLatBounds.convert(arr); // = LngLatBounds {_sw: LngLat {lng: -73.9876, lat: 40.7661}, _ne: LngLat {lng: -73.9397, lat: 40.8002}}
* ```
*/
static convert(input: LngLatBoundsLike | null): LngLatBounds {
if (input instanceof LngLatBounds) return input;
if (!input) return input as null;
return new LngLatBounds(input);
}
/**
* Returns a `LngLatBounds` from the coordinates extended by a given `radius`. The returned `LngLatBounds` completely contains the `radius`.
*
* @param center - center coordinates of the new bounds.
* @param radius - Distance in meters from the coordinates to extend the bounds.
* @returns A new `LngLatBounds` object representing the coordinates extended by the `radius`.
* @example
* ```ts
* let center = new LngLat(-73.9749, 40.7736);
* LngLatBounds.fromLngLat(100).toArray(); // = [[-73.97501862141328, 40.77351016847229], [-73.97478137858673, 40.77368983152771]]
* ```
*/
static fromLngLat(center: LngLat, radius:number = 0): LngLatBounds {
const earthCircumferenceInMetersAtEquator = 40075017;
const latAccuracy = 360 * radius / earthCircumferenceInMetersAtEquator,
lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * center.lat);
return new LngLatBounds(new LngLat(center.lng - lngAccuracy, center.lat - latAccuracy),
new LngLat(center.lng + lngAccuracy, center.lat + latAccuracy));
}
/**
* Adjusts the given bounds to handle the case where the bounds cross the 180th meridian (antimeridian).
*
* @returns The adjusted LngLatBounds
* @example
* ```ts
* let bounds = new LngLatBounds([175.813127, -20.157768], [-178. 340903, -15.449124]);
* let adjustedBounds = bounds.adjustAntiMeridian();
* // adjustedBounds will be: [[175.813127, -20.157768], [181.659097, -15.449124]]
* ```
*/
adjustAntiMeridian(): LngLatBounds {
const sw = new LngLat(this._sw.lng, this._sw.lat);
const ne = new LngLat(this._ne.lng, this._ne.lat);
if (sw.lng > ne.lng) {
return new LngLatBounds(
sw,
new LngLat(ne.lng + 360, ne.lat)
);
}
return new LngLatBounds(sw, ne);
}
}

View File

@@ -0,0 +1,35 @@
import {describe, test, expect} from 'vitest';
import {LngLat} from './lng_lat';
import {MercatorCoordinate, mercatorScale} from './mercator_coordinate';
describe('LngLat', () => {
test('constructor', () => {
expect(new MercatorCoordinate(0, 0) instanceof MercatorCoordinate).toBeTruthy();
expect(new MercatorCoordinate(0, 0, 0) instanceof MercatorCoordinate).toBeTruthy();
});
test('fromLngLat', () => {
const nullIsland = new LngLat(0, 0);
expect(MercatorCoordinate.fromLngLat(nullIsland)).toEqual({x: 0.5, y: 0.5, z: 0});
});
test('toLngLat', () => {
const dc = new LngLat(-77, 39);
expect(MercatorCoordinate.fromLngLat(dc, 500).toLngLat()).toEqual({lng: -77, lat: 39});
});
test('toAltitude', () => {
const dc = new LngLat(-77, 39);
expect(MercatorCoordinate.fromLngLat(dc, 500).toAltitude()).toBe(500);
});
test('mercatorScale', () => {
expect(mercatorScale(0)).toBe(1);
expect(mercatorScale(45)).toBe(1.414213562373095);
});
test('meterInMercatorCoordinateUnits', () => {
const nullIsland = new LngLat(0, 0);
expect(MercatorCoordinate.fromLngLat(nullIsland).meterInMercatorCoordinateUnits()).toBe(2.4981121214570498e-8);
});
});

157
node_modules/maplibre-gl/src/geo/mercator_coordinate.ts generated vendored Normal file
View File

@@ -0,0 +1,157 @@
import {LngLat, earthRadius} from '../geo/lng_lat';
import type {LngLatLike} from '../geo/lng_lat';
import {type IMercatorCoordinate} from '@maplibre/maplibre-gl-style-spec';
/*
* The average circumference of the world in meters.
*/
const earthCircumference = 2 * Math.PI * earthRadius; // meters
/*
* The circumference at a line of latitude in meters.
*/
function circumferenceAtLatitude(latitude: number) {
return earthCircumference * Math.cos(latitude * Math.PI / 180);
}
export function mercatorXfromLng(lng: number) {
return (180 + lng) / 360;
}
export function mercatorYfromLat(lat: number) {
return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
}
export function mercatorZfromAltitude(altitude: number, lat: number) {
return altitude / circumferenceAtLatitude(lat);
}
export function lngFromMercatorX(x: number) {
return x * 360 - 180;
}
export function latFromMercatorY(y: number) {
const y2 = 180 - y * 360;
return 360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90;
}
export function altitudeFromMercatorZ(z: number, y: number) {
return z * circumferenceAtLatitude(latFromMercatorY(y));
}
/**
* Determine the Mercator scale factor for a given latitude, see
* https://en.wikipedia.org/wiki/Mercator_projection#Scale_factor
*
* At the equator the scale factor will be 1, which increases at higher latitudes.
*
* @param lat - Latitude
* @returns scale factor
*/
export function mercatorScale(lat: number) {
return 1 / Math.cos(lat * Math.PI / 180);
}
/**
* A `MercatorCoordinate` object represents a projected three dimensional position.
*
* `MercatorCoordinate` uses the web mercator projection ([EPSG:3857](https://epsg.io/3857)) with slightly different units:
*
* - the size of 1 unit is the width of the projected world instead of the "mercator meter"
* - the origin of the coordinate space is at the north-west corner instead of the middle
*
* For example, `MercatorCoordinate(0, 0, 0)` is the north-west corner of the mercator world and
* `MercatorCoordinate(1, 1, 0)` is the south-east corner. If you are familiar with
* [vector tiles](https://github.com/mapbox/vector-tile-spec) it may be helpful to think
* of the coordinate space as the `0/0/0` tile with an extent of `1`.
*
* The `z` dimension of `MercatorCoordinate` is conformal. A cube in the mercator coordinate space would be rendered as a cube.
*
* @group Geography and Geometry
*
* @example
* ```ts
* let nullIsland = new MercatorCoordinate(0.5, 0.5, 0);
* ```
* @see [Add a custom style layer](https://maplibre.org/maplibre-gl-js/docs/examples/custom-style-layer/)
*/
export class MercatorCoordinate implements IMercatorCoordinate {
x: number;
y: number;
z: number;
/**
* @param x - The x component of the position.
* @param y - The y component of the position.
* @param z - The z component of the position.
*/
constructor(x: number, y: number, z: number = 0) {
this.x = +x;
this.y = +y;
this.z = +z;
}
/**
* Project a `LngLat` to a `MercatorCoordinate`.
*
* @param lngLatLike - The location to project.
* @param altitude - The altitude in meters of the position.
* @returns The projected mercator coordinate.
* @example
* ```ts
* let coord = MercatorCoordinate.fromLngLat({ lng: 0, lat: 0}, 0);
* coord; // MercatorCoordinate(0.5, 0.5, 0)
* ```
*/
static fromLngLat(lngLatLike: LngLatLike, altitude: number = 0): MercatorCoordinate {
const lngLat = LngLat.convert(lngLatLike);
return new MercatorCoordinate(
mercatorXfromLng(lngLat.lng),
mercatorYfromLat(lngLat.lat),
mercatorZfromAltitude(altitude, lngLat.lat));
}
/**
* Returns the `LngLat` for the coordinate.
*
* @returns The `LngLat` object.
* @example
* ```ts
* let coord = new MercatorCoordinate(0.5, 0.5, 0);
* let lngLat = coord.toLngLat(); // LngLat(0, 0)
* ```
*/
toLngLat() {
return new LngLat(
lngFromMercatorX(this.x),
latFromMercatorY(this.y));
}
/**
* Returns the altitude in meters of the coordinate.
*
* @returns The altitude in meters.
* @example
* ```ts
* let coord = new MercatorCoordinate(0, 0, 0.02);
* coord.toAltitude(); // 6914.281956295339
* ```
*/
toAltitude(): number {
return altitudeFromMercatorZ(this.z, this.y);
}
/**
* Returns the distance of 1 meter in `MercatorCoordinate` units at this latitude.
*
* For coordinates in real world units using meters, this naturally provides the scale
* to transform into `MercatorCoordinate`s.
*
* @returns Distance of 1 meter in `MercatorCoordinate` units.
*/
meterInMercatorCoordinateUnits(): number {
// 1 meter / circumference at equator in meters * Mercator projection scale factor at this latitude
return 1 / earthCircumference * mercatorScale(latFromMercatorY(this.y));
}
}

View File

@@ -0,0 +1,221 @@
import Point from '@mapbox/point-geometry';
import {type IReadonlyTransform, type ITransform} from '../transform_interface';
import {type LngLat, type LngLatLike} from '../lng_lat';
import {type CameraForBoundsOptions, type PointLike} from '../../ui/camera';
import {type PaddingOptions} from '../edge_insets';
import {type LngLatBounds} from '../lng_lat_bounds';
import {degreesToRadians, getRollPitchBearing, type RollPitchBearing, rollPitchBearingToQuat, scaleZoom, warnOnce, zoomScale} from '../../util/util';
import {quat} from 'gl-matrix';
import {interpolates} from '@maplibre/maplibre-gl-style-spec';
import {projectToWorldCoordinates, unprojectFromWorldCoordinates} from './mercator_utils';
export type MapControlsDeltas = {
panDelta: Point;
zoomDelta: number;
bearingDelta: number;
pitchDelta: number;
rollDelta: number;
around: Point;
};
export type CameraForBoxAndBearingHandlerResult = {
center: LngLat;
zoom: number;
bearing: number;
};
export type EaseToHandlerOptions = {
bearing: number;
pitch: number;
roll: number;
padding: PaddingOptions;
offsetAsPoint: Point;
around?: LngLat;
aroundPoint?: Point;
center?: LngLatLike;
zoom?: number;
offset?: PointLike;
};
export type EaseToHandlerResult = {
easeFunc: (k: number) => void;
elevationCenter: LngLat;
isZooming: boolean;
};
export type FlyToHandlerOptions = {
bearing: number;
pitch: number;
roll: number;
padding: PaddingOptions;
offsetAsPoint: Point;
center?: LngLatLike;
locationAtOffset: LngLat;
zoom?: number;
minZoom?: number;
};
export type FlyToHandlerResult = {
easeFunc: (k: number, scale: number, centerFactor: number, pointAtOffset: Point) => void;
scaleOfZoom: number;
scaleOfMinZoom?: number;
targetCenter: LngLat;
pixelPathLength: number;
};
export type UpdateRotationArgs = {
/**
* The starting Euler angles.
*/
startEulerAngles: RollPitchBearing;
/**
* The end Euler angles.
*/
endEulerAngles: RollPitchBearing;
/**
* The transform to be updated
*/
tr: ITransform;
/**
* The interpolation fraction, between 0 and 1.
*/
k: number;
/**
* If true, use spherical linear interpolation. If false, use linear interpolation of Euler angles.
*/
useSlerp: boolean;
};
/**
* @internal
*/
export function cameraBoundsWarning() {
warnOnce(
'Map cannot fit within canvas with the given bounds, padding, and/or offset.'
);
}
/**
* @internal
* Contains projection-specific functions related to camera controls, easeTo, flyTo, inertia, etc.
*/
export interface ICameraHelper {
get useGlobeControls(): boolean;
handlePanInertia(pan: Point, transform: IReadonlyTransform): {
easingCenter: LngLat;
easingOffset: Point;
};
handleMapControlsRollPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void;
handleMapControlsPan(deltas: MapControlsDeltas, tr: ITransform, preZoomAroundLoc: LngLat): void;
cameraForBoxAndBearing(options: CameraForBoundsOptions, padding: PaddingOptions, bounds: LngLatBounds, bearing: number, tr: IReadonlyTransform): CameraForBoxAndBearingHandlerResult;
handleJumpToCenterZoom(tr: ITransform, options: { zoom?: number; center?: LngLatLike }): void;
handleEaseTo(tr: ITransform, options: EaseToHandlerOptions): EaseToHandlerResult;
handleFlyTo(tr: ITransform, options: FlyToHandlerOptions): FlyToHandlerResult;
}
/**
* @internal
* Set a transform's rotation to a value interpolated between startEulerAngles and endEulerAngles
*/
export function updateRotation(args: UpdateRotationArgs) {
if (args.useSlerp) {
// At pitch ==0, the Euler angle representation is ambiguous. In this case, set the Euler angles
// to the representation requested by the caller
if (args.k < 1) {
const startRotation = rollPitchBearingToQuat(args.startEulerAngles.roll, args.startEulerAngles.pitch, args.startEulerAngles.bearing);
const endRotation = rollPitchBearingToQuat(args.endEulerAngles.roll, args.endEulerAngles.pitch, args.endEulerAngles.bearing);
const rotation: quat = new Float64Array(4) as any;
quat.slerp(rotation, startRotation, endRotation, args.k);
const eulerAngles = getRollPitchBearing(rotation);
args.tr.setRoll(eulerAngles.roll);
args.tr.setPitch(eulerAngles.pitch);
args.tr.setBearing(eulerAngles.bearing);
} else {
args.tr.setRoll(args.endEulerAngles.roll);
args.tr.setPitch(args.endEulerAngles.pitch);
args.tr.setBearing(args.endEulerAngles.bearing);
}
} else {
args.tr.setRoll(interpolates.number(args.startEulerAngles.roll, args.endEulerAngles.roll, args.k));
args.tr.setPitch(interpolates.number(args.startEulerAngles.pitch, args.endEulerAngles.pitch, args.k));
args.tr.setBearing(interpolates.number(args.startEulerAngles.bearing, args.endEulerAngles.bearing, args.k));
}
}
export function cameraForBoxAndBearing(options: CameraForBoundsOptions, padding: PaddingOptions, bounds: LngLatBounds, bearing: number, tr: IReadonlyTransform): CameraForBoxAndBearingHandlerResult {
const edgePadding = tr.padding;
// Consider all corners of the rotated bounding box derived from the given points
// when find the camera position that fits the given points.
const nwWorld = projectToWorldCoordinates(tr.worldSize, bounds.getNorthWest());
const neWorld = projectToWorldCoordinates(tr.worldSize, bounds.getNorthEast());
const seWorld = projectToWorldCoordinates(tr.worldSize, bounds.getSouthEast());
const swWorld = projectToWorldCoordinates(tr.worldSize, bounds.getSouthWest());
const bearingRadians = degreesToRadians(-bearing);
const nwRotatedWorld = nwWorld.rotate(bearingRadians);
const neRotatedWorld = neWorld.rotate(bearingRadians);
const seRotatedWorld = seWorld.rotate(bearingRadians);
const swRotatedWorld = swWorld.rotate(bearingRadians);
const upperRight = new Point(
Math.max(nwRotatedWorld.x, neRotatedWorld.x, swRotatedWorld.x, seRotatedWorld.x),
Math.max(nwRotatedWorld.y, neRotatedWorld.y, swRotatedWorld.y, seRotatedWorld.y)
);
const lowerLeft = new Point(
Math.min(nwRotatedWorld.x, neRotatedWorld.x, swRotatedWorld.x, seRotatedWorld.x),
Math.min(nwRotatedWorld.y, neRotatedWorld.y, swRotatedWorld.y, seRotatedWorld.y)
);
// Calculate zoom: consider the original bbox and padding.
const size = upperRight.sub(lowerLeft);
const availableWidth = (tr.width - (edgePadding.left + edgePadding.right + padding.left + padding.right));
const availableHeight = (tr.height - (edgePadding.top + edgePadding.bottom + padding.top + padding.bottom));
const scaleX = availableWidth / size.x;
const scaleY = availableHeight / size.y;
if (scaleY < 0 || scaleX < 0) {
cameraBoundsWarning();
return undefined;
}
const zoom = Math.min(scaleZoom(tr.scale * Math.min(scaleX, scaleY)), options.maxZoom);
// Calculate center: apply the zoom, the configured offset, as well as offset that exists as a result of padding.
const offset = Point.convert(options.offset);
const paddingOffsetX = (padding.left - padding.right) / 2;
const paddingOffsetY = (padding.top - padding.bottom) / 2;
const paddingOffset = new Point(paddingOffsetX, paddingOffsetY);
const rotatedPaddingOffset = paddingOffset.rotate(degreesToRadians(bearing));
const offsetAtInitialZoom = offset.add(rotatedPaddingOffset);
const offsetAtFinalZoom = offsetAtInitialZoom.mult(tr.scale / zoomScale(zoom));
const center = unprojectFromWorldCoordinates(
tr.worldSize,
// either world diagonal can be used (NW-SE or NE-SW)
nwWorld.add(seWorld).div(2).sub(offsetAtFinalZoom)
);
const result = {
center,
zoom,
bearing
};
return result;
}

View File

@@ -0,0 +1,837 @@
import {beforeEach, describe, expect, test} from 'vitest';
import {GlobeTransform} from './globe_transform';
import {LngLat} from '../lng_lat';
import {coveringTiles, coveringZoomLevel, createCalculateTileZoomFunction, type CoveringTilesOptions} from './covering_tiles';
import {OverscaledTileID} from '../../tile/tile_id';
import {MercatorTransform} from './mercator_transform';
import {globeConstants} from './vertical_perspective_projection';
describe('coveringTiles', () => {
describe('globe', () => {
beforeEach(() => {
// Force faster animations so we can use shorter sleeps when testing them
globeConstants.errorTransitionTimeSeconds = 0.1;
});
test('zoomed out', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(0.0, 0.0));
transform.setZoom(-1);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(0, 0, 0, 0, 0)
]);
});
test('zoomed in', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(-0.02, 0.01));
transform.setZoom(3);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(3, 0, 3, 3, 3),
new OverscaledTileID(3, 0, 3, 3, 4),
new OverscaledTileID(3, 0, 3, 4, 3),
new OverscaledTileID(3, 0, 3, 4, 4),
]);
});
test('zoomed in 512x512', () => {
const transform = new GlobeTransform();
transform.resize(512, 512);
transform.setCenter(new LngLat(-0.02, 0.01));
transform.setZoom(3);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(3, 0, 3, 3, 3),
new OverscaledTileID(3, 0, 3, 3, 4),
new OverscaledTileID(3, 0, 3, 4, 3),
new OverscaledTileID(3, 0, 3, 4, 4),
new OverscaledTileID(3, 0, 3, 2, 3),
new OverscaledTileID(3, 0, 3, 2, 4),
new OverscaledTileID(3, 0, 3, 5, 3),
new OverscaledTileID(3, 0, 3, 5, 4)
]);
});
test('pitched', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(-0.002, 0.001));
transform.setZoom(8);
transform.setMaxPitch(80);
transform.setPitch(80);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(6, 0, 6, 32, 31),
new OverscaledTileID(6, 0, 6, 31, 31),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512),
]);
});
test('pitched+rotated', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(-0.002, 0.001));
transform.setZoom(8);
transform.setMaxPitch(80);
transform.setPitch(80);
transform.setBearing(45);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(7, 0, 7, 64, 64),
new OverscaledTileID(7, 0, 7, 64, 63),
new OverscaledTileID(7, 0, 7, 63, 63),
new OverscaledTileID(10, 0, 10, 510, 512),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 511, 513)
]);
});
test('antimeridian1', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(179.99, -0.001));
transform.setZoom(5);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(5, 0, 5, 31, 16),
new OverscaledTileID(5, 0, 5, 31, 15),
new OverscaledTileID(5, 1, 5, 0, 16),
new OverscaledTileID(5, 1, 5, 0, 15),
]);
});
test('antimeridian2', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(-179.99, 0.001));
transform.setZoom(5);
const tiles = coveringTiles(transform, {
tileSize: 512,
});
expect(tiles).toEqual([
new OverscaledTileID(5, 0, 5, 0, 15),
new OverscaledTileID(5, 0, 5, 0, 16),
new OverscaledTileID(5, -1, 5, 31, 15),
new OverscaledTileID(5, -1, 5, 31, 16),
]);
});
test('zoom < 0', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setCenter(new LngLat(0.0, 80.0));
transform.setZoom(-0.5);
const tiles = coveringTiles(transform, {
tileSize: 512,
minzoom: 0,
maxzoom: 0,
reparseOverscaled: true
});
expect(tiles).toEqual([
new OverscaledTileID(0, 0, 0, 0, 0)
]);
});
test('zoom = 11', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(0);
transform.setCenter(new LngLat(-179.73, -0.087));
const tiles = coveringTiles(transform, {
tileSize: 512,
minzoom: 0,
maxzoom: 15,
reparseOverscaled: true
});
expect(tiles).toEqual([
new OverscaledTileID(11, 0, 11, 1, 1024)
]);
});
test('zoom = 11, mid lat', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(0);
transform.setCenter(new LngLat(-179.73, 60.02));
const tiles = coveringTiles(transform, {
tileSize: 512,
minzoom: 0,
maxzoom: 15,
reparseOverscaled: true
});
expect(tiles).toEqual([
new OverscaledTileID(11, 0, 11, 1, 594)
]);
});
test('zoom = 11, high lat', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(0);
transform.setCenter(new LngLat(-179.73, 85.028));
const tiles = coveringTiles(transform, {
tileSize: 512,
minzoom: 0,
maxzoom: 15,
reparseOverscaled: true
});
expect(tiles).toEqual([
new OverscaledTileID(11, 0, 11, 1, 1)
]);
});
test('zoom = 11, mid lat, mid lng', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(0);
transform.setCenter(new LngLat(-58.97, 60.02));
const tiles = coveringTiles(transform, {
tileSize: 512,
minzoom: 0,
maxzoom: 15,
reparseOverscaled: true
});
expect(tiles).toEqual([
new OverscaledTileID(11, 0, 11, 688, 594)
]);
});
test('zoom = 11, mid lng', () => {
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(0);
transform.setCenter(new LngLat(-58.97, -0.087));
const tiles = coveringTiles(transform, {
tileSize: 512,
minzoom: 0,
maxzoom: 15,
reparseOverscaled: true
});
expect(tiles).toEqual([
new OverscaledTileID(11, 0, 11, 688, 1024)
]);
});
describe('nonzero center elevation', () => {
test('looking down', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(0.021, 0.0915));
transform.setElevation(20000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1024, 1023),
new OverscaledTileID(10, 0, 10, 511, 511),
]);
});
describe('high pitch', () => {
test('bearing 0', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(70);
transform.setBearing(0);
transform.setCenter(new LngLat(0.021, 0.0915));
transform.setElevation(20000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1023, 1023),
new OverscaledTileID(11, 0, 11, 1024, 1022),
new OverscaledTileID(11, 0, 11, 1023, 1022),
new OverscaledTileID(12, 0, 12, 2048, 2046),
new OverscaledTileID(12, 0, 12, 2048, 2047),
]);
});
test('bearing 90', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(70);
transform.setBearing(90);
transform.setCenter(new LngLat(0.021, 0.0915));
transform.setElevation(20000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1024, 1023),
new OverscaledTileID(9, 0, 9, 256, 256),
new OverscaledTileID(12, 0, 12, 2047, 2046),
new OverscaledTileID(12, 0, 12, 2047, 2047),
]);
});
test('bearing 180', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(70);
transform.setBearing(180);
transform.setCenter(new LngLat(0.021, 0.0915));
transform.setElevation(20000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1023, 1023),
new OverscaledTileID(8, 0, 8, 128, 128),
new OverscaledTileID(8, 0, 8, 127, 128),
new OverscaledTileID(12, 0, 12, 2048, 2046),
new OverscaledTileID(12, 0, 12, 2048, 2047),
]);
});
test('bearing 270', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new GlobeTransform();
transform.resize(128, 128);
transform.setZoom(11);
transform.setPitch(70);
transform.setBearing(270);
transform.setCenter(new LngLat(0.021, 0.0915));
transform.setElevation(20000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(9, 0, 9, 255, 256),
new OverscaledTileID(12, 0, 12, 2048, 2046),
new OverscaledTileID(12, 0, 12, 2048, 2047),
]);
});
});
});
});
describe('mercator', () => {
const options = {
minzoom: 1,
maxzoom: 10,
tileSize: 512
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(200, 200);
test('general', () => {
// make slightly off center so that sort order is not subject to precision issues
transform.setCenter(new LngLat(-0.01, 0.01));
transform.setZoom(0);
expect(coveringTiles(transform, options)).toEqual([]);
transform.setZoom(1);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(1, 0, 1, 0, 0),
new OverscaledTileID(1, 0, 1, 1, 0),
new OverscaledTileID(1, 0, 1, 0, 1),
new OverscaledTileID(1, 0, 1, 1, 1)]);
transform.setZoom(2.4);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(2, 0, 2, 1, 1),
new OverscaledTileID(2, 0, 2, 2, 1),
new OverscaledTileID(2, 0, 2, 1, 2),
new OverscaledTileID(2, 0, 2, 2, 2)]);
transform.setZoom(10);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(10, 0, 10, 512, 511),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512)]);
transform.setZoom(11);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(10, 0, 10, 512, 511),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512)]);
transform.resize(2048, 128);
transform.setZoom(9);
transform.setPadding({top: 16});
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(9, 0, 9, 255, 255),
new OverscaledTileID(9, 0, 9, 256, 255),
new OverscaledTileID(9, 0, 9, 255, 256),
new OverscaledTileID(9, 0, 9, 256, 256),
new OverscaledTileID(9, 0, 9, 254, 255),
new OverscaledTileID(9, 0, 9, 254, 256),
new OverscaledTileID(9, 0, 9, 257, 255),
new OverscaledTileID(9, 0, 9, 257, 256),
new OverscaledTileID(9, 0, 9, 253, 255),
new OverscaledTileID(9, 0, 9, 253, 256)]);
transform.setPadding({top: 0});
transform.setZoom(5.1);
transform.setPitch(60.0);
transform.setBearing(32.0);
transform.setCenter(new LngLat(56.90, 48.20));
transform.resize(1024, 768);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(5, 0, 5, 21, 11),
new OverscaledTileID(5, 0, 5, 20, 11),
new OverscaledTileID(5, 0, 5, 21, 10),
new OverscaledTileID(5, 0, 5, 20, 10),
new OverscaledTileID(5, 0, 5, 21, 12),
new OverscaledTileID(5, 0, 5, 22, 11),
new OverscaledTileID(5, 0, 5, 20, 12),
new OverscaledTileID(5, 0, 5, 22, 10),
new OverscaledTileID(5, 0, 5, 21, 9),
new OverscaledTileID(5, 0, 5, 20, 9),
new OverscaledTileID(5, 0, 5, 22, 9),
new OverscaledTileID(5, 0, 5, 23, 10),
new OverscaledTileID(5, 0, 5, 21, 8),
new OverscaledTileID(5, 0, 5, 20, 8),
new OverscaledTileID(5, 0, 5, 23, 9),
new OverscaledTileID(5, 0, 5, 22, 8),
new OverscaledTileID(5, 0, 5, 23, 8),
new OverscaledTileID(5, 0, 5, 21, 7),
new OverscaledTileID(5, 0, 5, 20, 7),
new OverscaledTileID(5, 0, 5, 24, 9),
new OverscaledTileID(5, 0, 5, 22, 7)
]);
transform.setZoom(8);
transform.setPitch(85.0);
transform.setBearing(0.0);
transform.setCenter(new LngLat(20.918, 39.232));
transform.resize(50, 1000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(8, 0, 8, 142, 98),
new OverscaledTileID(7, 0, 7, 71, 48),
new OverscaledTileID(5, 0, 5, 17, 11),
new OverscaledTileID(5, 0, 5, 17, 10),
new OverscaledTileID(9, 0, 9, 285, 198),
new OverscaledTileID(9, 0, 9, 285, 199)
]);
transform.setZoom(8);
transform.setPitch(60);
transform.setBearing(45.0);
transform.setCenter(new LngLat(25.02, 60.15));
transform.resize(300, 50);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(8, 0, 8, 145, 74),
new OverscaledTileID(8, 0, 8, 145, 73),
new OverscaledTileID(8, 0, 8, 146, 74)
]);
transform.resize(50, 300);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(8, 0, 8, 145, 74),
new OverscaledTileID(8, 0, 8, 145, 73),
new OverscaledTileID(8, 0, 8, 146, 74),
new OverscaledTileID(8, 0, 8, 146, 73)
]);
const optionsWithCustomTileLoading = {
minzoom: 1,
maxzoom: 10,
tileSize: 512,
calculateTileZoom: (_requestedCenterZoom: number,
_distanceToTile2D: number,
_distanceToTileZ: number,
_distanceToCenter3D: number,
_cameraVerticalFOV: number) => { return 7; }
};
transform.resize(50, 300);
transform.setPitch(70);
expect(coveringTiles(transform, optionsWithCustomTileLoading)).toEqual([
new OverscaledTileID(7, 0, 7, 74, 36),
new OverscaledTileID(7, 0, 7, 73, 37),
new OverscaledTileID(7, 0, 7, 74, 35),
new OverscaledTileID(7, 0, 7, 73, 36),
new OverscaledTileID(7, 0, 7, 72, 37),
new OverscaledTileID(7, 0, 7, 73, 35),
new OverscaledTileID(7, 0, 7, 72, 36)
]);
});
test('calculates tile coverage with low number of zoom levels and low tile count', () => {
const optionsWithTileLodParams = {
minzoom: 1,
maxzoom: 10,
tileSize: 512,
calculateTileZoom: createCalculateTileZoomFunction(1.0, 1.0)
};
expect(coveringTiles(transform, optionsWithTileLodParams)).toEqual([
new OverscaledTileID(5, 0, 5, 18, 9),
new OverscaledTileID(5, 0, 5, 18, 8)
]);
});
test('calculates tile coverage with low tile count', () => {
const optionsWithTileLodParams = {
minzoom: 1,
maxzoom: 10,
tileSize: 512,
calculateTileZoom: createCalculateTileZoomFunction(1.0, 10.0)
};
expect(coveringTiles(transform, optionsWithTileLodParams)).toEqual([
new OverscaledTileID(6, 0, 6, 37, 18),
new OverscaledTileID(6, 0, 6, 37, 17),
new OverscaledTileID(6, 0, 6, 36, 18),
new OverscaledTileID(6, 0, 6, 36, 17)
]);
});
test('calculates tile coverage with low number of zoom levels', () => {
const optionsWithTileLodParams = {
minzoom: 1,
maxzoom: 10,
tileSize: 512,
calculateTileZoom: createCalculateTileZoomFunction(10.0, 1.0)
};
expect(coveringTiles(transform, optionsWithTileLodParams)).toEqual([
new OverscaledTileID(7, 0, 7, 73, 37),
new OverscaledTileID(7, 0, 7, 73, 36),
new OverscaledTileID(7, 0, 7, 72, 36),
new OverscaledTileID(6, 0, 6, 37, 18),
new OverscaledTileID(5, 0, 5, 18, 8),
new OverscaledTileID(9, 0, 9, 290, 148),
new OverscaledTileID(9, 0, 9, 291, 148)
]);
});
test('calculates tile coverage at w > 0', () => {
transform.setZoom(2);
transform.setPitch(0);
transform.setBearing(0);
transform.resize(300, 300);
transform.setCenter(new LngLat(630.01, 0.01));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(2, 2, 2, 1, 1),
new OverscaledTileID(2, 2, 2, 1, 2),
new OverscaledTileID(2, 2, 2, 0, 1),
new OverscaledTileID(2, 2, 2, 0, 2)
]);
});
test('calculates tile coverage at w = -1', () => {
transform.setCenter(new LngLat(-360.01, 0.01));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(2, -1, 2, 1, 1),
new OverscaledTileID(2, -1, 2, 1, 2),
new OverscaledTileID(2, -1, 2, 2, 1),
new OverscaledTileID(2, -1, 2, 2, 2)
]);
});
test('calculates tile coverage across meridian', () => {
transform.setZoom(1);
transform.setCenter(new LngLat(-180.01, 0.01));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(1, 0, 1, 0, 0),
new OverscaledTileID(1, 0, 1, 0, 1),
new OverscaledTileID(1, -1, 1, 1, 0),
new OverscaledTileID(1, -1, 1, 1, 1)
]);
});
test('only includes tiles for a single world, if renderWorldCopies is set to false', () => {
transform.setZoom(1);
transform.setCenter(new LngLat(-180.01, 0.01));
transform.setRenderWorldCopies(false);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(1, 0, 1, 0, 0),
new OverscaledTileID(1, 0, 1, 0, 1)
]);
});
test('overscaledZ', () => {
const options = {
minzoom: 1,
maxzoom: 10,
tileSize: 256,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 10, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(10, 400);
// make slightly off center so that sort order is not subject to precision issues
transform.setCenter(new LngLat(-0.01, 0.01));
transform.setPitch(85);
transform.setFov(10);
transform.setZoom(10);
const tiles = coveringTiles(transform, options);
for (const tile of tiles) {
expect(tile.overscaledZ).toBeGreaterThanOrEqual(tile.canonical.z);
}
});
test('maxzoom-0', () => {
const options = {
minzoom: 0,
maxzoom: 0,
tileSize: 512
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 0, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(200, 200);
transform.setCenter(new LngLat(0.01, 0.01));
transform.setZoom(8);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(0, 0, 0, 0, 0)
]);
});
test('z11', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(-179.73, -0.087));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1, 1024)
]);
});
test('z11, mid lat', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(-179.73, 60.02));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1, 594)
]);
});
test('z11, high lat', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(-179.73, 85.028));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1, 1)
]);
});
test('z11, mid lat, mid lng', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(-58.97, 60.02));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 688, 594)
]);
});
test('z11, low lat, mid lng', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(-58.97, -0.087));
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 688, 1024)
]);
});
test('nonzero center elevation', () => {
const options = {
minzoom: 1,
maxzoom: 15,
tileSize: 512,
reparseOverscaled: true
};
const transform = new MercatorTransform({minZoom: 0, maxZoom: 15, minPitch: 0, maxPitch: 85, renderWorldCopies: true});
transform.resize(128, 128);
transform.setZoom(11);
transform.setCenter(new LngLat(0.03, 0.0915));
transform.setElevation(20000);
expect(coveringTiles(transform, options)).toEqual([
new OverscaledTileID(11, 0, 11, 1024, 1023),
new OverscaledTileID(11, 0, 11, 1023, 1023)
]);
});
});
});
describe('coveringZoomLevel', () => {
let transform: MercatorTransform;
let options: CoveringTilesOptions;
beforeEach(() => {
transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
options = {
tileSize: 512,
roundZoom: false,
};
});
test('zoom 0', () => {
transform.setZoom(0);
expect(coveringZoomLevel(transform, options)).toBe(0);
});
test('small zoom should be floored to 0', () => {
transform.setZoom(0.1);
expect(coveringZoomLevel(transform, options)).toBe(0);
});
test('zoom 2.7 should be floored to 2', () => {
transform.setZoom(2.7);
expect(coveringZoomLevel(transform, options)).toBe(2);
});
test('zoom 0 for small tile size', () => {
options.tileSize = 256;
transform.setZoom(0);
expect(coveringZoomLevel(transform, options)).toBe(1);
});
test('zoom 0.1 for small tile size', () => {
options.tileSize = 256;
transform.setZoom(0.1);
expect(coveringZoomLevel(transform, options)).toBe(1);
});
test('zoom 1 for small tile size', () => {
options.tileSize = 256;
transform.setZoom(1);
expect(coveringZoomLevel(transform, options)).toBe(2);
});
test('zoom 2.4 for small tile size', () => {
options.tileSize = 256;
transform.setZoom(2.4);
expect(coveringZoomLevel(transform, options)).toBe(3);
});
test('zoom 11.5 with rounded setting and small tile size', () => {
options.tileSize = 256;
options.roundZoom = true;
transform.setZoom(11.5);
expect(coveringZoomLevel(transform, options)).toBe(13);
});
});

View File

@@ -0,0 +1,291 @@
import {OverscaledTileID} from '../../tile/tile_id';
import {vec2, type vec4} from 'gl-matrix';
import {MercatorCoordinate} from '../mercator_coordinate';
import {degreesToRadians, scaleZoom} from '../../util/util';
import type {IReadonlyTransform} from '../transform_interface';
import type {Terrain} from '../../render/terrain';
import type {Frustum} from '../../util/primitives/frustum';
import {maxMercatorHorizonAngle} from './mercator_utils';
import {type IBoundingVolume, IntersectionResult} from '../../util/primitives/bounding_volume';
type CoveringTilesResult = {
tileID: OverscaledTileID;
distanceSq: number;
tileDistanceToCamera: number;
};
type CoveringTilesStackEntry = {
zoom: number;
x: number;
y: number;
wrap: number;
fullyVisible: boolean;
};
export type CoveringTilesOptions = {
/**
* Smallest allowed tile zoom.
*/
minzoom?: number;
/**
* Largest allowed tile zoom.
*/
maxzoom?: number;
/**
* Whether to round or floor the target zoom level. If true, the value will be rounded to the closest integer. Otherwise the value will be floored.
*/
roundZoom?: boolean;
/**
* Tile size, expressed in screen pixels.
*/
tileSize: number;
};
export type CoveringTilesOptionsInternal = CoveringTilesOptions & {
/**
* `true` if tiles should be sent back to the worker for each overzoomed zoom level, `false` if not.
* Fill this option when computing covering tiles for a source.
* When true, any tile at `maxzoom` level that should be overscaled to a greater zoom will have
* its zoom set to the overscaled greater zoom. When false, such tiles will have zoom set to `maxzoom`.
*/
reparseOverscaled?: boolean;
/**
* When terrain is present, tile visibility will be computed in regards to the min and max elevations for each tile.
*/
terrain?: Terrain;
/**
* Optional function to redefine how tiles are loaded at high pitch angles.
*/
calculateTileZoom?: CalculateTileZoomFunction;
};
/**
* Function to define how tiles are loaded at high pitch angles
* @param requestedCenterZoom - the requested zoom level, valid at the center point.
* @param distanceToTile2D - 2D distance from the camera to the candidate tile, in mercator units.
* @param distanceToTileZ - vertical distance from the camera to the candidate tile, in mercator units.
* @param distanceToCenter3D - distance from camera to center point, in mercator units
* @param cameraVerticalFOV - camera vertical field of view, in degrees
* @return the desired zoom level for this tile. May not be an integer.
*/
export type CalculateTileZoomFunction = (requestedCenterZoom: number,
distanceToTile2D: number,
distanceToTileZ: number,
distanceToCenter3D: number,
cameraVerticalFOV: number) => number;
/**
* A simple/heuristic function that returns whether the tile is visible under the current transform.
* @returns an {@link IntersectionResult}.
*/
export function isTileVisible(frustum: Frustum, tileBoundingVolume: IBoundingVolume, plane?: vec4): IntersectionResult {
const frustumTest = tileBoundingVolume.intersectsFrustum(frustum);
if (!plane || frustumTest === IntersectionResult.None) {
return frustumTest;
}
const planeTest = tileBoundingVolume.intersectsPlane(plane);
if (planeTest === IntersectionResult.None) {
return IntersectionResult.None;
}
if (frustumTest === IntersectionResult.Full && planeTest === IntersectionResult.Full) {
return IntersectionResult.Full;
}
return IntersectionResult.Partial;
}
/**
* Definite integral of cos(x)^p. The analytical solution is described in `developer-guides/covering-tiles.md`,
* but here the integral is evaluated numerically.
* @param p - the power to raise cos(x) to inside the integral
* @param x1 - the starting point of the integral.
* @param x2 - the ending point of the integral.
* @return the integral of cos(x)^p from x=x1 to x=x2
*/
function integralOfCosXByP(p: number, x1: number, x2: number): number {
const numPoints = 10;
let sum = 0;
const dx = (x2 - x1 ) / numPoints;
// Midpoint integration
for( let i = 0; i < numPoints; i++)
{
const x = x1 + (i + 0.5)/numPoints * (x2 - x1);
sum += dx * Math.pow(Math.cos(x), p);
}
return sum;
}
export function createCalculateTileZoomFunction(maxZoomLevelsOnScreen: number, tileCountMaxMinRatio: number): CalculateTileZoomFunction {
return function (requestedCenterZoom: number,
distanceToTile2D: number,
distanceToTileZ: number,
distanceToCenter3D: number,
cameraVerticalFOV: number): number {
/**
* Controls how tiles are loaded at high pitch angles. Higher numbers cause fewer, lower resolution
* tiles to be loaded. Calculate the value that will result in the selected number of zoom levels in
* the worst-case condition (when the horizon is at the top of the screen). For more information, see
* `developer-guides/covering-tiles.md`
*/
const pitchTileLoadingBehavior = 2 * ((maxZoomLevelsOnScreen - 1) /
scaleZoom(Math.cos(degreesToRadians(maxMercatorHorizonAngle - cameraVerticalFOV)) /
Math.cos(degreesToRadians(maxMercatorHorizonAngle))) - 1);
const centerPitch = Math.acos(distanceToTileZ / distanceToCenter3D);
const tileCountPitch0 = 2 * integralOfCosXByP(pitchTileLoadingBehavior - 1, 0, degreesToRadians(cameraVerticalFOV / 2));
const highestPitch = Math.min(degreesToRadians(maxMercatorHorizonAngle), centerPitch + degreesToRadians(cameraVerticalFOV / 2));
const lowestPitch = Math.min(highestPitch, centerPitch - degreesToRadians(cameraVerticalFOV / 2));
const tileCount = integralOfCosXByP(pitchTileLoadingBehavior - 1, lowestPitch, highestPitch);
const thisTilePitch = Math.atan(distanceToTile2D / distanceToTileZ);
const distanceToTile3D = Math.hypot(distanceToTile2D, distanceToTileZ);
let thisTileDesiredZ = requestedCenterZoom;
// if distance to candidate tile is a tiny bit farther than distance to center,
// use the same zoom as the center. This is achieved by the scaling distance ratio by cos(fov/2)
thisTileDesiredZ = thisTileDesiredZ + scaleZoom(distanceToCenter3D / distanceToTile3D / Math.max(0.5, Math.cos(degreesToRadians(cameraVerticalFOV / 2))));
thisTileDesiredZ += pitchTileLoadingBehavior * scaleZoom(Math.cos(thisTilePitch)) / 2;
thisTileDesiredZ -= scaleZoom(Math.max(1, tileCount / tileCountPitch0 / tileCountMaxMinRatio)) / 2;
return thisTileDesiredZ;
};
}
const defaultMaxZoomLevelsOnScreen = 9.314;
const defaultTileCountMaxMinRatio = 3.0;
const defaultCalculateTileZoom = createCalculateTileZoomFunction(defaultMaxZoomLevelsOnScreen, defaultTileCountMaxMinRatio);
/**
* Return what zoom level of a tile source would most closely cover the tiles displayed by this transform.
* @param options - The options, most importantly the source's tile size.
* @returns An integer zoom level at which all tiles will be visible.
*/
export function coveringZoomLevel(transform: IReadonlyTransform, options: CoveringTilesOptions): number {
const z = (options.roundZoom ? Math.round : Math.floor)(
transform.zoom + scaleZoom(transform.tileSize / options.tileSize)
);
// At negative zoom levels load tiles from z0 because negative tile zoom levels don't exist.
return Math.max(0, z);
}
/**
* Returns a list of tiles that optimally covers the screen. Adapted for globe projection.
* Correctly handles LOD when moving over the antimeridian.
* @param transform - The transform instance.
* @param frustum - The covering frustum.
* @param plane - The clipping plane used by globe transform, or null.
* @param cameraCoord - The x, y, z position of the camera in MercatorCoordinates.
* @param centerCoord - The x, y, z position of the center point in MercatorCoordinates.
* @param options - Additional coveringTiles options.
* @param details - Interface to define required helper functions.
* @returns A list of tile coordinates, ordered by ascending distance from camera.
*/
export function coveringTiles(transform: IReadonlyTransform, options: CoveringTilesOptionsInternal): OverscaledTileID[] {
const frustum = transform.getCameraFrustum();
const plane = transform.getClippingPlane();
const cameraCoord = transform.screenPointToMercatorCoordinate(transform.getCameraPoint());
const centerCoord = MercatorCoordinate.fromLngLat(transform.center, transform.elevation);
cameraCoord.z = centerCoord.z + Math.cos(transform.pitchInRadians) * transform.cameraToCenterDistance / transform.worldSize;
const detailsProvider = transform.getCoveringTilesDetailsProvider();
const allowVariableZoom = detailsProvider.allowVariableZoom(transform, options);
const desiredZ = coveringZoomLevel(transform, options);
const minZoom = options.minzoom || 0;
const maxZoom = options.maxzoom !== undefined ? options.maxzoom : transform.maxZoom;
const nominalZ = Math.min(Math.max(0, desiredZ), maxZoom);
const numTiles = Math.pow(2, nominalZ);
const cameraPoint = [numTiles * cameraCoord.x, numTiles * cameraCoord.y, 0];
const centerPoint = [numTiles * centerCoord.x, numTiles * centerCoord.y, 0];
const distanceToCenter2d = Math.hypot(centerCoord.x - cameraCoord.x, centerCoord.y - cameraCoord.y);
const distanceZ = Math.abs(centerCoord.z - cameraCoord.z);
const distanceToCenter3d = Math.hypot(distanceToCenter2d, distanceZ);
const newRootTile = (wrap: number): CoveringTilesStackEntry => {
return {
zoom: 0,
x: 0,
y: 0,
wrap,
fullyVisible: false
};
};
// Do a depth-first traversal to find visible tiles and proper levels of detail
const stack: Array<CoveringTilesStackEntry> = [];
const result: Array<CoveringTilesResult> = [];
if (transform.renderWorldCopies && detailsProvider.allowWorldCopies()) {
// Render copy of the globe thrice on both sides
for (let i = 1; i <= 3; i++) {
stack.push(newRootTile(-i));
stack.push(newRootTile(i));
}
}
stack.push(newRootTile(0));
while (stack.length > 0) {
const it = stack.pop();
const x = it.x;
const y = it.y;
let fullyVisible = it.fullyVisible;
const tileID = {x, y, z: it.zoom};
const boundingVolume = detailsProvider.getTileBoundingVolume(tileID, it.wrap, transform.elevation, options);
// Visibility of a tile is not required if any of its ancestor is fully visible
if (!fullyVisible) {
const intersectResult = isTileVisible(frustum, boundingVolume, plane);
if (intersectResult === IntersectionResult.None)
continue;
fullyVisible = intersectResult === IntersectionResult.Full;
}
const distToTile2d = detailsProvider.distanceToTile2d(cameraCoord.x, cameraCoord.y, tileID, boundingVolume);
let thisTileDesiredZ = desiredZ;
if (allowVariableZoom) {
const tileZoomFunc = options.calculateTileZoom || defaultCalculateTileZoom;
thisTileDesiredZ = tileZoomFunc(transform.zoom + scaleZoom(transform.tileSize / options.tileSize),
distToTile2d,
distanceZ,
distanceToCenter3d,
transform.fov);
}
thisTileDesiredZ = (options.roundZoom ? Math.round : Math.floor)(thisTileDesiredZ);
thisTileDesiredZ = Math.max(0, thisTileDesiredZ);
const z = Math.min(thisTileDesiredZ, maxZoom);
// We need to compute a valid wrap value for the tile to keep globe compatibility with mercator
it.wrap = detailsProvider.getWrap(centerCoord, tileID, it.wrap);
// Have we reached the target depth?
if (it.zoom >= z) {
if (it.zoom < minZoom) {
continue;
}
const dz = nominalZ - it.zoom;
const dx = cameraPoint[0] - 0.5 - (x << dz);
const dy = cameraPoint[1] - 0.5 - (y << dz);
const overscaledZ = options.reparseOverscaled ? Math.max(it.zoom, thisTileDesiredZ) : it.zoom;
result.push({
tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y),
distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y]),
// this variable is currently not used, but may be important to reduce the amount of loaded tiles
tileDistanceToCamera: Math.sqrt(dx * dx + dy * dy)
});
continue;
}
for (let i = 0; i < 4; i++) {
const childX = (x << 1) + (i % 2);
const childY = (y << 1) + (i >> 1);
const childZ = it.zoom + 1;
stack.push({zoom: childZ, x: childX, y: childY, wrap: it.wrap, fullyVisible});
}
}
return result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID);
}

View File

@@ -0,0 +1,44 @@
import {type IBoundingVolume} from '../../util/primitives/bounding_volume';
import {type MercatorCoordinate} from '../mercator_coordinate';
import {type IReadonlyTransform} from '../transform_interface';
import {type CoveringTilesOptionsInternal} from './covering_tiles';
export interface CoveringTilesDetailsProvider {
/**
* Returns the distance from the point to the tile
* @param pointX - point x.
* @param pointY - point y.
* @param tileID - Tile x, y and z for zoom.
* @param boundingVolume - tile bounding volume
*/
distanceToTile2d: (pointX: number, pointY: number, tileID: {x: number; y: number; z: number}, boundingVolume: IBoundingVolume) => number;
/**
* Returns the wrap value for a given tile.
*/
getWrap: (centerCoord: MercatorCoordinate, tileID: {x:number; y: number; z: number}, parentWrap: number) => number;
/**
* Returns the bounding volume of the specified tile.
* @param tileID - Tile x, y and z for zoom.
* @param wrap - wrap number of the tile.
* @param elevation - camera center point elevation.
* @param options - CoveringTilesOptions.
*/
getTileBoundingVolume: (tileID: {x: number; y: number; z: number}, wrap: number, elevation: number, options: CoveringTilesOptionsInternal) => IBoundingVolume;
/**
* Whether to allow variable zoom, which is used at high pitch angle to avoid loading an excessive amount of tiles.
*/
allowVariableZoom: (transform: IReadonlyTransform, options: CoveringTilesOptionsInternal) => boolean;
/**
* Whether to allow world copies to be rendered.
*/
allowWorldCopies: () => boolean;
/**
* Prepare cache for the next frame.
*/
prepareNextFrame(): void;
}

View File

@@ -0,0 +1,66 @@
import {MercatorCameraHelper} from './mercator_camera_helper';
import {VerticalPerspectiveCameraHelper} from './vertical_perspective_camera_helper';
import type Point from '@mapbox/point-geometry';
import type {CameraForBoxAndBearingHandlerResult, EaseToHandlerResult, EaseToHandlerOptions, FlyToHandlerResult, FlyToHandlerOptions, ICameraHelper, MapControlsDeltas} from './camera_helper';
import type {LngLat, LngLatLike} from '../lng_lat';
import type {IReadonlyTransform, ITransform} from '../transform_interface';
import type {GlobeProjection} from './globe_projection';
import type {CameraForBoundsOptions} from '../../ui/camera';
import type {LngLatBounds} from '../lng_lat_bounds';
import type {PaddingOptions} from '../edge_insets';
/**
* @internal
*/
export class GlobeCameraHelper implements ICameraHelper {
private _globe: GlobeProjection;
private _mercatorCameraHelper: MercatorCameraHelper;
private _verticalPerspectiveCameraHelper: VerticalPerspectiveCameraHelper;
constructor(globe: GlobeProjection) {
this._globe = globe;
this._mercatorCameraHelper = new MercatorCameraHelper();
this._verticalPerspectiveCameraHelper = new VerticalPerspectiveCameraHelper();
}
get useGlobeControls(): boolean { return this._globe.useGlobeRendering; }
get currentHelper(): ICameraHelper {
return this.useGlobeControls ? this._verticalPerspectiveCameraHelper : this._mercatorCameraHelper;
}
handlePanInertia(pan: Point, transform: IReadonlyTransform): {
easingCenter: LngLat;
easingOffset: Point;
} {
return this.currentHelper.handlePanInertia(pan, transform);
}
handleMapControlsRollPitchBearingZoom(deltas: MapControlsDeltas, tr: ITransform): void {
return this.currentHelper.handleMapControlsRollPitchBearingZoom(deltas, tr);
}
handleMapControlsPan(deltas: MapControlsDeltas, tr: ITransform, preZoomAroundLoc: LngLat): void {
this.currentHelper.handleMapControlsPan(deltas, tr, preZoomAroundLoc);
}
cameraForBoxAndBearing(options: CameraForBoundsOptions, padding: PaddingOptions, bounds: LngLatBounds, bearing: number, tr: ITransform): CameraForBoxAndBearingHandlerResult {
return this.currentHelper.cameraForBoxAndBearing(options, padding, bounds, bearing, tr);
}
/**
* Handles the zoom and center change during camera jumpTo.
*/
handleJumpToCenterZoom(tr: ITransform, options: { zoom?: number; center?: LngLatLike }): void {
this.currentHelper.handleJumpToCenterZoom(tr, options);
}
handleEaseTo(tr: ITransform, options: EaseToHandlerOptions): EaseToHandlerResult {
return this.currentHelper.handleEaseTo(tr, options);
}
handleFlyTo(tr: ITransform, options: FlyToHandlerOptions): FlyToHandlerResult {
return this.currentHelper.handleFlyTo(tr, options);
}
}

View File

@@ -0,0 +1,105 @@
import {describe, expect, test} from 'vitest';
import {expectToBeCloseToArray} from '../../util/test/util';
import {GlobeCoveringTilesDetailsProvider} from './globe_covering_tiles_details_provider';
import {ConvexVolume} from '../../util/primitives/convex_volume';
describe('bounding volume creation', () => {
test('z=0', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
const convex = detailsProvider.getTileBoundingVolume({
x: 0,
y: 0,
z: 0,
}, null, null, null);
expect(convex).toEqual(ConvexVolume.fromAabb(
[-1, -1, -1],
[1, 1, 1],
));
});
test('z=1,x=0', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
const convex = detailsProvider.getTileBoundingVolume({
x: 0,
y: 0,
z: 1,
}, null, null, null);
expect(convex).toEqual(ConvexVolume.fromAabb(
[-1, 0, -1],
[0, 1, 1],
));
});
test('z=1,x=1', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
const convex = detailsProvider.getTileBoundingVolume({
x: 1,
y: 0,
z: 1,
}, null, null, null);
expect(convex).toEqual(ConvexVolume.fromAabb(
[0, 0, -1],
[1, 1, 1],
));
});
test('z=5,x=1,y=1', () => {
const detailsProvider = new GlobeCoveringTilesDetailsProvider();
const convex = detailsProvider.getTileBoundingVolume({
x: 1,
y: 1,
z: 5,
}, null, null, null);
const precision = 10;
const expectedMin = [-0.04878262717137475, 0.9918417649235776, -0.1250257487589308];
const expectedMax = [-0.020462724105427713, 0.9944839919477184, -0.09690430455523656];
const expectedPoints = [
[-0.040144275638466294, 0.9946001124628003, -0.09691685469802916],
[-0.04013795776704037, 0.9944589865528525, -0.09690160200714736],
[-0.02046537424682884, 0.9946001124628002, -0.10288638417221826],
[-0.020462153423906553, 0.9944589865528524, -0.10287019200194392],
[-0.04902182691658952, 0.9919123845540323, -0.11834915939433684],
[-0.049015509045163594, 0.9917712586440846, -0.11833390670345505],
[-0.02499111064168652, 0.9919123845540323, -0.1256387974810376],
[-0.02498788981876423, 0.9917712586440844, -0.12562260531076325]
];
const expectedPlanes = [
[0.033568258567807485, -0.9932912960221243, 0.11065971834147033, 1],
[
-0.033568258567807485,
0.9932912960221243,
-0.11065971834147033,
-0.999857920923587
],
[
-0.2883372432854479,
-0.11563909912606864,
-0.9505205062952928,
0.011318113428480242
],
[
0.2883372432854479,
0.11563909912606864,
0.9505205062952928,
0.011924266779254289
],
[
0.9238795325112867,
-3.8143839245115144e-17,
-0.38268343236509017,
0
],
[-0.9807852804032307, 0, 0.19509032201612764, 0]
];
expectToBeCloseToArray([...convex.min], expectedMin, precision);
expectToBeCloseToArray([...convex.max], expectedMax, precision);
expect(convex.points).toHaveLength(expectedPoints.length);
for (let i = 0; i < convex.points.length; i++) {
expectToBeCloseToArray([...convex.points[i]], expectedPoints[i], precision);
}
expect(convex.planes).toHaveLength(expectedPlanes.length);
for (let i = 0; i < convex.planes.length; i++) {
expectToBeCloseToArray([...convex.planes[i]], expectedPlanes[i], precision);
}
});
});

View File

@@ -0,0 +1,311 @@
import {EXTENT} from '../../data/extent';
import {projectTileCoordinatesToSphere} from './globe_utils';
import {BoundingVolumeCache} from '../../util/primitives/bounding_volume_cache';
import {coveringZoomLevel, type CoveringTilesOptionsInternal} from './covering_tiles';
import {vec3, type vec4} from 'gl-matrix';
import type {IReadonlyTransform} from '../transform_interface';
import type {MercatorCoordinate} from '../mercator_coordinate';
import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
import {OverscaledTileID} from '../../tile/tile_id';
import {earthRadius} from '../lng_lat';
import {ConvexVolume} from '../../util/primitives/convex_volume';
import {threePlaneIntersection} from '../../util/util';
/**
* Computes distance of a point to a tile in an arbitrary axis.
* World is assumed to have size 1, distance returned is to the nearer tile edge.
* @param point - Point position.
* @param tile - Tile position.
* @param tileSize - Tile size.
*/
function distanceToTileSimple(point: number, tile: number, tileSize: number): number {
const delta = point - tile;
return (delta < 0) ? -delta : Math.max(0, delta - tileSize);
}
function distanceToTileWrapX(pointX: number, pointY: number, tileCornerX: number, tileCornerY: number, tileSize: number): number {
const tileCornerToPointX = pointX - tileCornerX;
let distanceX: number;
if (tileCornerToPointX < 0) {
// Point is left of tile
distanceX = Math.min(-tileCornerToPointX, 1.0 + tileCornerToPointX - tileSize);
} else if (tileCornerToPointX > tileSize) {
// Point is right of tile
distanceX = Math.min(Math.max(tileCornerToPointX - tileSize, 0), 1.0 - tileCornerToPointX);
} else {
// Point is inside tile in the X axis.
distanceX = 0;
}
return Math.max(distanceX, distanceToTileSimple(pointY, tileCornerY, tileSize));
}
export class GlobeCoveringTilesDetailsProvider implements CoveringTilesDetailsProvider {
private _boundingVolumeCache: BoundingVolumeCache<ConvexVolume> = new BoundingVolumeCache(this._computeTileBoundingVolume);
/**
* Prepares the internal bounding volume cache for the next frame.
*/
prepareNextFrame() {
this._boundingVolumeCache.swapBuffers();
}
/**
* Returns the distance of a point to a square tile. If the point is inside the tile, returns 0.
* Assumes the world to be of size 1.
* Handles distances on a sphere correctly: X is wrapped when crossing the antimeridian,
* when crossing the poles Y is mirrored and X is shifted by half world size.
*/
distanceToTile2d(pointX: number, pointY: number, tileID: {x: number; y: number; z: number}, _bv: ConvexVolume): number {
const scale = 1 << tileID.z;
const tileMercatorSize = 1.0 / scale;
const tileCornerX = tileID.x / scale; // In range 0..1
const tileCornerY = tileID.y / scale; // In range 0..1
const worldSize = 1.0;
const halfWorld = 0.5 * worldSize;
let smallestDistance = 2.0 * worldSize;
// Original tile
smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX, tileCornerY, tileMercatorSize));
// Up
smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX + halfWorld, -tileCornerY - tileMercatorSize, tileMercatorSize));
// Down
smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX + halfWorld, worldSize + worldSize - tileCornerY - tileMercatorSize, tileMercatorSize));
return smallestDistance;
}
/**
* Returns the wrap value for a given tile, computed so that tiles will remain loaded when crossing the antimeridian.
*/
getWrap(centerCoord: MercatorCoordinate, tileID: {x: number; y: number; z: number}, _parentWrap: number): number {
const scale = 1 << tileID.z;
const tileMercatorSize = 1.0 / scale;
const tileX = tileID.x / scale; // In range 0..1
const distanceCurrent = distanceToTileSimple(centerCoord.x, tileX, tileMercatorSize);
const distanceLeft = distanceToTileSimple(centerCoord.x, tileX - 1.0, tileMercatorSize);
const distanceRight = distanceToTileSimple(centerCoord.x, tileX + 1.0, tileMercatorSize);
const distanceSmallest = Math.min(distanceCurrent, distanceLeft, distanceRight);
if (distanceSmallest === distanceRight) {
return 1;
}
if (distanceSmallest === distanceLeft) {
return -1;
}
return 0;
}
allowVariableZoom(transform: IReadonlyTransform, options: CoveringTilesOptionsInternal): boolean {
return coveringZoomLevel(transform, options) > 4;
}
allowWorldCopies(): boolean {
return false;
}
getTileBoundingVolume(tileID: { x: number; y: number; z: number }, wrap: number, elevation: number, options: CoveringTilesOptionsInternal) {
return this._boundingVolumeCache.getTileBoundingVolume(tileID, wrap, elevation, options);
}
private _computeTileBoundingVolume(tileID: {x: number; y: number; z: number}, wrap: number, elevation: number, options: CoveringTilesOptionsInternal): ConvexVolume {
let minElevation = 0;
let maxElevation = 0;
if (options?.terrain) {
const overscaledTileID = new OverscaledTileID(tileID.z, wrap, tileID.z, tileID.x, tileID.y);
const minMax = options.terrain.getMinMaxElevation(overscaledTileID);
minElevation = minMax.minElevation ?? Math.min(0, elevation);
maxElevation = minMax.maxElevation ?? Math.max(0, elevation);
}
// Convert elevation to distances from center of a unit sphere planet (so that 1 is surface)
minElevation /= earthRadius;
maxElevation /= earthRadius;
minElevation += 1;
maxElevation += 1;
if (tileID.z <= 0) {
// Tile covers the entire sphere.
return ConvexVolume.fromAabb( // We return an AABB in this case.
[-maxElevation, -maxElevation, -maxElevation],
[maxElevation, maxElevation, maxElevation]
);
} else if (tileID.z === 1) {
// Tile covers a quarter of the sphere.
// X is 1 at lng=E90°
// Y is 1 at **north** pole
// Z is 1 at null island
return ConvexVolume.fromAabb( // We also just use AABBs for this zoom level.
[tileID.x === 0 ? -maxElevation : 0, tileID.y === 0 ? 0 : -maxElevation, -maxElevation],
[tileID.x === 0 ? 0 : maxElevation, tileID.y === 0 ? maxElevation : 0, maxElevation]
);
} else {
const corners = [
projectTileCoordinatesToSphere(0, 0, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(EXTENT, 0, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(EXTENT, EXTENT, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(0, EXTENT, tileID.x, tileID.y, tileID.z),
];
const extremesPoints = [];
for (const c of corners) {
extremesPoints.push(vec3.scale([] as any, c, maxElevation));
}
if (maxElevation !== minElevation) {
// Only add additional points if terrain is enabled and is not flat.
for (const c of corners) {
extremesPoints.push(vec3.scale([] as any, c, minElevation));
}
}
// Special handling of poles - we need to extend the tile AABB
// to include the pole for tiles that border mercator north/south edge.
if (tileID.y === 0) {
extremesPoints.push([0, 1, 0]); // North pole
}
if (tileID.y === (1 << tileID.z) - 1) {
extremesPoints.push([0, -1, 0]); // South pole
}
// Compute a best-fit AABB for the frustum rejection test
const aabbMin: vec3 = [1, 1, 1];
const aabbMax: vec3 = [-1, -1, -1];
for (const c of extremesPoints) {
for (let i = 0; i < 3; i++) {
aabbMin[i] = Math.min(aabbMin[i], c[i]);
aabbMax[i] = Math.max(aabbMax[i], c[i]);
}
}
// Now we compute the actual bounding volume.
// The up/down plane will be normal to the tile's center.
// The north/south plane will be used for the tile's north and south edge and will be orthogonal to the up/down plane.
// The left and right planes will be determined by the tile's east/west edges and will differ slightly - we are not creating a box!
// We will find the min and max extents for the up/down and north/south planes using the set of points
// where the extremes are likely to lie.
// Vector "center" (from planet center to tile center) will be our up/down axis.
const center = projectTileCoordinatesToSphere(EXTENT / 2, EXTENT / 2, tileID.x, tileID.y, tileID.z);
// Vector to the east of "center".
const centerEast = vec3.cross([] as any, [0, 1, 0], center);
vec3.normalize(centerEast, centerEast);
// Vector to the north of "center" will be our north/south axis.
const north = vec3.cross([] as any, center, centerEast);
vec3.normalize(north, north);
// Axes for the east and west edge of our bounding volume.
// These axes are NOT opposites of each other, they differ!
// They are also not orthogonal to the up/down and north/south axes.
const axisEast = vec3.cross([] as any, corners[2], corners[1]);
vec3.normalize(axisEast, axisEast);
const axisWest = vec3.cross([] as any, corners[0], corners[3]);
vec3.normalize(axisWest, axisWest);
// Now we will expand the extremes point set for bounding volume creation.
// We will also include the tile center point, since it will always be an extreme for the "center" axis.
extremesPoints.push(vec3.scale([] as any, center, maxElevation));
// No need to include a minElevation-scaled center, since we already have minElevation corners in the set and these will always lie lower than the center.
// The extremes might also lie on the midpoint of the north or south edge.
// For tiles in the north hemisphere, only the south edge can contain an extreme,
// since when we imagine the tile's actual shape projected onto the plane normal to "center" vector,
// the tile's north edge will curve towards the tile center, thus its extremes are accounted for by the
// corners, however the south edge will curve away from the center point, extending beyond the tile's edges,
// thus it must be included.
// The poles are an exception - they must always be included in the extremes, if the tile touches the north/south mercator range edge.
//
// A tile's exaggerated shape on the northern hemisphere, projected onto the normal plane of "center".
// The "c" is the tile's center point. The "m" is the edge mid point we are looking for.
//
// /-- --\
// / ------- \
// / \
// / c \
// / \
// /-- --\
// ----- -----
// ---m---
if (tileID.y >= (1 << tileID.z) / 2) {
// South hemisphere - include the tile's north edge midpoint
extremesPoints.push(vec3.scale([] as any, projectTileCoordinatesToSphere(EXTENT / 2, 0, tileID.x, tileID.y, tileID.z), maxElevation));
// No need to include minElevation variant of this point, for the same reason why we don't include minElevation center.
}
if (tileID.y < (1 << tileID.z) / 2) {
// North hemisphere - include the tile's south edge midpoint
extremesPoints.push(vec3.scale([] as any, projectTileCoordinatesToSphere(EXTENT / 2, EXTENT, tileID.x, tileID.y, tileID.z), maxElevation));
// No need to include minElevation variant of this point, for the same reason why we don't include minElevation center.
}
// Find the min and max extends and the midpoints along each axis,
// using the set of extreme points.
const upDownMinMax = findAxisMinMax(center, extremesPoints);
const northSouthMinMax = findAxisMinMax(north, extremesPoints);
const planeUp = [-center[0], -center[1], -center[2], upDownMinMax.max] as vec4;
const planeDown = [center[0], center[1], center[2], -upDownMinMax.min] as vec4;
const planeNorth = [-north[0], -north[1], -north[2], northSouthMinMax.max] as vec4;
const planeSouth = [north[0], north[1], north[2], -northSouthMinMax.min] as vec4;
const planeEast = [...axisEast, 0] as vec4;
const planeWest = [...axisWest, 0] as vec4;
const points: vec3[] = [];
// North points
if (tileID.y === 0) {
// If the tile borders a pole, then
points.push(
threePlaneIntersection(planeWest, planeEast, planeUp),
threePlaneIntersection(planeWest, planeEast, planeDown),
);
} else {
points.push(
threePlaneIntersection(planeNorth, planeEast, planeUp),
threePlaneIntersection(planeNorth, planeEast, planeDown),
threePlaneIntersection(planeNorth, planeWest, planeUp),
threePlaneIntersection(planeNorth, planeWest, planeDown)
);
}
// South points
if (tileID.y === (1 << tileID.z) - 1) {
points.push(
threePlaneIntersection(planeWest, planeEast, planeUp),
threePlaneIntersection(planeWest, planeEast, planeDown),
);
} else {
points.push(
threePlaneIntersection(planeSouth, planeEast, planeUp),
threePlaneIntersection(planeSouth, planeEast, planeDown),
threePlaneIntersection(planeSouth, planeWest, planeUp),
threePlaneIntersection(planeSouth, planeWest, planeDown)
);
}
return new ConvexVolume(points, [
planeUp,
planeDown,
planeNorth,
planeSouth,
planeEast,
planeWest
], aabbMin, aabbMax);
}
}
}
function findAxisMinMax(axis: vec3, points: vec3[]) {
let min = +Infinity;
let max = -Infinity;
for (const c of points) {
const dot = vec3.dot(axis, c);
min = Math.min(min, dot);
max = Math.max(max, dot);
}
return {
min,
max
};
}

View File

@@ -0,0 +1,139 @@
import {ProjectionDefinition, type ProjectionDefinitionSpecification, type ProjectionSpecification, type StylePropertySpecification, latest as styleSpec} from '@maplibre/maplibre-gl-style-spec';
import {DataConstantProperty, type PossiblyEvaluated, Properties, Transitionable, type Transitioning, type TransitionParameters} from '../../style/properties';
import {Evented} from '../../util/evented';
import {EvaluationParameters} from '../../style/evaluation_parameters';
import {MercatorProjection} from './mercator_projection';
import {VerticalPerspectiveProjection} from './vertical_perspective_projection';
import {type Projection, type ProjectionGPUContext, type TileMeshUsage} from './projection';
import {type PreparedShader} from '../../shaders/shaders';
import {type SubdivisionGranularitySetting} from '../../render/subdivision_granularity_settings';
import {type Context} from '../../gl/context';
import {type CanonicalTileID} from '../../tile/tile_id';
import {type Mesh} from '../../render/mesh';
type ProjectionProps = {
type: DataConstantProperty<ProjectionDefinition>;
};
type ProjectionPossiblyEvaluated = {
type: ProjectionDefinitionSpecification;
};
const properties: Properties<ProjectionProps> = new Properties({
'type': new DataConstantProperty(styleSpec.projection.type as StylePropertySpecification)
});
export class GlobeProjection extends Evented implements Projection {
properties: PossiblyEvaluated<ProjectionProps, ProjectionPossiblyEvaluated>;
_transitionable: Transitionable<ProjectionProps>;
_transitioning: Transitioning<ProjectionProps>;
_mercatorProjection: MercatorProjection;
_verticalPerspectiveProjection: VerticalPerspectiveProjection;
constructor(projection?: ProjectionSpecification) {
super();
this._transitionable = new Transitionable(properties, undefined);
this.setProjection(projection);
this._transitioning = this._transitionable.untransitioned();
this.recalculate(new EvaluationParameters(0));
this._mercatorProjection = new MercatorProjection();
this._verticalPerspectiveProjection = new VerticalPerspectiveProjection();
}
public get transitionState(): number {
const currentProjectionSpecValue = this.properties.get('type');
if (typeof currentProjectionSpecValue === 'string' && currentProjectionSpecValue === 'mercator') {
return 0;
}
if (typeof currentProjectionSpecValue === 'string' && currentProjectionSpecValue === 'vertical-perspective') {
return 1;
}
if (currentProjectionSpecValue instanceof ProjectionDefinition) {
if (currentProjectionSpecValue.from === 'vertical-perspective' && currentProjectionSpecValue.to === 'mercator') {
return 1 - currentProjectionSpecValue.transition;
}
if (currentProjectionSpecValue.from === 'mercator' && currentProjectionSpecValue.to === 'vertical-perspective') {
return currentProjectionSpecValue.transition;
}
};
return 1;
}
get useGlobeRendering(): boolean {
return this.transitionState > 0;
}
get latitudeErrorCorrectionRadians(): number { return this._verticalPerspectiveProjection.latitudeErrorCorrectionRadians; }
private get currentProjection(): Projection {
return this.useGlobeRendering ? this._verticalPerspectiveProjection : this._mercatorProjection;
}
get name(): ProjectionSpecification['type'] {
return 'globe';
}
get useSubdivision(): boolean {
return this.currentProjection.useSubdivision;
}
get shaderVariantName(): string {
return this.currentProjection.shaderVariantName;
}
get shaderDefine(): string {
return this.currentProjection.shaderDefine;
}
get shaderPreludeCode(): PreparedShader {
return this.currentProjection.shaderPreludeCode;
}
get vertexShaderPreludeCode(): string {
return this.currentProjection.vertexShaderPreludeCode;
}
get subdivisionGranularity(): SubdivisionGranularitySetting {
return this.currentProjection.subdivisionGranularity;
}
get useGlobeControls(): boolean {
return this.transitionState > 0;
}
public destroy(): void {
this._mercatorProjection.destroy();
this._verticalPerspectiveProjection.destroy();
}
public updateGPUdependent(context: ProjectionGPUContext): void {
this._mercatorProjection.updateGPUdependent(context);
this._verticalPerspectiveProjection.updateGPUdependent(context);
}
public getMeshFromTileID(context: Context, _tileID: CanonicalTileID, _hasBorder: boolean, _allowPoles: boolean, _usage: TileMeshUsage): Mesh {
return this.currentProjection.getMeshFromTileID(context, _tileID, _hasBorder, _allowPoles, _usage);
}
setProjection(projection?: ProjectionSpecification) {
this._transitionable.setValue('type', projection?.type || 'mercator');
}
updateTransitions(parameters: TransitionParameters) {
this._transitioning = this._transitionable.transitioned(parameters, this._transitioning);
}
hasTransition(): boolean {
return this._transitioning.hasTransition() || this.currentProjection.hasTransition();
}
recalculate(parameters: EvaluationParameters) {
this.properties = this._transitioning.possiblyEvaluate(parameters);
}
setErrorQueryLatitudeDegrees(value: number) {
this._verticalPerspectiveProjection.setErrorQueryLatitudeDegrees(value);
this._mercatorProjection.setErrorQueryLatitudeDegrees(value);
}
}

Some files were not shown because too many files have changed in this diff Show More