From 614f1ad22898e4de884da4f32bdd9b36db1426ed Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Fri, 4 Oct 2024 02:04:32 +0900 Subject: [PATCH] First sketch of style switcher --- README.md | 2 +- build/generate-docs.ts | 70 ++++++++++- build/style-switcher.js | 115 ++++++++++++++++++ docs/index.md | 13 +- test/examples/add-3d-model-babylon.html | 2 +- test/examples/add-3d-model.html | 5 +- test/examples/add-a-marker.html | 5 +- test/examples/add-image-animated.html | 7 +- test/examples/add-image-generated.html | 5 +- .../examples/add-image-missing-generated.html | 5 +- test/examples/add-image-stretchable.html | 5 +- test/examples/add-image.html | 5 +- test/examples/animate-a-line.html | 3 +- test/examples/satellite-map.html | 10 +- 14 files changed, 221 insertions(+), 31 deletions(-) create mode 100644 build/style-switcher.js 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 @@ - \ No newline at end of file + diff --git a/test/examples/add-a-marker.html b/test/examples/add-a-marker.html index 3f6845145c..1c09b5a109 100644 --- a/test/examples/add-a-marker.html +++ b/test/examples/add-a-marker.html @@ -18,8 +18,7 @@ - \ No newline at end of file + diff --git a/test/examples/add-image-animated.html b/test/examples/add-image-animated.html index 7b85b28a32..e0f1449dde 100644 --- a/test/examples/add-image-animated.html +++ b/test/examples/add-image-animated.html @@ -18,8 +18,7 @@ - \ No newline at end of file + diff --git a/test/examples/add-image-generated.html b/test/examples/add-image-generated.html index dfb1b6d12d..c0f9b7abb4 100644 --- a/test/examples/add-image-generated.html +++ b/test/examples/add-image-generated.html @@ -18,8 +18,7 @@ - \ No newline at end of file + diff --git a/test/examples/add-image-missing-generated.html b/test/examples/add-image-missing-generated.html index 3337083105..11ee307b1b 100644 --- a/test/examples/add-image-missing-generated.html +++ b/test/examples/add-image-missing-generated.html @@ -18,8 +18,7 @@ - \ No newline at end of file + diff --git a/test/examples/add-image-stretchable.html b/test/examples/add-image-stretchable.html index 2914c42a7b..c0beb3da83 100644 --- a/test/examples/add-image-stretchable.html +++ b/test/examples/add-image-stretchable.html @@ -18,8 +18,7 @@ - \ No newline at end of file + diff --git a/test/examples/add-image.html b/test/examples/add-image.html index 77b83fd741..7bd44919e8 100644 --- a/test/examples/add-image.html +++ b/test/examples/add-image.html @@ -18,8 +18,7 @@ - \ No newline at end of file + diff --git a/test/examples/animate-a-line.html b/test/examples/animate-a-line.html index 0796732e07..408f4403d1 100644 --- a/test/examples/animate-a-line.html +++ b/test/examples/animate-a-line.html @@ -33,8 +33,7 @@ - \ No newline at end of file +