add addzone and update zone in map-adapter interface

This commit is contained in:
2025-09-29 14:55:14 -04:00
parent 67c94197e1
commit db3ec6cddd
7851 changed files with 833548 additions and 18 deletions
+12
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
*/`;
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');
+77
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>`);
+26
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
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
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
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`);
+438
View File
@@ -0,0 +1,438 @@
/*
* 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';
// 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
};
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
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));
}
+56
View File
@@ -0,0 +1,56 @@
# 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
## Bundeling 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 bundeling
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>
### Generate Release Nodes
`release-notes.js` Used to generate release notes when releasing a new version
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env node
import * as fs from 'fs';
import semver from 'semver';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
const templatedReleaseNotes = `https://github.com/maplibre/maplibre-gl-js
[Changes](https://github.com/maplibre/maplibre-gl-js/compare/v${previous.version}...v${latest.version}) since [MapLibre GL JS v${previous.version}](https://github.com/maplibre/maplibre-gl-js/releases/tag/v${previous.version}):
${latest.changelog}
${semver.prerelease(latest.version) ? 'Pre-release version' : ''}`;
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());
+29
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
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;
+73
View File
@@ -0,0 +1,73 @@
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 strip from '@rollup/plugin-strip';
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 && strip({
sourceMap: true,
functions: ['PerformanceUtils.*']
}),
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');
}
};