diff --git a/README.md b/README.md
index ae70321ed6..10ef59ebef 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ Read the [CONTRIBUTING.md](CONTRIBUTING.md) guide in order to get familiar with
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).
+> **MapLibre GL JS** is developed following [Semantic Versioning (2.0.0)](https://semver.org/spec/v2.0.0.html).
### Bounties
diff --git a/build/generate-docs.ts b/build/generate-docs.ts
index 859fd1a55f..aa211801cf 100644
--- a/build/generate-docs.ts
+++ b/build/generate-docs.ts
@@ -74,9 +74,45 @@ function generateReadme() {
fs.rmSync(globalsFile);
}
+/**
+ * Attempts to parse configuration metadata out of the first comment block,
+ * interpreting it JSON.
+ * If JSON parsing succeeds, the comment is removed and the JSON object returned as a configuration object.
+ * Otherwise, the HTML is left intact and the config is null.
+ * @param rawHtml - A raw HTML string to preprocess.
+ * @returns A JSON object with two keys: config and htmlContent. Config may be null.
+ */
+function preprocessHTML(rawHtml: string) {
+ const configPattern = //;
+ const match = rawHtml.match(configPattern);
+
+ if (match) {
+ const configBody = match[1].trim();
+ let config;
+
+ try {
+ config = JSON.parse(configBody);
+ const htmlContent = rawHtml.replace(configPattern, '').trim();
+
+ return {
+ config,
+ htmlContent
+ };
+ } catch (error) {
+ console.info(`Ignoring comment ${configBody} as it does not appear to be JSON.`);
+ }
+ }
+
+ // If no config is found, return the original HTML with no config
+ return {
+ config: null,
+ htmlContent: rawHtml
+ };
+}
+
/**
* 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.
+ * It also creates an index file with all the examples and their images.
*/
function generateExamplesFolder() {
const examplesDocsFolder = path.join('docs', 'examples');
@@ -87,15 +123,43 @@ function generateExamplesFolder() {
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 styleSwitcherScript = fs.readFileSync(path.join('build', 'style-switcher.js'));
const indexArray = [] as HtmlDoc[];
+ // TODO: In which cases should we include the MapLibre Demo Tiles? These are only useful for very "zoomed out" maps.
+ const defaultMapStyles = {
+ americana: {name: 'Americana', styleUrl: 'https://americanamap.org/style.json'},
+ maptilerStreets: {name: 'MapTiler Streets', styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL'},
+ alidadeSmoothDark: {name: 'Stadia Maps Alidade Smooth Dark', styleUrl: 'https://tiles.stadiamaps.com/styles/alidade_smooth_dark.json'}
+ };
+
for (const file of files) {
const htmlFile = path.join(examplesFolder, file);
- let htmlContent = fs.readFileSync(htmlFile, 'utf-8');
+ let {config, htmlContent} = preprocessHTML(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('
', '').replace('', '').trim()!;
const description = htmlContentLines.find(l => l.includes('og:description'))?.replace(/.*content=\"(.*)\".*/, '$1')!;
+
+ const displayedHtmlContent = htmlContent;
+ // Decide whether we want to add the style switcher.
+ // Currently looks for the Americana style, but could be more sophisticated with some effort.
+ const injectStyleSwitcher = htmlContent.indexOf('https://americanamap.org/style.json') !== -1 || config?.availableMapStyles;
+
+ // If possible, inject a style switcher into the HTML.
+ // This will not show up in the copyable example code.
+ if (injectStyleSwitcher) {
+ const sentinel = '';
+ const lastIndex = htmlContent.lastIndexOf(sentinel);
+
+ if (lastIndex !== -1) {
+ const availableMapStyles = JSON.stringify(config?.availableMapStyles || defaultMapStyles);
+ const originalHead = htmlContent.substring(0, lastIndex);
+ const originalTail = htmlContent.substring(lastIndex);
+ htmlContent = `${originalHead}\nconst availableMapStyles = ${availableMapStyles};\n${styleSwitcherScript}\n${originalTail}`;
+ }
+ }
+
fs.writeFileSync(path.join(examplesDocsFolder, file), htmlContent);
const mdFileName = file.replace('.html', '.md');
indexArray.push({
@@ -103,7 +167,7 @@ function generateExamplesFolder() {
description,
mdFileName
});
- const exampleMarkdown = generateMarkdownForExample(title, description, file, htmlContent);
+ const exampleMarkdown = generateMarkdownForExample(title, description, file, displayedHtmlContent);
fs.writeFileSync(path.join(examplesDocsFolder, mdFileName), exampleMarkdown);
}
diff --git a/build/style-switcher.js b/build/style-switcher.js
new file mode 100644
index 0000000000..819404742b
--- /dev/null
+++ b/build/style-switcher.js
@@ -0,0 +1,115 @@
+// Style switcher for embedding into example pages.
+// Note that there are several uses of `window.parent` throughout this file.
+// This is because the code is executing from an example
+// that is embedded into the page via an iframe.
+// As these are served from the same origin, this is allowed by JavaScript.
+
+/**
+ * Gets a list of nodes whose text content includes the given string.
+ *
+ * @param searchText The text to look for in the element text node.
+ * @param root The root node to start traversing from.
+ * @returns A list of DOM nodes matching the search.
+ */
+function getNodesByTextContent(searchText, root = window.parent.document.body) {
+ const matchingNodes = [];
+
+ function traverse(node) {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ node.childNodes.forEach(traverse);
+ } else if (node.nodeType === Node.TEXT_NODE) {
+ if (node.nodeValue.includes(searchText)) {
+ matchingNodes.push(node);
+ }
+ }
+ }
+
+ traverse(root);
+
+ return matchingNodes.map(node => node.parentNode); // Return parent nodes of the matching text nodes
+}
+
+/**
+ * Gets the current map style slug from the query string.
+ * @returns {string}
+ */
+function getMapStyleQueryParam() {
+ const url = new URL(window.parent.location.href);
+ return url.searchParams.get('mapStyle');
+}
+
+/**
+ * Sets the map style slug in the browser's query string
+ * (ex: when the user selects a new style).
+ * @param styleKey
+ */
+function setMapStyleQueryParam(styleKey) {
+ const url = new URL(window.parent.location.href);
+ if (url.searchParams.get('mapStyle') !== styleKey) {
+ url.searchParams.set('mapStyle', styleKey);
+ // TODO: Observe URL changes ex: forward and back
+ // Manipulates the window history so that the page doesn't reload.
+ window.parent.history.pushState(null, '', url);
+ }
+}
+
+class StyleSwitcherControl {
+ constructor () {
+ this.el = document.createElement('div');
+ }
+
+ onAdd (_) {
+ this.el.className = 'maplibregl-ctrl';
+
+ const select = document.createElement('select');
+ select.oninput = (event) => {
+ const styleKey = event.target.value;
+ const style = availableMapStyles[styleKey];
+ this.setStyle(styleKey, style);
+ };
+
+ const mapStyleKey = getMapStyleQueryParam();
+
+ for (const key in availableMapStyles) {
+ if (availableMapStyles.hasOwnProperty(key)) {
+ const style = availableMapStyles[key];
+ let selected = '';
+
+ // As we go through the styles, look for it in the rendered example.
+ if (this.styleURLNode === undefined && getNodesByTextContent(style.styleUrl)) {
+ this.styleURLNode = getNodesByTextContent(style.styleUrl)[0];
+ }
+
+ if (key === mapStyleKey) {
+ selected = ' selected';
+ this.setStyle(key, style);
+ }
+
+ select.insertAdjacentHTML('beforeend', ``);
+ }
+ }
+
+ // Add the select to the element
+ this.el.append(select);
+
+ return this.el;
+ }
+
+ onRemove (_) {
+ // Remove all children
+ this.el.replaceChildren()
+ }
+
+ setStyle(styleKey, style) {
+ // Change the map style
+ map.setStyle(style.styleUrl)
+
+ // Update the example
+ this.styleURLNode.innerText = `'${style.styleUrl}'`;
+
+ // Update the URL
+ setMapStyleQueryParam(styleKey);
+ }
+}
+
+map.addControl(new StyleSwitcherControl(), 'top-left');
diff --git a/docs/index.md b/docs/index.md
index ca9ecdc1db..2f729ed11d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -34,7 +34,18 @@ This documentation is divided into several sections:
Each section describes classes or objects as well as their **properties**, **parameters**, **instance members**, and associated **events**. Many sections also include inline code examples and related resources.
-In the examples, we use vector tiles from our [Demo tiles repository](https://github.com/maplibre/demotiles) and from [MapTiler](https://maptiler.com). Get your own API key if you want to use MapTiler data in your project.
+In the examples, we use vector tiles from our [Demo tiles repository](https://github.com/maplibre/demotiles)
+and the following other providers (presented in alphabetical order).
+
+| | |
+|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Americana OSM](https://tile.ourmap.us/) | A community tile server run by (amazingly dedicated) volunteers. Consult their [tile usage policy](https://tile.ourmap.us/usage.html) for acceptable uses. |
+| [MapTiler](https://maptiler.com) | A commercial tile provider; requires an API key for use. |
+| [Stadia Maps](https://stadiamaps.com/) | A commercial tile provider; requires an API key or domain registration for use. |
+
+You can find a list of other tile providers on the
+[Awesome MapLibre](https://github.com/maplibre/awesome-maplibre?tab=readme-ov-file#maptile-providers) tile provider section,
+or on the [OSM Wiki](https://wiki.openstreetmap.org/wiki/Vector_tiles#Providers).
## NPM
diff --git a/test/examples/add-3d-model-babylon.html b/test/examples/add-3d-model-babylon.html
index 8f706cc286..dd3ce79f49 100644
--- a/test/examples/add-3d-model-babylon.html
+++ b/test/examples/add-3d-model-babylon.html
@@ -21,7 +21,7 @@
const map = (window.map = new maplibregl.Map({
container: 'map',
- style: 'https://api.maptiler.com/maps/basic/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
+ style: 'https://americanamap.org/style.json',
zoom: 18,
center: [148.9819, -35.3981],
pitch: 60,
diff --git a/test/examples/add-3d-model.html b/test/examples/add-3d-model.html
index 445c697a79..6f0cacd0d4 100644
--- a/test/examples/add-3d-model.html
+++ b/test/examples/add-3d-model.html
@@ -19,8 +19,7 @@