Skip to content

Commit

Permalink
First sketch of style switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthetechie committed Oct 3, 2024
1 parent d5d4f0d commit 614f1ad
Show file tree
Hide file tree
Showing 14 changed files with 221 additions and 31 deletions.
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 @@ 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 = /<!--([\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 @@ 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.

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

0 comments on commit 614f1ad

Please sign in to comment.