Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style Switcher for Examples #4813

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 67 additions & 3 deletions build/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,45 @@
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 = /<!--([\s\S]*?)-->/;
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();

Check failure

Code scanning / CodeQL

Incomplete multi-character sanitization High

This string may still contain
<!--
, which may cause an HTML element injection vulnerability.

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');
Expand All @@ -87,23 +123,51 @@
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.

Check failure on line 128 in build/generate-docs.ts

View workflow job for this annotation

GitHub Actions / Code Hygiene

Unexpected 'todo' comment: 'TODO: In which cases should we include...'
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('<title'))?.replace('<title>', '').replace('</title>', '').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 = '</script>';
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({
title,
description,
mdFileName
});
const exampleMarkdown = generateMarkdownForExample(title, description, file, htmlContent);
const exampleMarkdown = generateMarkdownForExample(title, description, file, displayedHtmlContent);
fs.writeFileSync(path.join(examplesDocsFolder, mdFileName), exampleMarkdown);
}

Expand Down
115 changes: 115 additions & 0 deletions build/style-switcher.js
Original file line number Diff line number Diff line change
@@ -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', `<option value="${key}"${selected}>${style.name}</option>`);
}
}

// 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');
13 changes: 12 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion test/examples/add-3d-model-babylon.html
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions test/examples/add-3d-model.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
<script>
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,
Expand Down Expand Up @@ -142,4 +141,4 @@
});
</script>
</body>
</html>
</html>
5 changes: 2 additions & 3 deletions test/examples/add-a-marker.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
style: 'https://americanamap.org/style.json',
center: [12.550343, 55.665957],
zoom: 8
});
Expand All @@ -29,4 +28,4 @@
.addTo(map);
</script>
</body>
</html>
</html>
7 changes: 3 additions & 4 deletions test/examples/add-image-animated.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL'
style: 'https://americanamap.org/style.json'
});

const size = 200;
Expand Down Expand Up @@ -92,7 +91,7 @@
}
};

map.on('load', () => {
map.on('styledata', () => {
map.addImage('pulsing-dot', pulsingDot, {pixelRatio: 2});

map.addSource('points', {
Expand Down Expand Up @@ -121,4 +120,4 @@
});
</script>
</body>
</html>
</html>
5 changes: 2 additions & 3 deletions test/examples/add-image-generated.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL'
style: 'https://americanamap.org/style.json'
});

map.on('load', () => {
Expand Down Expand Up @@ -65,4 +64,4 @@
});
</script>
</body>
</html>
</html>
5 changes: 2 additions & 3 deletions test/examples/add-image-missing-generated.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL'
style: 'https://americanamap.org/style.json'
});

map.on('styleimagemissing', (e) => {
Expand Down Expand Up @@ -100,4 +99,4 @@
});
</script>
</body>
</html>
</html>
5 changes: 2 additions & 3 deletions test/examples/add-image-stretchable.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
style: 'https://americanamap.org/style.json',
zoom: 0.1
});

Expand Down Expand Up @@ -153,4 +152,4 @@
});
</script>
</body>
</html>
</html>
5 changes: 2 additions & 3 deletions test/examples/add-image.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL'
style: 'https://americanamap.org/style.json'
});

map.on('load', async () => {
Expand Down Expand Up @@ -52,4 +51,4 @@
});
</script>
</body>
</html>
</html>
3 changes: 1 addition & 2 deletions test/examples/animate-a-line.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
<script>
const map = new maplibregl.Map({
container: 'map',
style:
'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
style: 'https://americanamap.org/style.json',
center: [0, 0],
zoom: 0.5
});
Expand Down
Loading
Loading