Initial commit
This commit is contained in:
116
node_modules/@maplibre/maplibre-gl-style-spec/LICENSE.txt
generated
vendored
Normal file
116
node_modules/@maplibre/maplibre-gl-style-spec/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
Copyright (c) 2020, MapLibre contributors
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of MapLibre GL JS nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Contains code from mapbox-gl-js v1.13 and earlier
|
||||
|
||||
Version v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license
|
||||
|
||||
Copyright (c) 2020, Mapbox
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of Mapbox GL JS nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
Contains code from glfx.js
|
||||
|
||||
Copyright (C) 2011 by Evan Wallace
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Contains a portion of d3-color https://github.com/d3/d3-color
|
||||
|
||||
Copyright 2010-2016 Mike Bostock
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the author nor the names of contributors may be used to
|
||||
endorse or promote products derived from this software without specific prior
|
||||
written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
94
node_modules/@maplibre/maplibre-gl-style-spec/README.md
generated
vendored
Normal file
94
node_modules/@maplibre/maplibre-gl-style-spec/README.md
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/7ff2cda8-f564-4e70-a971-d34152f969f0#gh-light-mode-only" alt="MapLibre Logo" width="200">
|
||||
<img src="https://github.com/user-attachments/assets/cee8376b-9812-40ff-91c6-2d53f9581b83#gh-dark-mode-only" alt="MapLibre Logo" width="200">
|
||||
</p>
|
||||
|
||||
# MapLibre Style Specification & Utilities
|
||||
|
||||
[](https://npmjs.org/package/@maplibre/maplibre-gl-style-spec)
|
||||
[](LICENSE.txt) [](https://opensource.org/licenses/BSD-3-Clause) [](https://codecov.io/gh/maplibre/maplibre-style-spec)
|
||||
|
||||
This repository contains code and reference files that define the MapLibre style specification and provides some utilities for working with MapLibre styles.
|
||||
|
||||
The style specification is used in MapLibre GL JS and in MapLibre Native. Our long-term goal is to have feature parity between the web and the native libraries.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to contribute to the style specification, please open an issue with a design proposal. Once your design proposal has been accepted, you can open a pull request and implement your changes.
|
||||
|
||||
We aim to avoid breaking changes in the MapLibre style specification because it makes life easier for our users.
|
||||
|
||||
## Documentation
|
||||
|
||||
The [documentation](https://maplibre.org/maplibre-style-spec) of the style specification also lives in this repository.
|
||||
We use [Zensical](https://www.zensical.org/).
|
||||
|
||||
To work on the documentation locally, you need to have Docker installed and running.
|
||||
Start Zensical with
|
||||
|
||||
```bash
|
||||
npm run start-docs
|
||||
```
|
||||
|
||||
Most of the documentation is generated (from e.g. `v8.json`).
|
||||
In another terminal, run:
|
||||
|
||||
```bash
|
||||
WATCH=1 npm run generate-docs
|
||||
```
|
||||
|
||||
This will re-run the generation script when needed.
|
||||
|
||||
Note that generated files should not be checked in, and they are excluded in `.gitignore`.
|
||||
Make sure to keep this file up to date and ignore generated files while making sure static Markdown files are not ignored.
|
||||
|
||||
## NPM Package
|
||||
|
||||
|
||||
The MapLibre style specification and utilities are published as a separate npm
|
||||
package so that they can be installed without the bulk of GL JS.
|
||||
|
||||
```bash
|
||||
npm install @maplibre/maplibre-gl-style-spec
|
||||
```
|
||||
|
||||
## CLI Tools
|
||||
|
||||
If you install this package globally, you will have access to several CLI tools.
|
||||
|
||||
```bash
|
||||
npm install @maplibre/maplibre-gl-style-spec --global
|
||||
```
|
||||
|
||||
### `gl-style-migrate`
|
||||
|
||||
This repo contains scripts for migrating GL styles of any version to the latest version (currently v8).
|
||||
You can migrate a style like this:
|
||||
|
||||
```bash
|
||||
$ gl-style-migrate bright-v7.json > bright-v8.json
|
||||
```
|
||||
|
||||
To migrate a file in place, you can use the `sponge` utility from the `moreutils` package:
|
||||
|
||||
```bash
|
||||
$ brew install moreutils
|
||||
$ gl-style-migrate bright.json | sponge bright.json
|
||||
```
|
||||
|
||||
### `gl-style-format`
|
||||
|
||||
```bash
|
||||
$ gl-style-format style.json
|
||||
```
|
||||
|
||||
Will format the given style JSON to use standard indentation and sorted object keys.
|
||||
|
||||
### `gl-style-validate`
|
||||
|
||||
```bash
|
||||
$ gl-style-validate style.json
|
||||
```
|
||||
|
||||
Will validate the given style JSON and print errors to stdout.
|
||||
Provide a `--json` flag to get JSON output.
|
||||
22
node_modules/@maplibre/maplibre-gl-style-spec/bin/gl-style-format.ts
generated
vendored
Normal file
22
node_modules/@maplibre/maplibre-gl-style-spec/bin/gl-style-format.ts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import minimist from 'minimist';
|
||||
import {format} from '../src/format';
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
|
||||
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
|
||||
help();
|
||||
} else {
|
||||
console.log(format(JSON.parse(fs.readFileSync(argv._[0]).toString()), argv.space));
|
||||
}
|
||||
|
||||
function help() {
|
||||
console.log('usage:');
|
||||
console.log(' gl-style-format source.json > destination.json');
|
||||
console.log('');
|
||||
console.log('options:');
|
||||
console.log(' --space <num>');
|
||||
console.log(' Number of spaces in output (default "2")');
|
||||
console.log(' Pass "0" for minified output.');
|
||||
}
|
||||
18
node_modules/@maplibre/maplibre-gl-style-spec/bin/gl-style-migrate.ts
generated
vendored
Normal file
18
node_modules/@maplibre/maplibre-gl-style-spec/bin/gl-style-migrate.ts
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import minimist from 'minimist';
|
||||
import {format} from '../src/format';
|
||||
import {migrate} from '../src/migrate';
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
|
||||
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
|
||||
help();
|
||||
} else {
|
||||
console.log(format(migrate(JSON.parse(fs.readFileSync(argv._[0]).toString()))));
|
||||
}
|
||||
|
||||
function help() {
|
||||
console.log('usage:');
|
||||
console.log(' gl-style-migrate style-v7.json > style-v8.json');
|
||||
}
|
||||
44
node_modules/@maplibre/maplibre-gl-style-spec/bin/gl-style-validate.ts
generated
vendored
Normal file
44
node_modules/@maplibre/maplibre-gl-style-spec/bin/gl-style-validate.ts
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import minimist from 'minimist';
|
||||
import rw from 'rw';
|
||||
import {validateStyle as validate} from '../src/validate_style';
|
||||
|
||||
const argv = minimist(process.argv.slice(2), {
|
||||
boolean: 'json'
|
||||
});
|
||||
|
||||
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
|
||||
help();
|
||||
} else {
|
||||
let status = 0;
|
||||
|
||||
if (!argv._.length) {
|
||||
argv._.push('/dev/stdin');
|
||||
}
|
||||
|
||||
argv._.forEach((file) => {
|
||||
const errors = validate(rw.readFileSync(file, 'utf8'));
|
||||
if (errors.length) {
|
||||
if (argv.json) {
|
||||
process.stdout.write(JSON.stringify(errors, null, 2));
|
||||
} else {
|
||||
errors.forEach((e) => {
|
||||
console.log('%s:%d: %s', file, e.line, e.message);
|
||||
});
|
||||
}
|
||||
status = 1;
|
||||
}
|
||||
});
|
||||
|
||||
process.exit(status);
|
||||
}
|
||||
|
||||
function help() {
|
||||
console.log('usage:');
|
||||
console.log(' gl-style-validate file.json');
|
||||
console.log(' gl-style-validate < file.json');
|
||||
console.log('');
|
||||
console.log('options:');
|
||||
console.log('--json output errors as json');
|
||||
}
|
||||
591
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.cjs
generated
vendored
Normal file
591
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.cjs
generated
vendored
Normal file
@@ -0,0 +1,591 @@
|
||||
#!/usr/bin/env node
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('fs')) :
|
||||
typeof define === 'function' && define.amd ? define(['fs'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.fs));
|
||||
})(this, (function (fs) { 'use strict';
|
||||
|
||||
function getDefaultExportFromCjs (x) {
|
||||
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
||||
}
|
||||
|
||||
var minimist$1;
|
||||
var hasRequiredMinimist;
|
||||
|
||||
function requireMinimist () {
|
||||
if (hasRequiredMinimist) return minimist$1;
|
||||
hasRequiredMinimist = 1;
|
||||
|
||||
function hasKey(obj, keys) {
|
||||
var o = obj;
|
||||
keys.slice(0, -1).forEach(function (key) {
|
||||
o = o[key] || {};
|
||||
});
|
||||
|
||||
var key = keys[keys.length - 1];
|
||||
return key in o;
|
||||
}
|
||||
|
||||
function isNumber(x) {
|
||||
if (typeof x === 'number') { return true; }
|
||||
if ((/^0x[0-9a-f]+$/i).test(x)) { return true; }
|
||||
return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x);
|
||||
}
|
||||
|
||||
function isConstructorOrProto(obj, key) {
|
||||
return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__';
|
||||
}
|
||||
|
||||
minimist$1 = function (args, opts) {
|
||||
if (!opts) { opts = {}; }
|
||||
|
||||
var flags = {
|
||||
bools: {},
|
||||
strings: {},
|
||||
unknownFn: null,
|
||||
};
|
||||
|
||||
if (typeof opts.unknown === 'function') {
|
||||
flags.unknownFn = opts.unknown;
|
||||
}
|
||||
|
||||
if (typeof opts.boolean === 'boolean' && opts.boolean) {
|
||||
flags.allBools = true;
|
||||
} else {
|
||||
[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
|
||||
flags.bools[key] = true;
|
||||
});
|
||||
}
|
||||
|
||||
var aliases = {};
|
||||
|
||||
function aliasIsBoolean(key) {
|
||||
return aliases[key].some(function (x) {
|
||||
return flags.bools[x];
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(opts.alias || {}).forEach(function (key) {
|
||||
aliases[key] = [].concat(opts.alias[key]);
|
||||
aliases[key].forEach(function (x) {
|
||||
aliases[x] = [key].concat(aliases[key].filter(function (y) {
|
||||
return x !== y;
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
[].concat(opts.string).filter(Boolean).forEach(function (key) {
|
||||
flags.strings[key] = true;
|
||||
if (aliases[key]) {
|
||||
[].concat(aliases[key]).forEach(function (k) {
|
||||
flags.strings[k] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var defaults = opts.default || {};
|
||||
|
||||
var argv = { _: [] };
|
||||
|
||||
function argDefined(key, arg) {
|
||||
return (flags.allBools && (/^--[^=]+$/).test(arg))
|
||||
|| flags.strings[key]
|
||||
|| flags.bools[key]
|
||||
|| aliases[key];
|
||||
}
|
||||
|
||||
function setKey(obj, keys, value) {
|
||||
var o = obj;
|
||||
for (var i = 0; i < keys.length - 1; i++) {
|
||||
var key = keys[i];
|
||||
if (isConstructorOrProto(o, key)) { return; }
|
||||
if (o[key] === undefined) { o[key] = {}; }
|
||||
if (
|
||||
o[key] === Object.prototype
|
||||
|| o[key] === Number.prototype
|
||||
|| o[key] === String.prototype
|
||||
) {
|
||||
o[key] = {};
|
||||
}
|
||||
if (o[key] === Array.prototype) { o[key] = []; }
|
||||
o = o[key];
|
||||
}
|
||||
|
||||
var lastKey = keys[keys.length - 1];
|
||||
if (isConstructorOrProto(o, lastKey)) { return; }
|
||||
if (
|
||||
o === Object.prototype
|
||||
|| o === Number.prototype
|
||||
|| o === String.prototype
|
||||
) {
|
||||
o = {};
|
||||
}
|
||||
if (o === Array.prototype) { o = []; }
|
||||
if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') {
|
||||
o[lastKey] = value;
|
||||
} else if (Array.isArray(o[lastKey])) {
|
||||
o[lastKey].push(value);
|
||||
} else {
|
||||
o[lastKey] = [o[lastKey], value];
|
||||
}
|
||||
}
|
||||
|
||||
function setArg(key, val, arg) {
|
||||
if (arg && flags.unknownFn && !argDefined(key, arg)) {
|
||||
if (flags.unknownFn(arg) === false) { return; }
|
||||
}
|
||||
|
||||
var value = !flags.strings[key] && isNumber(val)
|
||||
? Number(val)
|
||||
: val;
|
||||
setKey(argv, key.split('.'), value);
|
||||
|
||||
(aliases[key] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(flags.bools).forEach(function (key) {
|
||||
setArg(key, defaults[key] === undefined ? false : defaults[key]);
|
||||
});
|
||||
|
||||
var notFlags = [];
|
||||
|
||||
if (args.indexOf('--') !== -1) {
|
||||
notFlags = args.slice(args.indexOf('--') + 1);
|
||||
args = args.slice(0, args.indexOf('--'));
|
||||
}
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
var key;
|
||||
var next;
|
||||
|
||||
if ((/^--.+=/).test(arg)) {
|
||||
// Using [\s\S] instead of . because js doesn't support the
|
||||
// 'dotall' regex modifier. See:
|
||||
// http://stackoverflow.com/a/1068308/13216
|
||||
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||
key = m[1];
|
||||
var value = m[2];
|
||||
if (flags.bools[key]) {
|
||||
value = value !== 'false';
|
||||
}
|
||||
setArg(key, value, arg);
|
||||
} else if ((/^--no-.+/).test(arg)) {
|
||||
key = arg.match(/^--no-(.+)/)[1];
|
||||
setArg(key, false, arg);
|
||||
} else if ((/^--.+/).test(arg)) {
|
||||
key = arg.match(/^--(.+)/)[1];
|
||||
next = args[i + 1];
|
||||
if (
|
||||
next !== undefined
|
||||
&& !(/^(-|--)[^-]/).test(next)
|
||||
&& !flags.bools[key]
|
||||
&& !flags.allBools
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)
|
||||
) {
|
||||
setArg(key, next, arg);
|
||||
i += 1;
|
||||
} else if ((/^(true|false)$/).test(next)) {
|
||||
setArg(key, next === 'true', arg);
|
||||
i += 1;
|
||||
} else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
} else if ((/^-[^-]+/).test(arg)) {
|
||||
var letters = arg.slice(1, -1).split('');
|
||||
|
||||
var broken = false;
|
||||
for (var j = 0; j < letters.length; j++) {
|
||||
next = arg.slice(j + 2);
|
||||
|
||||
if (next === '-') {
|
||||
setArg(letters[j], next, arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
|
||||
setArg(letters[j], next.slice(1), arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
(/[A-Za-z]/).test(letters[j])
|
||||
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
|
||||
) {
|
||||
setArg(letters[j], next, arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
||||
setArg(letters[j], arg.slice(j + 2), arg);
|
||||
broken = true;
|
||||
break;
|
||||
} else {
|
||||
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
|
||||
key = arg.slice(-1)[0];
|
||||
if (!broken && key !== '-') {
|
||||
if (
|
||||
args[i + 1]
|
||||
&& !(/^(-|--)[^-]/).test(args[i + 1])
|
||||
&& !flags.bools[key]
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)
|
||||
) {
|
||||
setArg(key, args[i + 1], arg);
|
||||
i += 1;
|
||||
} else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) {
|
||||
setArg(key, args[i + 1] === 'true', arg);
|
||||
i += 1;
|
||||
} else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
|
||||
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg));
|
||||
}
|
||||
if (opts.stopEarly) {
|
||||
argv._.push.apply(argv._, args.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(defaults).forEach(function (k) {
|
||||
if (!hasKey(argv, k.split('.'))) {
|
||||
setKey(argv, k.split('.'), defaults[k]);
|
||||
|
||||
(aliases[k] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), defaults[k]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (opts['--']) {
|
||||
argv['--'] = notFlags.slice();
|
||||
} else {
|
||||
notFlags.forEach(function (k) {
|
||||
argv._.push(k);
|
||||
});
|
||||
}
|
||||
|
||||
return argv;
|
||||
};
|
||||
return minimist$1;
|
||||
}
|
||||
|
||||
var minimistExports = requireMinimist();
|
||||
var minimist = /*@__PURE__*/getDefaultExportFromCjs(minimistExports);
|
||||
|
||||
var $root = {
|
||||
version: {
|
||||
required: true,
|
||||
type: "enum",
|
||||
values: [
|
||||
8
|
||||
]
|
||||
},
|
||||
name: {
|
||||
type: "string"
|
||||
},
|
||||
metadata: {
|
||||
type: "*"
|
||||
},
|
||||
center: {
|
||||
type: "array",
|
||||
value: "number",
|
||||
length: 2
|
||||
},
|
||||
centerAltitude: {
|
||||
type: "number"
|
||||
},
|
||||
zoom: {
|
||||
type: "number"
|
||||
},
|
||||
bearing: {
|
||||
type: "number",
|
||||
"default": 0,
|
||||
period: 360,
|
||||
units: "degrees"
|
||||
},
|
||||
pitch: {
|
||||
type: "number",
|
||||
"default": 0,
|
||||
units: "degrees"
|
||||
},
|
||||
roll: {
|
||||
type: "number",
|
||||
"default": 0,
|
||||
units: "degrees"
|
||||
},
|
||||
state: {
|
||||
type: "state",
|
||||
"default": {
|
||||
}
|
||||
},
|
||||
light: {
|
||||
type: "light"
|
||||
},
|
||||
sky: {
|
||||
type: "sky"
|
||||
},
|
||||
projection: {
|
||||
type: "projection"
|
||||
},
|
||||
terrain: {
|
||||
type: "terrain"
|
||||
},
|
||||
sources: {
|
||||
required: true,
|
||||
type: "sources"
|
||||
},
|
||||
sprite: {
|
||||
type: "sprite"
|
||||
},
|
||||
glyphs: {
|
||||
type: "string"
|
||||
},
|
||||
"font-faces": {
|
||||
type: "fontFaces"
|
||||
},
|
||||
transition: {
|
||||
type: "transition"
|
||||
},
|
||||
layers: {
|
||||
required: true,
|
||||
type: "array",
|
||||
value: "layer"
|
||||
}
|
||||
};
|
||||
var layer = {
|
||||
id: {
|
||||
type: "string",
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: "enum",
|
||||
values: {
|
||||
fill: {
|
||||
},
|
||||
line: {
|
||||
},
|
||||
symbol: {
|
||||
},
|
||||
circle: {
|
||||
},
|
||||
heatmap: {
|
||||
},
|
||||
"fill-extrusion": {
|
||||
},
|
||||
raster: {
|
||||
},
|
||||
hillshade: {
|
||||
},
|
||||
"color-relief": {
|
||||
},
|
||||
background: {
|
||||
}
|
||||
},
|
||||
required: true
|
||||
},
|
||||
metadata: {
|
||||
type: "*"
|
||||
},
|
||||
source: {
|
||||
type: "string"
|
||||
},
|
||||
"source-layer": {
|
||||
type: "string"
|
||||
},
|
||||
minzoom: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 24
|
||||
},
|
||||
maxzoom: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 24
|
||||
},
|
||||
filter: {
|
||||
type: "filter"
|
||||
},
|
||||
layout: {
|
||||
type: "layout"
|
||||
},
|
||||
paint: {
|
||||
type: "paint"
|
||||
}
|
||||
};
|
||||
var latest = {
|
||||
$root: $root,
|
||||
layer: layer};
|
||||
|
||||
// Note: This regex matches even invalid JSON strings, but since we’re
|
||||
// working on the output of `JSON.stringify` we know that only valid strings
|
||||
// are present (unless the user supplied a weird `options.indent` but in
|
||||
// that case we don’t care since the output would be invalid anyway).
|
||||
const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g;
|
||||
|
||||
function stringify(passedObj, options = {}) {
|
||||
const indent = JSON.stringify(
|
||||
[1],
|
||||
undefined,
|
||||
options.indent === undefined ? 2 : options.indent
|
||||
).slice(2, -3);
|
||||
|
||||
const maxLength =
|
||||
indent === ""
|
||||
? Infinity
|
||||
: options.maxLength === undefined
|
||||
? 80
|
||||
: options.maxLength;
|
||||
|
||||
let { replacer } = options;
|
||||
|
||||
return (function _stringify(obj, currentIndent, reserved) {
|
||||
if (obj && typeof obj.toJSON === "function") {
|
||||
obj = obj.toJSON();
|
||||
}
|
||||
|
||||
const string = JSON.stringify(obj, replacer);
|
||||
|
||||
if (string === undefined) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const length = maxLength - currentIndent.length - reserved;
|
||||
|
||||
if (string.length <= length) {
|
||||
const prettified = string.replace(
|
||||
stringOrChar,
|
||||
(match, stringLiteral) => {
|
||||
return stringLiteral || `${match} `;
|
||||
}
|
||||
);
|
||||
if (prettified.length <= length) {
|
||||
return prettified;
|
||||
}
|
||||
}
|
||||
|
||||
if (replacer != null) {
|
||||
obj = JSON.parse(string);
|
||||
replacer = undefined;
|
||||
}
|
||||
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
const nextIndent = currentIndent + indent;
|
||||
const items = [];
|
||||
let index = 0;
|
||||
let start;
|
||||
let end;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
start = "[";
|
||||
end = "]";
|
||||
const { length } = obj;
|
||||
for (; index < length; index++) {
|
||||
items.push(
|
||||
_stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||
|
||||
"null"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
start = "{";
|
||||
end = "}";
|
||||
const keys = Object.keys(obj);
|
||||
const { length } = keys;
|
||||
for (; index < length; index++) {
|
||||
const key = keys[index];
|
||||
const keyPart = `${JSON.stringify(key)}: `;
|
||||
const value = _stringify(
|
||||
obj[key],
|
||||
nextIndent,
|
||||
keyPart.length + (index === length - 1 ? 0 : 1)
|
||||
);
|
||||
if (value !== undefined) {
|
||||
items.push(keyPart + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
return [start, indent + items.join(`,\n${nextIndent}`), end].join(
|
||||
`\n${currentIndent}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return string;
|
||||
})(passedObj, "", 0);
|
||||
}
|
||||
|
||||
function sortKeysBy(obj, reference) {
|
||||
const result = {};
|
||||
for (const key in reference) {
|
||||
if (obj[key] !== undefined) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (result[key] === undefined) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Format a MapLibre Style. Returns a stringified style with its keys
|
||||
* sorted in the same order as the reference style.
|
||||
*
|
||||
* The optional `space` argument is passed to
|
||||
* [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
|
||||
* to generate formatted output.
|
||||
*
|
||||
* If `space` is unspecified, a default of `2` spaces will be used.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} style a MapLibre Style
|
||||
* @param {number} [space] space argument to pass to `JSON.stringify`
|
||||
* @returns {string} stringified formatted JSON
|
||||
* @example
|
||||
* var fs = require('fs');
|
||||
* var format = require('maplibre-gl-style-spec').format;
|
||||
* var style = fs.readFileSync('./source.json', 'utf8');
|
||||
* fs.writeFileSync('./dest.json', format(style));
|
||||
* fs.writeFileSync('./dest.min.json', format(style, 0));
|
||||
*/
|
||||
function format(style, space = 2) {
|
||||
style = sortKeysBy(style, latest.$root);
|
||||
if (style.layers) {
|
||||
style.layers = style.layers.map((layer) => sortKeysBy(layer, latest.layer));
|
||||
}
|
||||
return stringify(style, { indent: space });
|
||||
}
|
||||
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
|
||||
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
|
||||
help();
|
||||
} else {
|
||||
console.log(format(JSON.parse(fs.readFileSync(argv._[0]).toString()), argv.space));
|
||||
}
|
||||
|
||||
function help() {
|
||||
console.log('usage:');
|
||||
console.log(' gl-style-format source.json > destination.json');
|
||||
console.log('');
|
||||
console.log('options:');
|
||||
console.log(' --space <num>');
|
||||
console.log(' Number of spaces in output (default "2")');
|
||||
console.log(' Pass "0" for minified output.');
|
||||
}
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=gl-style-format.cjs.map
|
||||
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.cjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
585
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.mjs
generated
vendored
Normal file
585
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.mjs
generated
vendored
Normal file
@@ -0,0 +1,585 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
|
||||
function getDefaultExportFromCjs (x) {
|
||||
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
||||
}
|
||||
|
||||
var minimist$1;
|
||||
var hasRequiredMinimist;
|
||||
|
||||
function requireMinimist () {
|
||||
if (hasRequiredMinimist) return minimist$1;
|
||||
hasRequiredMinimist = 1;
|
||||
|
||||
function hasKey(obj, keys) {
|
||||
var o = obj;
|
||||
keys.slice(0, -1).forEach(function (key) {
|
||||
o = o[key] || {};
|
||||
});
|
||||
|
||||
var key = keys[keys.length - 1];
|
||||
return key in o;
|
||||
}
|
||||
|
||||
function isNumber(x) {
|
||||
if (typeof x === 'number') { return true; }
|
||||
if ((/^0x[0-9a-f]+$/i).test(x)) { return true; }
|
||||
return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x);
|
||||
}
|
||||
|
||||
function isConstructorOrProto(obj, key) {
|
||||
return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__';
|
||||
}
|
||||
|
||||
minimist$1 = function (args, opts) {
|
||||
if (!opts) { opts = {}; }
|
||||
|
||||
var flags = {
|
||||
bools: {},
|
||||
strings: {},
|
||||
unknownFn: null,
|
||||
};
|
||||
|
||||
if (typeof opts.unknown === 'function') {
|
||||
flags.unknownFn = opts.unknown;
|
||||
}
|
||||
|
||||
if (typeof opts.boolean === 'boolean' && opts.boolean) {
|
||||
flags.allBools = true;
|
||||
} else {
|
||||
[].concat(opts.boolean).filter(Boolean).forEach(function (key) {
|
||||
flags.bools[key] = true;
|
||||
});
|
||||
}
|
||||
|
||||
var aliases = {};
|
||||
|
||||
function aliasIsBoolean(key) {
|
||||
return aliases[key].some(function (x) {
|
||||
return flags.bools[x];
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(opts.alias || {}).forEach(function (key) {
|
||||
aliases[key] = [].concat(opts.alias[key]);
|
||||
aliases[key].forEach(function (x) {
|
||||
aliases[x] = [key].concat(aliases[key].filter(function (y) {
|
||||
return x !== y;
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
[].concat(opts.string).filter(Boolean).forEach(function (key) {
|
||||
flags.strings[key] = true;
|
||||
if (aliases[key]) {
|
||||
[].concat(aliases[key]).forEach(function (k) {
|
||||
flags.strings[k] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var defaults = opts.default || {};
|
||||
|
||||
var argv = { _: [] };
|
||||
|
||||
function argDefined(key, arg) {
|
||||
return (flags.allBools && (/^--[^=]+$/).test(arg))
|
||||
|| flags.strings[key]
|
||||
|| flags.bools[key]
|
||||
|| aliases[key];
|
||||
}
|
||||
|
||||
function setKey(obj, keys, value) {
|
||||
var o = obj;
|
||||
for (var i = 0; i < keys.length - 1; i++) {
|
||||
var key = keys[i];
|
||||
if (isConstructorOrProto(o, key)) { return; }
|
||||
if (o[key] === undefined) { o[key] = {}; }
|
||||
if (
|
||||
o[key] === Object.prototype
|
||||
|| o[key] === Number.prototype
|
||||
|| o[key] === String.prototype
|
||||
) {
|
||||
o[key] = {};
|
||||
}
|
||||
if (o[key] === Array.prototype) { o[key] = []; }
|
||||
o = o[key];
|
||||
}
|
||||
|
||||
var lastKey = keys[keys.length - 1];
|
||||
if (isConstructorOrProto(o, lastKey)) { return; }
|
||||
if (
|
||||
o === Object.prototype
|
||||
|| o === Number.prototype
|
||||
|| o === String.prototype
|
||||
) {
|
||||
o = {};
|
||||
}
|
||||
if (o === Array.prototype) { o = []; }
|
||||
if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') {
|
||||
o[lastKey] = value;
|
||||
} else if (Array.isArray(o[lastKey])) {
|
||||
o[lastKey].push(value);
|
||||
} else {
|
||||
o[lastKey] = [o[lastKey], value];
|
||||
}
|
||||
}
|
||||
|
||||
function setArg(key, val, arg) {
|
||||
if (arg && flags.unknownFn && !argDefined(key, arg)) {
|
||||
if (flags.unknownFn(arg) === false) { return; }
|
||||
}
|
||||
|
||||
var value = !flags.strings[key] && isNumber(val)
|
||||
? Number(val)
|
||||
: val;
|
||||
setKey(argv, key.split('.'), value);
|
||||
|
||||
(aliases[key] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), value);
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(flags.bools).forEach(function (key) {
|
||||
setArg(key, defaults[key] === undefined ? false : defaults[key]);
|
||||
});
|
||||
|
||||
var notFlags = [];
|
||||
|
||||
if (args.indexOf('--') !== -1) {
|
||||
notFlags = args.slice(args.indexOf('--') + 1);
|
||||
args = args.slice(0, args.indexOf('--'));
|
||||
}
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var arg = args[i];
|
||||
var key;
|
||||
var next;
|
||||
|
||||
if ((/^--.+=/).test(arg)) {
|
||||
// Using [\s\S] instead of . because js doesn't support the
|
||||
// 'dotall' regex modifier. See:
|
||||
// http://stackoverflow.com/a/1068308/13216
|
||||
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||
key = m[1];
|
||||
var value = m[2];
|
||||
if (flags.bools[key]) {
|
||||
value = value !== 'false';
|
||||
}
|
||||
setArg(key, value, arg);
|
||||
} else if ((/^--no-.+/).test(arg)) {
|
||||
key = arg.match(/^--no-(.+)/)[1];
|
||||
setArg(key, false, arg);
|
||||
} else if ((/^--.+/).test(arg)) {
|
||||
key = arg.match(/^--(.+)/)[1];
|
||||
next = args[i + 1];
|
||||
if (
|
||||
next !== undefined
|
||||
&& !(/^(-|--)[^-]/).test(next)
|
||||
&& !flags.bools[key]
|
||||
&& !flags.allBools
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)
|
||||
) {
|
||||
setArg(key, next, arg);
|
||||
i += 1;
|
||||
} else if ((/^(true|false)$/).test(next)) {
|
||||
setArg(key, next === 'true', arg);
|
||||
i += 1;
|
||||
} else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
} else if ((/^-[^-]+/).test(arg)) {
|
||||
var letters = arg.slice(1, -1).split('');
|
||||
|
||||
var broken = false;
|
||||
for (var j = 0; j < letters.length; j++) {
|
||||
next = arg.slice(j + 2);
|
||||
|
||||
if (next === '-') {
|
||||
setArg(letters[j], next, arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') {
|
||||
setArg(letters[j], next.slice(1), arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
(/[A-Za-z]/).test(letters[j])
|
||||
&& (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next)
|
||||
) {
|
||||
setArg(letters[j], next, arg);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
||||
setArg(letters[j], arg.slice(j + 2), arg);
|
||||
broken = true;
|
||||
break;
|
||||
} else {
|
||||
setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
|
||||
key = arg.slice(-1)[0];
|
||||
if (!broken && key !== '-') {
|
||||
if (
|
||||
args[i + 1]
|
||||
&& !(/^(-|--)[^-]/).test(args[i + 1])
|
||||
&& !flags.bools[key]
|
||||
&& (aliases[key] ? !aliasIsBoolean(key) : true)
|
||||
) {
|
||||
setArg(key, args[i + 1], arg);
|
||||
i += 1;
|
||||
} else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) {
|
||||
setArg(key, args[i + 1] === 'true', arg);
|
||||
i += 1;
|
||||
} else {
|
||||
setArg(key, flags.strings[key] ? '' : true, arg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
|
||||
argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg));
|
||||
}
|
||||
if (opts.stopEarly) {
|
||||
argv._.push.apply(argv._, args.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(defaults).forEach(function (k) {
|
||||
if (!hasKey(argv, k.split('.'))) {
|
||||
setKey(argv, k.split('.'), defaults[k]);
|
||||
|
||||
(aliases[k] || []).forEach(function (x) {
|
||||
setKey(argv, x.split('.'), defaults[k]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (opts['--']) {
|
||||
argv['--'] = notFlags.slice();
|
||||
} else {
|
||||
notFlags.forEach(function (k) {
|
||||
argv._.push(k);
|
||||
});
|
||||
}
|
||||
|
||||
return argv;
|
||||
};
|
||||
return minimist$1;
|
||||
}
|
||||
|
||||
var minimistExports = requireMinimist();
|
||||
var minimist = /*@__PURE__*/getDefaultExportFromCjs(minimistExports);
|
||||
|
||||
var $root = {
|
||||
version: {
|
||||
required: true,
|
||||
type: "enum",
|
||||
values: [
|
||||
8
|
||||
]
|
||||
},
|
||||
name: {
|
||||
type: "string"
|
||||
},
|
||||
metadata: {
|
||||
type: "*"
|
||||
},
|
||||
center: {
|
||||
type: "array",
|
||||
value: "number",
|
||||
length: 2
|
||||
},
|
||||
centerAltitude: {
|
||||
type: "number"
|
||||
},
|
||||
zoom: {
|
||||
type: "number"
|
||||
},
|
||||
bearing: {
|
||||
type: "number",
|
||||
"default": 0,
|
||||
period: 360,
|
||||
units: "degrees"
|
||||
},
|
||||
pitch: {
|
||||
type: "number",
|
||||
"default": 0,
|
||||
units: "degrees"
|
||||
},
|
||||
roll: {
|
||||
type: "number",
|
||||
"default": 0,
|
||||
units: "degrees"
|
||||
},
|
||||
state: {
|
||||
type: "state",
|
||||
"default": {
|
||||
}
|
||||
},
|
||||
light: {
|
||||
type: "light"
|
||||
},
|
||||
sky: {
|
||||
type: "sky"
|
||||
},
|
||||
projection: {
|
||||
type: "projection"
|
||||
},
|
||||
terrain: {
|
||||
type: "terrain"
|
||||
},
|
||||
sources: {
|
||||
required: true,
|
||||
type: "sources"
|
||||
},
|
||||
sprite: {
|
||||
type: "sprite"
|
||||
},
|
||||
glyphs: {
|
||||
type: "string"
|
||||
},
|
||||
"font-faces": {
|
||||
type: "fontFaces"
|
||||
},
|
||||
transition: {
|
||||
type: "transition"
|
||||
},
|
||||
layers: {
|
||||
required: true,
|
||||
type: "array",
|
||||
value: "layer"
|
||||
}
|
||||
};
|
||||
var layer = {
|
||||
id: {
|
||||
type: "string",
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: "enum",
|
||||
values: {
|
||||
fill: {
|
||||
},
|
||||
line: {
|
||||
},
|
||||
symbol: {
|
||||
},
|
||||
circle: {
|
||||
},
|
||||
heatmap: {
|
||||
},
|
||||
"fill-extrusion": {
|
||||
},
|
||||
raster: {
|
||||
},
|
||||
hillshade: {
|
||||
},
|
||||
"color-relief": {
|
||||
},
|
||||
background: {
|
||||
}
|
||||
},
|
||||
required: true
|
||||
},
|
||||
metadata: {
|
||||
type: "*"
|
||||
},
|
||||
source: {
|
||||
type: "string"
|
||||
},
|
||||
"source-layer": {
|
||||
type: "string"
|
||||
},
|
||||
minzoom: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 24
|
||||
},
|
||||
maxzoom: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
maximum: 24
|
||||
},
|
||||
filter: {
|
||||
type: "filter"
|
||||
},
|
||||
layout: {
|
||||
type: "layout"
|
||||
},
|
||||
paint: {
|
||||
type: "paint"
|
||||
}
|
||||
};
|
||||
var latest = {
|
||||
$root: $root,
|
||||
layer: layer};
|
||||
|
||||
// Note: This regex matches even invalid JSON strings, but since we’re
|
||||
// working on the output of `JSON.stringify` we know that only valid strings
|
||||
// are present (unless the user supplied a weird `options.indent` but in
|
||||
// that case we don’t care since the output would be invalid anyway).
|
||||
const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g;
|
||||
|
||||
function stringify(passedObj, options = {}) {
|
||||
const indent = JSON.stringify(
|
||||
[1],
|
||||
undefined,
|
||||
options.indent === undefined ? 2 : options.indent
|
||||
).slice(2, -3);
|
||||
|
||||
const maxLength =
|
||||
indent === ""
|
||||
? Infinity
|
||||
: options.maxLength === undefined
|
||||
? 80
|
||||
: options.maxLength;
|
||||
|
||||
let { replacer } = options;
|
||||
|
||||
return (function _stringify(obj, currentIndent, reserved) {
|
||||
if (obj && typeof obj.toJSON === "function") {
|
||||
obj = obj.toJSON();
|
||||
}
|
||||
|
||||
const string = JSON.stringify(obj, replacer);
|
||||
|
||||
if (string === undefined) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const length = maxLength - currentIndent.length - reserved;
|
||||
|
||||
if (string.length <= length) {
|
||||
const prettified = string.replace(
|
||||
stringOrChar,
|
||||
(match, stringLiteral) => {
|
||||
return stringLiteral || `${match} `;
|
||||
}
|
||||
);
|
||||
if (prettified.length <= length) {
|
||||
return prettified;
|
||||
}
|
||||
}
|
||||
|
||||
if (replacer != null) {
|
||||
obj = JSON.parse(string);
|
||||
replacer = undefined;
|
||||
}
|
||||
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
const nextIndent = currentIndent + indent;
|
||||
const items = [];
|
||||
let index = 0;
|
||||
let start;
|
||||
let end;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
start = "[";
|
||||
end = "]";
|
||||
const { length } = obj;
|
||||
for (; index < length; index++) {
|
||||
items.push(
|
||||
_stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||
|
||||
"null"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
start = "{";
|
||||
end = "}";
|
||||
const keys = Object.keys(obj);
|
||||
const { length } = keys;
|
||||
for (; index < length; index++) {
|
||||
const key = keys[index];
|
||||
const keyPart = `${JSON.stringify(key)}: `;
|
||||
const value = _stringify(
|
||||
obj[key],
|
||||
nextIndent,
|
||||
keyPart.length + (index === length - 1 ? 0 : 1)
|
||||
);
|
||||
if (value !== undefined) {
|
||||
items.push(keyPart + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
return [start, indent + items.join(`,\n${nextIndent}`), end].join(
|
||||
`\n${currentIndent}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return string;
|
||||
})(passedObj, "", 0);
|
||||
}
|
||||
|
||||
function sortKeysBy(obj, reference) {
|
||||
const result = {};
|
||||
for (const key in reference) {
|
||||
if (obj[key] !== undefined) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (result[key] === undefined) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Format a MapLibre Style. Returns a stringified style with its keys
|
||||
* sorted in the same order as the reference style.
|
||||
*
|
||||
* The optional `space` argument is passed to
|
||||
* [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
|
||||
* to generate formatted output.
|
||||
*
|
||||
* If `space` is unspecified, a default of `2` spaces will be used.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} style a MapLibre Style
|
||||
* @param {number} [space] space argument to pass to `JSON.stringify`
|
||||
* @returns {string} stringified formatted JSON
|
||||
* @example
|
||||
* var fs = require('fs');
|
||||
* var format = require('maplibre-gl-style-spec').format;
|
||||
* var style = fs.readFileSync('./source.json', 'utf8');
|
||||
* fs.writeFileSync('./dest.json', format(style));
|
||||
* fs.writeFileSync('./dest.min.json', format(style, 0));
|
||||
*/
|
||||
function format(style, space = 2) {
|
||||
style = sortKeysBy(style, latest.$root);
|
||||
if (style.layers) {
|
||||
style.layers = style.layers.map((layer) => sortKeysBy(layer, latest.layer));
|
||||
}
|
||||
return stringify(style, { indent: space });
|
||||
}
|
||||
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
|
||||
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
|
||||
help();
|
||||
} else {
|
||||
console.log(format(JSON.parse(fs.readFileSync(argv._[0]).toString()), argv.space));
|
||||
}
|
||||
|
||||
function help() {
|
||||
console.log('usage:');
|
||||
console.log(' gl-style-format source.json > destination.json');
|
||||
console.log('');
|
||||
console.log('options:');
|
||||
console.log(' --space <num>');
|
||||
console.log(' Number of spaces in output (default "2")');
|
||||
console.log(' Pass "0" for minified output.');
|
||||
}
|
||||
//# sourceMappingURL=gl-style-format.mjs.map
|
||||
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.mjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-format.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
9439
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.cjs
generated
vendored
Normal file
9439
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.cjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
9433
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.mjs
generated
vendored
Normal file
9433
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.mjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.mjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-migrate.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
11370
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.cjs
generated
vendored
Normal file
11370
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.cjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
11364
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.mjs
generated
vendored
Normal file
11364
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.mjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.mjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/gl-style-validate.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
12206
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.cjs
generated
vendored
Normal file
12206
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.cjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
12440
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.d.ts
generated
vendored
Normal file
12440
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
12144
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.mjs
generated
vendored
Normal file
12144
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.mjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.mjs.map
generated
vendored
Normal file
1
node_modules/@maplibre/maplibre-gl-style-spec/dist/index.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
8425
node_modules/@maplibre/maplibre-gl-style-spec/dist/latest.json
generated
vendored
Normal file
8425
node_modules/@maplibre/maplibre-gl-style-spec/dist/latest.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
95
node_modules/@maplibre/maplibre-gl-style-spec/package.json
generated
vendored
Normal file
95
node_modules/@maplibre/maplibre-gl-style-spec/package.json
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "@maplibre/maplibre-gl-style-spec",
|
||||
"description": "a specification for maplibre styles",
|
||||
"version": "24.8.1",
|
||||
"author": "MapLibre",
|
||||
"keywords": [
|
||||
"mapbox",
|
||||
"mapbox-gl",
|
||||
"mapbox-gl-js",
|
||||
"maplibre",
|
||||
"maplibre-gl",
|
||||
"maplibre-gl-js"
|
||||
],
|
||||
"license": "ISC",
|
||||
"homepage": "https://maplibre.org/maplibre-style-spec/",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "rollup --configPlugin @rollup/plugin-typescript -c rollup.config.ts && cp ./src/reference/v8.json ./dist/latest.json",
|
||||
"generate-style-spec": "node --no-warnings --loader ts-node/esm build/generate-style-spec.ts",
|
||||
"generate-typings": "dts-bundle-generator -o ./dist/index.d.ts ./src/index.ts",
|
||||
"generate-docs": "node ${WATCH+--watch} --no-warnings --loader ts-node/esm build/generate-docs.ts",
|
||||
"start-docs": "docker run --rm -v ${PWD}:/docs zensical/zensical serve --open",
|
||||
"docs": "npm run generate-docs && docker run --rm -v ${PWD}:/docs zensical/zensical build",
|
||||
"test": "vitest",
|
||||
"test-unit": "vitest run --config vitest.config.unit.ts",
|
||||
"test-unit-ci": "vitest run --config vitest.config.unit.ts --coverage",
|
||||
"test-integration": "vitest run --config vitest.config.integration.ts",
|
||||
"test-integration-ci": "vitest run --config vitest.config.integration.ts --coverage",
|
||||
"test-build": "vitest run --config vitest.config.build.ts",
|
||||
"test-build-ci": "vitest run --config vitest.config.build.ts --coverage",
|
||||
"test-watch-roots": "vitest --config vitest.config.unit.ts --watch",
|
||||
"compile": "tsc",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepare": "npm run generate-style-spec",
|
||||
"fmt": "oxfmt"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maplibre/maplibre-style-spec"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs",
|
||||
"gl-style-format": "dist/gl-style-format.mjs"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"bin"
|
||||
],
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"rw": "^1.3.3",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"devDependencies": {
|
||||
"oxfmt": "^0.42.0",
|
||||
"@rollup/plugin-commonjs": "^29.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
"@rollup/plugin-strip": "^3.0.4",
|
||||
"@rollup/plugin-terser": "^1.0.0",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
||||
"@typescript-eslint/parser": "^8.57.1",
|
||||
"@vitest/coverage-v8": "4.1.2",
|
||||
"@vitest/eslint-plugin": "^1.6.13",
|
||||
"@vitest/ui": "4.1.2",
|
||||
"dts-bundle-generator": "^9.5.1",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-plugin-jsdoc": "^62.8.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.4.0",
|
||||
"rollup": "^4.60.0",
|
||||
"rollup-plugin-preserve-shebang": "^1.0.1",
|
||||
"semver": "^7.7.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "4.1.2"
|
||||
}
|
||||
}
|
||||
52
node_modules/@maplibre/maplibre-gl-style-spec/src/deref.test.ts
generated
vendored
Normal file
52
node_modules/@maplibre/maplibre-gl-style-spec/src/deref.test.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import {derefLayers, type LayerWithRef} from './deref';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('deref', () => {
|
||||
test('derefs a ref layer which follows its parent', () => {
|
||||
expect(
|
||||
derefLayers([
|
||||
{
|
||||
id: 'parent',
|
||||
type: 'line'
|
||||
} as LayerWithRef,
|
||||
{
|
||||
id: 'child',
|
||||
ref: 'parent'
|
||||
} as LayerWithRef
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
id: 'parent',
|
||||
type: 'line'
|
||||
},
|
||||
{
|
||||
id: 'child',
|
||||
type: 'line'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('derefs a ref layer which precedes its parent', () => {
|
||||
expect(
|
||||
derefLayers([
|
||||
{
|
||||
id: 'child',
|
||||
ref: 'parent'
|
||||
} as LayerWithRef,
|
||||
{
|
||||
id: 'parent',
|
||||
type: 'line'
|
||||
} as LayerWithRef
|
||||
])
|
||||
).toEqual([
|
||||
{
|
||||
id: 'child',
|
||||
type: 'line'
|
||||
},
|
||||
{
|
||||
id: 'parent',
|
||||
type: 'line'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
49
node_modules/@maplibre/maplibre-gl-style-spec/src/deref.ts
generated
vendored
Normal file
49
node_modules/@maplibre/maplibre-gl-style-spec/src/deref.ts
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import {refProperties} from './util/ref_properties';
|
||||
import {LayerSpecification} from './types.g';
|
||||
|
||||
export type LayerWithRef = LayerSpecification & {ref?: string};
|
||||
|
||||
function deref(layer: LayerWithRef, parent: LayerSpecification): LayerSpecification {
|
||||
const result: Partial<LayerSpecification> = {};
|
||||
|
||||
for (const k in layer) {
|
||||
if (k !== 'ref') {
|
||||
result[k] = layer[k];
|
||||
}
|
||||
}
|
||||
|
||||
refProperties.forEach((k) => {
|
||||
if (k in parent) {
|
||||
result[k] = parent[k];
|
||||
}
|
||||
});
|
||||
|
||||
return result as LayerSpecification;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* The input is not modified. The output may contain references to portions
|
||||
* of the input.
|
||||
*
|
||||
* @param layers - array of layers, some of which may contain `ref` properties
|
||||
* whose value is the `id` of another property
|
||||
* @returns a new array where such layers have been augmented with the 'type', 'source', etc. properties
|
||||
* from the parent layer, and the `ref` property has been removed.
|
||||
*/
|
||||
export function derefLayers(layers: LayerWithRef[]): LayerSpecification[] {
|
||||
layers = layers.slice();
|
||||
|
||||
const map = Object.create(null);
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
map[layers[i].id] = layers[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < layers.length; i++) {
|
||||
if ('ref' in layers[i]) {
|
||||
layers[i] = deref(layers[i], map[layers[i].ref]);
|
||||
}
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
825
node_modules/@maplibre/maplibre-gl-style-spec/src/diff.test.ts
generated
vendored
Normal file
825
node_modules/@maplibre/maplibre-gl-style-spec/src/diff.test.ts
generated
vendored
Normal file
@@ -0,0 +1,825 @@
|
||||
import {diff} from './diff';
|
||||
import {StyleSpecification} from './types.g';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('diff', () => {
|
||||
test('layers id equal', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('version not equal', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
version: 7,
|
||||
layers: [{id: 'a'}]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
version: 8,
|
||||
layers: [{id: 'a'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setStyle', args: [{version: 8, layers: [{id: 'a'}]}]}]);
|
||||
});
|
||||
|
||||
test('add layer at the end', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a'}, {id: 'b'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'addLayer', args: [{id: 'b'}, undefined]}]);
|
||||
});
|
||||
|
||||
test('add layer at the beginning', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'b'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a'}, {id: 'b'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'addLayer', args: [{id: 'a'}, 'b']}]);
|
||||
});
|
||||
|
||||
test('remove layer', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a'}, {id: 'b', source: 'foo', nested: [1]}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'removeLayer', args: ['b']}]);
|
||||
});
|
||||
|
||||
test('remove and add layer', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a'}, {id: 'b'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'b'}, {id: 'a'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeLayer', args: ['a']},
|
||||
{command: 'addLayer', args: [{id: 'a'}, undefined]}
|
||||
]);
|
||||
});
|
||||
|
||||
test('set paint property', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', paint: {foo: 1}}]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', paint: {foo: 2}}]
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setPaintProperty', args: ['a', 'foo', 2, null]}]);
|
||||
});
|
||||
|
||||
test('set paint property with light', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', 'paint.light': {foo: 1}}]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', 'paint.light': {foo: 2}}]
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setPaintProperty', args: ['a', 'foo', 2, 'light']}]);
|
||||
});
|
||||
|
||||
test('set paint property with ramp', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', paint: {foo: {ramp: [1, 2]}}}]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', paint: {foo: {ramp: [1]}}}]
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setPaintProperty', args: ['a', 'foo', {ramp: [1]}, null]}]);
|
||||
});
|
||||
|
||||
test('set layout property', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', layout: {foo: 1}}]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', layout: {foo: 2}}]
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setLayoutProperty', args: ['a', 'foo', 2, null]}]);
|
||||
});
|
||||
|
||||
test('set filter', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', filter: ['==', 'foo', 'bar']}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', filter: ['==', 'foo', 'baz']}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setFilter', args: ['a', ['==', 'foo', 'baz']]}]);
|
||||
});
|
||||
|
||||
test('remove source', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {foo: 1}
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'removeSource', args: ['foo']}]);
|
||||
});
|
||||
|
||||
test('add source', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
sources: {foo: 1}
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'addSource', args: ['foo', 1]}]);
|
||||
});
|
||||
|
||||
test('set goejson source data', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []}
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {type: 'Point', coordinates: [10, 20]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
command: 'setGeoJSONSourceData',
|
||||
args: [
|
||||
'foo',
|
||||
{
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {type: 'Point', coordinates: [10, 20]}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('remove and add source', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []}
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []},
|
||||
cluster: true
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeSource', args: ['foo']},
|
||||
{
|
||||
command: 'addSource',
|
||||
args: [
|
||||
'foo',
|
||||
{
|
||||
type: 'geojson',
|
||||
cluster: true,
|
||||
data: {type: 'FeatureCollection', features: []}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('remove and add source with clusterRadius', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []},
|
||||
cluster: true
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []},
|
||||
cluster: true,
|
||||
clusterRadius: 100
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeSource', args: ['foo']},
|
||||
{
|
||||
command: 'addSource',
|
||||
args: [
|
||||
'foo',
|
||||
{
|
||||
type: 'geojson',
|
||||
cluster: true,
|
||||
clusterRadius: 100,
|
||||
data: {type: 'FeatureCollection', features: []}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('remove and add source without clusterRadius', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []},
|
||||
cluster: true,
|
||||
clusterRadius: 100
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {
|
||||
foo: {
|
||||
type: 'geojson',
|
||||
data: {type: 'FeatureCollection', features: []},
|
||||
cluster: true
|
||||
}
|
||||
}
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeSource', args: ['foo']},
|
||||
{
|
||||
command: 'addSource',
|
||||
args: [
|
||||
'foo',
|
||||
{
|
||||
type: 'geojson',
|
||||
cluster: true,
|
||||
data: {type: 'FeatureCollection', features: []}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('global metadata', () => {
|
||||
expect(
|
||||
diff(
|
||||
{} as StyleSpecification,
|
||||
{
|
||||
metadata: {'maplibre:author': 'nobody'}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('layer metadata', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', metadata: {'maplibre:group': 'Group Name'}}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', metadata: {'maplibre:group': 'Another Name'}}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('set state', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
state: {foo: 1}
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
state: {foo: 2}
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setGlobalState', args: [{foo: 2}]}]);
|
||||
});
|
||||
|
||||
test('set center', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
center: [0, 0]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
center: [1, 1]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setCenter', args: [[1, 1]]}]);
|
||||
});
|
||||
|
||||
test('set centerAltitude to undefined', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
centerAltitude: 1
|
||||
} as StyleSpecification,
|
||||
{} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setCenterAltitude', args: [undefined]}]);
|
||||
});
|
||||
|
||||
test('set centerAltitude', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
centerAltitude: 0
|
||||
} as StyleSpecification,
|
||||
{
|
||||
centerAltitude: 1
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setCenterAltitude', args: [1]}]);
|
||||
});
|
||||
|
||||
test('set zoom', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
zoom: 12
|
||||
} as StyleSpecification,
|
||||
{
|
||||
zoom: 15
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setZoom', args: [15]}]);
|
||||
});
|
||||
|
||||
test('set bearing', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
bearing: 0
|
||||
} as StyleSpecification,
|
||||
{
|
||||
bearing: 180
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setBearing', args: [180]}]);
|
||||
});
|
||||
|
||||
test('set pitch', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
pitch: 0
|
||||
} as StyleSpecification,
|
||||
{
|
||||
pitch: 1
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setPitch', args: [1]}]);
|
||||
});
|
||||
|
||||
test('set roll to undefined', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
roll: 1
|
||||
} as StyleSpecification,
|
||||
{} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setRoll', args: [undefined]}]);
|
||||
});
|
||||
|
||||
test('set roll', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
roll: 0
|
||||
} as StyleSpecification,
|
||||
{
|
||||
roll: 1
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setRoll', args: [1]}]);
|
||||
});
|
||||
|
||||
test('no changes in light', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
light: {
|
||||
anchor: 'map',
|
||||
color: 'white',
|
||||
position: [0, 1, 0],
|
||||
intensity: 1
|
||||
}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
light: {
|
||||
anchor: 'map',
|
||||
color: 'white',
|
||||
position: [0, 1, 0],
|
||||
intensity: 1
|
||||
}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('set light anchor', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
light: {anchor: 'map'}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
light: {anchor: 'viewport'}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setLight', args: [{anchor: 'viewport'}]}]);
|
||||
});
|
||||
|
||||
test('set light color', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
light: {color: 'white'}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
light: {color: 'red'}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setLight', args: [{color: 'red'}]}]);
|
||||
});
|
||||
|
||||
test('set light position', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
light: {position: [0, 1, 0]}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
light: {position: [1, 0, 0]}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setLight', args: [{position: [1, 0, 0]}]}]);
|
||||
});
|
||||
|
||||
test('set light intensity', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
light: {intensity: 1}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
light: {intensity: 10}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setLight', args: [{intensity: 10}]}]);
|
||||
});
|
||||
|
||||
test('set light anchor and color', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
light: {
|
||||
anchor: 'map',
|
||||
color: 'orange',
|
||||
position: [2, 80, 30],
|
||||
intensity: 1.0
|
||||
}
|
||||
} as StyleSpecification,
|
||||
{
|
||||
light: {
|
||||
anchor: 'map',
|
||||
color: 'red',
|
||||
position: [1, 40, 30],
|
||||
intensity: 1.0
|
||||
}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
command: 'setLight',
|
||||
args: [
|
||||
{
|
||||
anchor: 'map',
|
||||
color: 'red',
|
||||
position: [1, 40, 30],
|
||||
intensity: 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('add and remove layer on source change', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', source: 'source-one'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', source: 'source-two'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeLayer', args: ['a']},
|
||||
{command: 'addLayer', args: [{id: 'a', source: 'source-two'}, undefined]}
|
||||
]);
|
||||
});
|
||||
|
||||
test('add and remove layer on type change', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', type: 'fill'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', type: 'line'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeLayer', args: ['a']},
|
||||
{command: 'addLayer', args: [{id: 'a', type: 'line'}, undefined]}
|
||||
]);
|
||||
});
|
||||
|
||||
test('add and remove layer on source-layer change', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'a', source: 'a', 'source-layer': 'layer-one'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'a', source: 'a', 'source-layer': 'layer-two'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeLayer', args: ['a']},
|
||||
{
|
||||
command: 'addLayer',
|
||||
args: [{id: 'a', source: 'a', 'source-layer': 'layer-two'}, undefined]
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test('add and remove layers on different order and type', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
layers: [{id: 'b'}, {id: 'c'}, {id: 'a', type: 'fill'}]
|
||||
} as StyleSpecification,
|
||||
{
|
||||
layers: [{id: 'c'}, {id: 'a', type: 'line'}, {id: 'b'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeLayer', args: ['b']},
|
||||
{command: 'addLayer', args: [{id: 'b'}, undefined]},
|
||||
{command: 'removeLayer', args: ['a']},
|
||||
{command: 'addLayer', args: [{id: 'a', type: 'line'}, 'b']}
|
||||
]);
|
||||
});
|
||||
|
||||
test('add and remove layer and source on source data change', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {foo: {data: 1}, bar: {}},
|
||||
layers: [
|
||||
{id: 'a', source: 'bar'},
|
||||
{id: 'b', source: 'foo'},
|
||||
{id: 'c', source: 'bar'}
|
||||
]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {foo: {data: 2}, bar: {}},
|
||||
layers: [
|
||||
{id: 'a', source: 'bar'},
|
||||
{id: 'b', source: 'foo'},
|
||||
{id: 'c', source: 'bar'}
|
||||
]
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{command: 'removeLayer', args: ['b']},
|
||||
{command: 'removeSource', args: ['foo']},
|
||||
{command: 'addSource', args: ['foo', {data: 2}]},
|
||||
{command: 'addLayer', args: [{id: 'b', source: 'foo'}, 'c']}
|
||||
]);
|
||||
});
|
||||
|
||||
test('set transition', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sources: {foo: {data: 1}, bar: {}},
|
||||
layers: [{id: 'a', source: 'bar'}]
|
||||
} as any as StyleSpecification,
|
||||
{
|
||||
sources: {foo: {data: 1}, bar: {}},
|
||||
layers: [{id: 'a', source: 'bar'}],
|
||||
transition: 'transition'
|
||||
} as any as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setTransition', args: ['transition']}]);
|
||||
});
|
||||
|
||||
test('no sprite change', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sprite: 'a'
|
||||
} as StyleSpecification,
|
||||
{
|
||||
sprite: 'a'
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('set sprite', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sprite: 'a'
|
||||
} as StyleSpecification,
|
||||
{
|
||||
sprite: 'b'
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setSprite', args: ['b']}]);
|
||||
});
|
||||
|
||||
test('set sprite for multiple sprites', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
sprite: 'a'
|
||||
} as StyleSpecification,
|
||||
{
|
||||
sprite: [{id: 'default', url: 'b'}]
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setSprite', args: [[{id: 'default', url: 'b'}]]}]);
|
||||
});
|
||||
|
||||
test('no glyphs change', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
glyphs: 'a'
|
||||
} as StyleSpecification,
|
||||
{
|
||||
glyphs: 'a'
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('set glyphs', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
glyphs: 'a'
|
||||
} as StyleSpecification,
|
||||
{
|
||||
glyphs: 'b'
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setGlyphs', args: ['b']}]);
|
||||
});
|
||||
|
||||
test('remove terrain', () => {
|
||||
expect(
|
||||
diff(
|
||||
{
|
||||
terrain: {
|
||||
source: 'maplibre-dem',
|
||||
exaggeration: 1.5
|
||||
}
|
||||
} as StyleSpecification,
|
||||
{} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setTerrain', args: [undefined]}]);
|
||||
});
|
||||
|
||||
test('add terrain', () => {
|
||||
expect(
|
||||
diff(
|
||||
{} as StyleSpecification,
|
||||
{
|
||||
terrain: {
|
||||
source: 'maplibre-dem',
|
||||
exaggeration: 1.5
|
||||
}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setTerrain', args: [{source: 'maplibre-dem', exaggeration: 1.5}]}]);
|
||||
});
|
||||
|
||||
test('set sky', () => {
|
||||
expect(
|
||||
diff(
|
||||
{} as StyleSpecification,
|
||||
{
|
||||
sky: {
|
||||
'fog-color': 'green',
|
||||
'fog-ground-blend': 0.2
|
||||
}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([{command: 'setSky', args: [{'fog-color': 'green', 'fog-ground-blend': 0.2}]}]);
|
||||
});
|
||||
|
||||
test('set projection', () => {
|
||||
expect(
|
||||
diff(
|
||||
{} as StyleSpecification,
|
||||
{
|
||||
projection: {type: ['vertical-perspective', 'mercator', 0.5]}
|
||||
} as StyleSpecification
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
command: 'setProjection',
|
||||
args: [{type: ['vertical-perspective', 'mercator', 0.5]}]
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
485
node_modules/@maplibre/maplibre-gl-style-spec/src/diff.ts
generated
vendored
Normal file
485
node_modules/@maplibre/maplibre-gl-style-spec/src/diff.ts
generated
vendored
Normal file
@@ -0,0 +1,485 @@
|
||||
import {
|
||||
GeoJSONSourceSpecification,
|
||||
LayerSpecification,
|
||||
LightSpecification,
|
||||
ProjectionSpecification,
|
||||
SkySpecification,
|
||||
SourceSpecification,
|
||||
SpriteSpecification,
|
||||
StyleSpecification,
|
||||
TerrainSpecification,
|
||||
TransitionSpecification,
|
||||
StateSpecification
|
||||
} from './types.g';
|
||||
import {deepEqual} from './util/deep_equal';
|
||||
|
||||
/**
|
||||
* Operations that can be performed by the diff.
|
||||
* Below are the operations and their arguments, the arguments should be aligned with the style methods in maplibre-gl-js.
|
||||
*/
|
||||
export type DiffOperationsMap = {
|
||||
setStyle: [StyleSpecification];
|
||||
addLayer: [LayerSpecification, string | null];
|
||||
removeLayer: [string];
|
||||
setPaintProperty: [string, string, unknown, string | null];
|
||||
setLayoutProperty: [string, string, unknown, string | null];
|
||||
setFilter: [string, unknown];
|
||||
addSource: [string, SourceSpecification];
|
||||
removeSource: [string];
|
||||
setGeoJSONSourceData: [string, unknown];
|
||||
setLayerZoomRange: [string, number, number];
|
||||
setLayerProperty: [string, string, unknown];
|
||||
setCenter: [number[]];
|
||||
setCenterAltitude: [number];
|
||||
setZoom: [number];
|
||||
setBearing: [number];
|
||||
setPitch: [number];
|
||||
setRoll: [number];
|
||||
setSprite: [SpriteSpecification];
|
||||
setGlyphs: [string];
|
||||
setTransition: [TransitionSpecification];
|
||||
setLight: [LightSpecification];
|
||||
setTerrain: [TerrainSpecification];
|
||||
setSky: [SkySpecification];
|
||||
setProjection: [ProjectionSpecification];
|
||||
setGlobalState: [StateSpecification];
|
||||
};
|
||||
|
||||
export type DiffOperations = keyof DiffOperationsMap;
|
||||
|
||||
export type DiffCommand<T extends DiffOperations> = {
|
||||
command: T;
|
||||
args: DiffOperationsMap[T];
|
||||
};
|
||||
|
||||
/**
|
||||
* The main reason for this method is to allow type check when adding a command to the array.
|
||||
* @param commands - The commands array to add to
|
||||
* @param command - The command to add
|
||||
*/
|
||||
function addCommand<T extends DiffOperations>(
|
||||
commands: DiffCommand<DiffOperations>[],
|
||||
command: DiffCommand<T>
|
||||
) {
|
||||
commands.push(command);
|
||||
}
|
||||
|
||||
function addSource(
|
||||
sourceId: string,
|
||||
after: {[key: string]: SourceSpecification},
|
||||
commands: DiffCommand<DiffOperations>[]
|
||||
) {
|
||||
addCommand(commands, {command: 'addSource', args: [sourceId, after[sourceId]]});
|
||||
}
|
||||
|
||||
function removeSource(
|
||||
sourceId: string,
|
||||
commands: DiffCommand<DiffOperations>[],
|
||||
sourcesRemoved: {[key: string]: boolean}
|
||||
) {
|
||||
addCommand(commands, {command: 'removeSource', args: [sourceId]});
|
||||
sourcesRemoved[sourceId] = true;
|
||||
}
|
||||
|
||||
function updateSource(
|
||||
sourceId: string,
|
||||
after: {[key: string]: SourceSpecification},
|
||||
commands: DiffCommand<DiffOperations>[],
|
||||
sourcesRemoved: {[key: string]: boolean}
|
||||
) {
|
||||
removeSource(sourceId, commands, sourcesRemoved);
|
||||
addSource(sourceId, after, commands);
|
||||
}
|
||||
|
||||
function canUpdateGeoJSON(
|
||||
before: {[key: string]: SourceSpecification},
|
||||
after: {[key: string]: SourceSpecification},
|
||||
sourceId: string
|
||||
) {
|
||||
let prop;
|
||||
for (prop in before[sourceId]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(before[sourceId], prop)) continue;
|
||||
if (prop !== 'data' && !deepEqual(before[sourceId][prop], after[sourceId][prop])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (prop in after[sourceId]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(after[sourceId], prop)) continue;
|
||||
if (prop !== 'data' && !deepEqual(before[sourceId][prop], after[sourceId][prop])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function diffSources(
|
||||
before: {[key: string]: SourceSpecification},
|
||||
after: {[key: string]: SourceSpecification},
|
||||
commands: DiffCommand<DiffOperations>[],
|
||||
sourcesRemoved: {[key: string]: boolean}
|
||||
) {
|
||||
before = before || ({} as {[key: string]: SourceSpecification});
|
||||
after = after || ({} as {[key: string]: SourceSpecification});
|
||||
|
||||
let sourceId: string;
|
||||
|
||||
// look for sources to remove
|
||||
for (sourceId in before) {
|
||||
if (!Object.prototype.hasOwnProperty.call(before, sourceId)) continue;
|
||||
if (!Object.prototype.hasOwnProperty.call(after, sourceId)) {
|
||||
removeSource(sourceId, commands, sourcesRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
// look for sources to add/update
|
||||
for (sourceId in after) {
|
||||
if (!Object.prototype.hasOwnProperty.call(after, sourceId)) continue;
|
||||
if (!Object.prototype.hasOwnProperty.call(before, sourceId)) {
|
||||
addSource(sourceId, after, commands);
|
||||
} else if (!deepEqual(before[sourceId], after[sourceId])) {
|
||||
if (
|
||||
before[sourceId].type === 'geojson' &&
|
||||
after[sourceId].type === 'geojson' &&
|
||||
canUpdateGeoJSON(before, after, sourceId)
|
||||
) {
|
||||
addCommand(commands, {
|
||||
command: 'setGeoJSONSourceData',
|
||||
args: [sourceId, (after[sourceId] as GeoJSONSourceSpecification).data]
|
||||
});
|
||||
} else {
|
||||
// no update command, must remove then add
|
||||
updateSource(sourceId, after, commands, sourcesRemoved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function diffLayerPropertyChanges(
|
||||
before: LayerSpecification['layout'] | LayerSpecification['paint'],
|
||||
after: LayerSpecification['layout'] | LayerSpecification['paint'],
|
||||
commands: DiffCommand<DiffOperations>[],
|
||||
layerId: string,
|
||||
klass: string | null,
|
||||
command: 'setPaintProperty' | 'setLayoutProperty'
|
||||
) {
|
||||
before = before || ({} as LayerSpecification['layout'] | LayerSpecification['paint']);
|
||||
after = after || ({} as LayerSpecification['layout'] | LayerSpecification['paint']);
|
||||
|
||||
for (const prop in before) {
|
||||
if (!Object.prototype.hasOwnProperty.call(before, prop)) continue;
|
||||
if (!deepEqual(before[prop], after[prop])) {
|
||||
commands.push({command, args: [layerId, prop, after[prop], klass]});
|
||||
}
|
||||
}
|
||||
for (const prop in after) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(after, prop) ||
|
||||
Object.prototype.hasOwnProperty.call(before, prop)
|
||||
)
|
||||
continue;
|
||||
if (!deepEqual(before[prop], after[prop])) {
|
||||
commands.push({command, args: [layerId, prop, after[prop], klass]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pluckId(layer: LayerSpecification) {
|
||||
return layer.id;
|
||||
}
|
||||
function indexById(group: {[key: string]: LayerSpecification}, layer: LayerSpecification) {
|
||||
group[layer.id] = layer;
|
||||
return group;
|
||||
}
|
||||
|
||||
function diffLayers(
|
||||
before: LayerSpecification[],
|
||||
after: LayerSpecification[],
|
||||
commands: DiffCommand<DiffOperations>[]
|
||||
) {
|
||||
before = before || [];
|
||||
after = after || [];
|
||||
|
||||
// order of layers by id
|
||||
const beforeOrder = before.map(pluckId);
|
||||
const afterOrder = after.map(pluckId);
|
||||
|
||||
// index of layer by id
|
||||
const beforeIndex = before.reduce(indexById, {});
|
||||
const afterIndex = after.reduce(indexById, {});
|
||||
|
||||
// track order of layers as if they have been mutated
|
||||
const tracker = beforeOrder.slice();
|
||||
|
||||
// layers that have been added do not need to be diffed
|
||||
const clean = Object.create(null);
|
||||
|
||||
let layerId: string;
|
||||
let beforeLayer: LayerSpecification & {source?: string; filter?: unknown};
|
||||
let afterLayer: LayerSpecification & {source?: string; filter?: unknown};
|
||||
let insertBeforeLayerId: string;
|
||||
let prop: string;
|
||||
|
||||
// remove layers
|
||||
for (let i = 0, d = 0; i < beforeOrder.length; i++) {
|
||||
layerId = beforeOrder[i];
|
||||
if (!Object.prototype.hasOwnProperty.call(afterIndex, layerId)) {
|
||||
addCommand(commands, {command: 'removeLayer', args: [layerId]});
|
||||
tracker.splice(tracker.indexOf(layerId, d), 1);
|
||||
} else {
|
||||
// limit where in tracker we need to look for a match
|
||||
d++;
|
||||
}
|
||||
}
|
||||
|
||||
// add/reorder layers
|
||||
for (let i = 0, d = 0; i < afterOrder.length; i++) {
|
||||
// work backwards as insert is before an existing layer
|
||||
layerId = afterOrder[afterOrder.length - 1 - i];
|
||||
|
||||
if (tracker[tracker.length - 1 - i] === layerId) continue;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(beforeIndex, layerId)) {
|
||||
// remove the layer before we insert at the correct position
|
||||
addCommand(commands, {command: 'removeLayer', args: [layerId]});
|
||||
tracker.splice(tracker.lastIndexOf(layerId, tracker.length - d), 1);
|
||||
} else {
|
||||
// limit where in tracker we need to look for a match
|
||||
d++;
|
||||
}
|
||||
|
||||
// add layer at correct position
|
||||
insertBeforeLayerId = tracker[tracker.length - i];
|
||||
addCommand(commands, {
|
||||
command: 'addLayer',
|
||||
args: [afterIndex[layerId], insertBeforeLayerId]
|
||||
});
|
||||
tracker.splice(tracker.length - i, 0, layerId);
|
||||
clean[layerId] = true;
|
||||
}
|
||||
|
||||
// update layers
|
||||
for (let i = 0; i < afterOrder.length; i++) {
|
||||
layerId = afterOrder[i];
|
||||
beforeLayer = beforeIndex[layerId];
|
||||
afterLayer = afterIndex[layerId];
|
||||
|
||||
// no need to update if previously added (new or moved)
|
||||
if (clean[layerId] || deepEqual(beforeLayer, afterLayer)) continue;
|
||||
|
||||
// If source, source-layer, or type have changes, then remove the layer
|
||||
// and add it back 'from scratch'.
|
||||
if (
|
||||
!deepEqual(beforeLayer.source, afterLayer.source) ||
|
||||
!deepEqual(beforeLayer['source-layer'], afterLayer['source-layer']) ||
|
||||
!deepEqual(beforeLayer.type, afterLayer.type)
|
||||
) {
|
||||
addCommand(commands, {command: 'removeLayer', args: [layerId]});
|
||||
// we add the layer back at the same position it was already in, so
|
||||
// there's no need to update the `tracker`
|
||||
insertBeforeLayerId = tracker[tracker.lastIndexOf(layerId) + 1];
|
||||
addCommand(commands, {command: 'addLayer', args: [afterLayer, insertBeforeLayerId]});
|
||||
continue;
|
||||
}
|
||||
|
||||
// layout, paint, filter, minzoom, maxzoom
|
||||
diffLayerPropertyChanges(
|
||||
beforeLayer.layout,
|
||||
afterLayer.layout,
|
||||
commands,
|
||||
layerId,
|
||||
null,
|
||||
'setLayoutProperty'
|
||||
);
|
||||
diffLayerPropertyChanges(
|
||||
beforeLayer.paint,
|
||||
afterLayer.paint,
|
||||
commands,
|
||||
layerId,
|
||||
null,
|
||||
'setPaintProperty'
|
||||
);
|
||||
if (!deepEqual(beforeLayer.filter, afterLayer.filter)) {
|
||||
addCommand(commands, {command: 'setFilter', args: [layerId, afterLayer.filter]});
|
||||
}
|
||||
if (
|
||||
!deepEqual(beforeLayer.minzoom, afterLayer.minzoom) ||
|
||||
!deepEqual(beforeLayer.maxzoom, afterLayer.maxzoom)
|
||||
) {
|
||||
addCommand(commands, {
|
||||
command: 'setLayerZoomRange',
|
||||
args: [layerId, afterLayer.minzoom, afterLayer.maxzoom]
|
||||
});
|
||||
}
|
||||
|
||||
// handle all other layer props, including paint.*
|
||||
for (prop in beforeLayer) {
|
||||
if (!Object.prototype.hasOwnProperty.call(beforeLayer, prop)) continue;
|
||||
if (
|
||||
prop === 'layout' ||
|
||||
prop === 'paint' ||
|
||||
prop === 'filter' ||
|
||||
prop === 'metadata' ||
|
||||
prop === 'minzoom' ||
|
||||
prop === 'maxzoom'
|
||||
)
|
||||
continue;
|
||||
if (prop.indexOf('paint.') === 0) {
|
||||
diffLayerPropertyChanges(
|
||||
beforeLayer[prop],
|
||||
afterLayer[prop],
|
||||
commands,
|
||||
layerId,
|
||||
prop.slice(6),
|
||||
'setPaintProperty'
|
||||
);
|
||||
} else if (!deepEqual(beforeLayer[prop], afterLayer[prop])) {
|
||||
addCommand(commands, {
|
||||
command: 'setLayerProperty',
|
||||
args: [layerId, prop, afterLayer[prop]]
|
||||
});
|
||||
}
|
||||
}
|
||||
for (prop in afterLayer) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(afterLayer, prop) ||
|
||||
Object.prototype.hasOwnProperty.call(beforeLayer, prop)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
prop === 'layout' ||
|
||||
prop === 'paint' ||
|
||||
prop === 'filter' ||
|
||||
prop === 'metadata' ||
|
||||
prop === 'minzoom' ||
|
||||
prop === 'maxzoom'
|
||||
)
|
||||
continue;
|
||||
if (prop.indexOf('paint.') === 0) {
|
||||
diffLayerPropertyChanges(
|
||||
beforeLayer[prop],
|
||||
afterLayer[prop],
|
||||
commands,
|
||||
layerId,
|
||||
prop.slice(6),
|
||||
'setPaintProperty'
|
||||
);
|
||||
} else if (!deepEqual(beforeLayer[prop], afterLayer[prop])) {
|
||||
addCommand(commands, {
|
||||
command: 'setLayerProperty',
|
||||
args: [layerId, prop, afterLayer[prop]]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff two stylesheet
|
||||
*
|
||||
* Creates semanticly aware diffs that can easily be applied at runtime.
|
||||
* Operations produced by the diff closely resemble the maplibre-gl-js API. Any
|
||||
* error creating the diff will fall back to the 'setStyle' operation.
|
||||
*
|
||||
* Example diff:
|
||||
* [
|
||||
* { command: 'setConstant', args: ['@water', '#0000FF'] },
|
||||
* { command: 'setPaintProperty', args: ['background', 'background-color', 'black'] }
|
||||
* ]
|
||||
*
|
||||
* @private
|
||||
* @param {*} [before] stylesheet to compare from
|
||||
* @param {*} after stylesheet to compare to
|
||||
* @returns Array list of changes
|
||||
*/
|
||||
export function diff(
|
||||
before: StyleSpecification,
|
||||
after: StyleSpecification
|
||||
): DiffCommand<DiffOperations>[] {
|
||||
if (!before) return [{command: 'setStyle', args: [after]}];
|
||||
|
||||
let commands: DiffCommand<DiffOperations>[] = [];
|
||||
|
||||
try {
|
||||
// Handle changes to top-level properties
|
||||
if (!deepEqual(before.version, after.version)) {
|
||||
return [{command: 'setStyle', args: [after]}];
|
||||
}
|
||||
if (!deepEqual(before.center, after.center)) {
|
||||
commands.push({command: 'setCenter', args: [after.center]});
|
||||
}
|
||||
if (!deepEqual(before.state, after.state)) {
|
||||
commands.push({command: 'setGlobalState', args: [after.state]});
|
||||
}
|
||||
if (!deepEqual(before.centerAltitude, after.centerAltitude)) {
|
||||
commands.push({command: 'setCenterAltitude', args: [after.centerAltitude]});
|
||||
}
|
||||
if (!deepEqual(before.zoom, after.zoom)) {
|
||||
commands.push({command: 'setZoom', args: [after.zoom]});
|
||||
}
|
||||
if (!deepEqual(before.bearing, after.bearing)) {
|
||||
commands.push({command: 'setBearing', args: [after.bearing]});
|
||||
}
|
||||
if (!deepEqual(before.pitch, after.pitch)) {
|
||||
commands.push({command: 'setPitch', args: [after.pitch]});
|
||||
}
|
||||
if (!deepEqual(before.roll, after.roll)) {
|
||||
commands.push({command: 'setRoll', args: [after.roll]});
|
||||
}
|
||||
if (!deepEqual(before.sprite, after.sprite)) {
|
||||
commands.push({command: 'setSprite', args: [after.sprite]});
|
||||
}
|
||||
if (!deepEqual(before.glyphs, after.glyphs)) {
|
||||
commands.push({command: 'setGlyphs', args: [after.glyphs]});
|
||||
}
|
||||
if (!deepEqual(before.transition, after.transition)) {
|
||||
commands.push({command: 'setTransition', args: [after.transition]});
|
||||
}
|
||||
if (!deepEqual(before.light, after.light)) {
|
||||
commands.push({command: 'setLight', args: [after.light]});
|
||||
}
|
||||
if (!deepEqual(before.terrain, after.terrain)) {
|
||||
commands.push({command: 'setTerrain', args: [after.terrain]});
|
||||
}
|
||||
if (!deepEqual(before.sky, after.sky)) {
|
||||
commands.push({command: 'setSky', args: [after.sky]});
|
||||
}
|
||||
if (!deepEqual(before.projection, after.projection)) {
|
||||
commands.push({command: 'setProjection', args: [after.projection]});
|
||||
}
|
||||
|
||||
// Handle changes to `sources`
|
||||
// If a source is to be removed, we also--before the removeSource
|
||||
// command--need to remove all the style layers that depend on it.
|
||||
const sourcesRemoved = {};
|
||||
|
||||
// First collect the {add,remove}Source commands
|
||||
const removeOrAddSourceCommands = [];
|
||||
diffSources(before.sources, after.sources, removeOrAddSourceCommands, sourcesRemoved);
|
||||
|
||||
// Push a removeLayer command for each style layer that depends on a
|
||||
// source that's being removed.
|
||||
// Also, exclude any such layers them from the input to `diffLayers`
|
||||
// below, so that diffLayers produces the appropriate `addLayers`
|
||||
// command
|
||||
const beforeLayers = [];
|
||||
if (before.layers) {
|
||||
before.layers.forEach((layer) => {
|
||||
if ('source' in layer && sourcesRemoved[layer.source]) {
|
||||
commands.push({command: 'removeLayer', args: [layer.id]});
|
||||
} else {
|
||||
beforeLayers.push(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
commands = commands.concat(removeOrAddSourceCommands);
|
||||
|
||||
// Handle changes to `layers`
|
||||
diffLayers(beforeLayers, after.layers, commands);
|
||||
} catch (e) {
|
||||
// fall back to setStyle
|
||||
console.warn('Unable to compute style diff:', e);
|
||||
commands = [{command: 'setStyle', args: [after]}];
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
15
node_modules/@maplibre/maplibre-gl-style-spec/src/empty.test.ts
generated
vendored
Normal file
15
node_modules/@maplibre/maplibre-gl-style-spec/src/empty.test.ts
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import {emptyStyle} from './empty';
|
||||
import {validateStyleMin} from './validate_style.min';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('empty', () => {
|
||||
test('it generates something', () => {
|
||||
const style = emptyStyle();
|
||||
expect(style).toBeTruthy();
|
||||
});
|
||||
|
||||
test('generated empty style is a valid style', () => {
|
||||
const errors = validateStyleMin(emptyStyle());
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
30
node_modules/@maplibre/maplibre-gl-style-spec/src/empty.ts
generated
vendored
Normal file
30
node_modules/@maplibre/maplibre-gl-style-spec/src/empty.ts
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
import {latest} from './reference/latest';
|
||||
import {StyleSpecification} from './types.g';
|
||||
|
||||
export function emptyStyle(): StyleSpecification {
|
||||
const style = {};
|
||||
|
||||
const version = latest['$version'];
|
||||
for (const styleKey in latest['$root']) {
|
||||
const specification = latest['$root'][styleKey];
|
||||
|
||||
if (specification.required) {
|
||||
let value = null;
|
||||
if (styleKey === 'version') {
|
||||
value = version;
|
||||
} else {
|
||||
if (specification.type === 'array') {
|
||||
value = [];
|
||||
} else {
|
||||
value = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
style[styleKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style as StyleSpecification;
|
||||
}
|
||||
14
node_modules/@maplibre/maplibre-gl-style-spec/src/error/parsing_error.ts
generated
vendored
Normal file
14
node_modules/@maplibre/maplibre-gl-style-spec/src/error/parsing_error.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
|
||||
|
||||
export class ParsingError {
|
||||
message: string;
|
||||
error: Error;
|
||||
line: number;
|
||||
|
||||
constructor(error: Error) {
|
||||
this.error = error;
|
||||
this.message = error.message;
|
||||
const match = error.message.match(/line (\d+)/);
|
||||
this.line = match ? parseInt(match[1], 10) : 0;
|
||||
}
|
||||
}
|
||||
23
node_modules/@maplibre/maplibre-gl-style-spec/src/error/validation_error.ts
generated
vendored
Normal file
23
node_modules/@maplibre/maplibre-gl-style-spec/src/error/validation_error.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
|
||||
|
||||
export class ValidationError {
|
||||
message: string;
|
||||
identifier: string;
|
||||
line: number;
|
||||
|
||||
constructor(
|
||||
key: string,
|
||||
value: any & {
|
||||
__line__: number;
|
||||
},
|
||||
message: string,
|
||||
identifier?: string | null
|
||||
) {
|
||||
this.message = (key ? `${key}: ` : '') + message;
|
||||
if (identifier) this.identifier = identifier;
|
||||
|
||||
if (value !== null && value !== undefined && value.__line__) {
|
||||
this.line = value.__line__;
|
||||
}
|
||||
}
|
||||
}
|
||||
647
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/compound_expression.ts
generated
vendored
Normal file
647
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/compound_expression.ts
generated
vendored
Normal file
@@ -0,0 +1,647 @@
|
||||
import {
|
||||
typeToString,
|
||||
NumberType,
|
||||
StringType,
|
||||
BooleanType,
|
||||
ColorType,
|
||||
ObjectType,
|
||||
ValueType,
|
||||
ErrorType,
|
||||
CollatorType,
|
||||
array
|
||||
} from './types';
|
||||
|
||||
import {ParsingContext} from './parsing_context';
|
||||
import {EvaluationContext} from './evaluation_context';
|
||||
|
||||
import {expressions} from './definitions';
|
||||
import {CollatorExpression} from './definitions/collator';
|
||||
import {Within} from './definitions/within';
|
||||
import {Literal} from './definitions/literal';
|
||||
import {Assertion} from './definitions/assertion';
|
||||
import {Coercion} from './definitions/coercion';
|
||||
import {Var} from './definitions/var';
|
||||
import {Distance} from './definitions/distance';
|
||||
import {GlobalState} from './definitions/global_state';
|
||||
|
||||
import type {Expression, ExpressionRegistry} from './expression';
|
||||
import type {Value} from './values';
|
||||
import type {Type} from './types';
|
||||
|
||||
import {typeOf, validateRGBA, valueToString} from './values';
|
||||
import {RuntimeError} from './runtime_error';
|
||||
import {Color} from './types/color';
|
||||
|
||||
export type Varargs = {
|
||||
type: Type;
|
||||
};
|
||||
type Signature = Array<Type> | Varargs;
|
||||
type Evaluate = (b: EvaluationContext, a: Array<Expression>) => Value;
|
||||
|
||||
type Definition =
|
||||
| [Type, Signature, Evaluate]
|
||||
| {
|
||||
type: Type;
|
||||
overloads: Array<[Signature, Evaluate]>;
|
||||
};
|
||||
|
||||
export class CompoundExpression implements Expression {
|
||||
name: string;
|
||||
type: Type;
|
||||
_evaluate: Evaluate;
|
||||
args: Array<Expression>;
|
||||
|
||||
static definitions: {[_: string]: Definition};
|
||||
|
||||
constructor(name: string, type: Type, evaluate: Evaluate, args: Array<Expression>) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this._evaluate = evaluate;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
return this._evaluate(ctx, this.args);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
this.args.forEach(fn);
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
const op: string = args[0] as any;
|
||||
const definition = CompoundExpression.definitions[op];
|
||||
if (!definition) {
|
||||
return context.error(
|
||||
`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`,
|
||||
0
|
||||
) as null;
|
||||
}
|
||||
|
||||
// Now check argument types against each signature
|
||||
const type = Array.isArray(definition) ? definition[0] : definition.type;
|
||||
|
||||
const availableOverloads = Array.isArray(definition)
|
||||
? [[definition[1], definition[2]]]
|
||||
: definition.overloads;
|
||||
|
||||
const overloads = availableOverloads.filter(
|
||||
([signature]) =>
|
||||
!Array.isArray(signature) || // varags
|
||||
signature.length === args.length - 1 // correct param count
|
||||
);
|
||||
|
||||
let signatureContext: ParsingContext = null;
|
||||
|
||||
for (const [params, evaluate] of overloads) {
|
||||
// Use a fresh context for each attempted signature so that, if
|
||||
// we eventually succeed, we haven't polluted `context.errors`.
|
||||
signatureContext = new ParsingContext(
|
||||
context.registry,
|
||||
isExpressionConstant,
|
||||
context.path,
|
||||
null,
|
||||
context.scope
|
||||
);
|
||||
|
||||
// First parse all the args, potentially coercing to the
|
||||
// types expected by this overload.
|
||||
const parsedArgs: Array<Expression> = [];
|
||||
let argParseFailed = false;
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const expectedType = Array.isArray(params)
|
||||
? params[i - 1]
|
||||
: (params as Varargs).type;
|
||||
|
||||
const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType);
|
||||
if (!parsed) {
|
||||
argParseFailed = true;
|
||||
break;
|
||||
}
|
||||
parsedArgs.push(parsed);
|
||||
}
|
||||
if (argParseFailed) {
|
||||
// Couldn't coerce args of this overload to expected type, move
|
||||
// on to next one.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(params)) {
|
||||
if (params.length !== parsedArgs.length) {
|
||||
signatureContext.error(
|
||||
`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < parsedArgs.length; i++) {
|
||||
const expected = Array.isArray(params) ? params[i] : (params as Varargs).type;
|
||||
const arg = parsedArgs[i];
|
||||
signatureContext.concat(i + 1).checkSubtype(expected, arg.type);
|
||||
}
|
||||
|
||||
if (signatureContext.errors.length === 0) {
|
||||
return new CompoundExpression(op, type, evaluate as Evaluate, parsedArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (overloads.length === 1) {
|
||||
context.errors.push(...signatureContext.errors);
|
||||
} else {
|
||||
const expected = overloads.length ? overloads : availableOverloads;
|
||||
const signatures = expected
|
||||
.map(([params]) => stringifySignature(params as Signature))
|
||||
.join(' | ');
|
||||
|
||||
const actualTypes = [];
|
||||
// For error message, re-parse arguments without trying to
|
||||
// apply any coercions
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const parsed = context.parse(args[i], 1 + actualTypes.length);
|
||||
if (!parsed) return null;
|
||||
actualTypes.push(typeToString(parsed.type));
|
||||
}
|
||||
context.error(
|
||||
`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static register(registry: ExpressionRegistry, definitions: {[_: string]: Definition}) {
|
||||
CompoundExpression.definitions = definitions;
|
||||
for (const name in definitions) {
|
||||
registry[name] = CompoundExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rgba(ctx, [r, g, b, a]) {
|
||||
r = r.evaluate(ctx);
|
||||
g = g.evaluate(ctx);
|
||||
b = b.evaluate(ctx);
|
||||
const alpha = a ? a.evaluate(ctx) : 1;
|
||||
const error = validateRGBA(r, g, b, alpha);
|
||||
if (error) throw new RuntimeError(error);
|
||||
return new Color(r / 255, g / 255, b / 255, alpha, false);
|
||||
}
|
||||
|
||||
function has(key, obj) {
|
||||
return key in obj;
|
||||
}
|
||||
|
||||
function get(key, obj) {
|
||||
const v = obj[key];
|
||||
return typeof v === 'undefined' ? null : v;
|
||||
}
|
||||
|
||||
function binarySearch(v, a, i, j) {
|
||||
while (i <= j) {
|
||||
const m = (i + j) >> 1;
|
||||
if (a[m] === v) return true;
|
||||
if (a[m] > v) j = m - 1;
|
||||
else i = m + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function varargs(type: Type): Varargs {
|
||||
return {type};
|
||||
}
|
||||
|
||||
CompoundExpression.register(expressions, {
|
||||
error: [
|
||||
ErrorType,
|
||||
[StringType],
|
||||
(ctx, [v]) => {
|
||||
throw new RuntimeError(v.evaluate(ctx));
|
||||
}
|
||||
],
|
||||
typeof: [StringType, [ValueType], (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))],
|
||||
'to-rgba': [
|
||||
array(NumberType, 4),
|
||||
[ColorType],
|
||||
(ctx, [v]) => {
|
||||
const [r, g, b, a] = v.evaluate(ctx).rgb;
|
||||
return [r * 255, g * 255, b * 255, a];
|
||||
}
|
||||
],
|
||||
rgb: [ColorType, [NumberType, NumberType, NumberType], rgba],
|
||||
rgba: [ColorType, [NumberType, NumberType, NumberType, NumberType], rgba],
|
||||
has: {
|
||||
type: BooleanType,
|
||||
overloads: [
|
||||
[[StringType], (ctx, [key]) => has(key.evaluate(ctx), ctx.properties())],
|
||||
[
|
||||
[StringType, ObjectType],
|
||||
(ctx, [key, obj]) => has(key.evaluate(ctx), obj.evaluate(ctx))
|
||||
]
|
||||
]
|
||||
},
|
||||
get: {
|
||||
type: ValueType,
|
||||
overloads: [
|
||||
[[StringType], (ctx, [key]) => get(key.evaluate(ctx), ctx.properties())],
|
||||
[
|
||||
[StringType, ObjectType],
|
||||
(ctx, [key, obj]) => get(key.evaluate(ctx), obj.evaluate(ctx))
|
||||
]
|
||||
]
|
||||
},
|
||||
'feature-state': [
|
||||
ValueType,
|
||||
[StringType],
|
||||
(ctx, [key]) => get(key.evaluate(ctx), ctx.featureState || {})
|
||||
],
|
||||
properties: [ObjectType, [], (ctx) => ctx.properties()],
|
||||
'geometry-type': [StringType, [], (ctx) => ctx.geometryType()],
|
||||
id: [ValueType, [], (ctx) => ctx.id()],
|
||||
zoom: [NumberType, [], (ctx) => ctx.globals.zoom],
|
||||
'heatmap-density': [NumberType, [], (ctx) => ctx.globals.heatmapDensity || 0],
|
||||
elevation: [NumberType, [], (ctx) => ctx.globals.elevation || 0],
|
||||
'line-progress': [NumberType, [], (ctx) => ctx.globals.lineProgress || 0],
|
||||
accumulated: [
|
||||
ValueType,
|
||||
[],
|
||||
(ctx) => (ctx.globals.accumulated === undefined ? null : ctx.globals.accumulated)
|
||||
],
|
||||
'+': [
|
||||
NumberType,
|
||||
varargs(NumberType),
|
||||
(ctx, args) => {
|
||||
let result = 0;
|
||||
for (const arg of args) {
|
||||
result += arg.evaluate(ctx);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
],
|
||||
'*': [
|
||||
NumberType,
|
||||
varargs(NumberType),
|
||||
(ctx, args) => {
|
||||
let result = 1;
|
||||
for (const arg of args) {
|
||||
result *= arg.evaluate(ctx);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
],
|
||||
'-': {
|
||||
type: NumberType,
|
||||
overloads: [
|
||||
[[NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) - b.evaluate(ctx)],
|
||||
[[NumberType], (ctx, [a]) => -a.evaluate(ctx)]
|
||||
]
|
||||
},
|
||||
'/': [NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) / b.evaluate(ctx)],
|
||||
'%': [NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) % b.evaluate(ctx)],
|
||||
ln2: [NumberType, [], () => Math.LN2],
|
||||
pi: [NumberType, [], () => Math.PI],
|
||||
e: [NumberType, [], () => Math.E],
|
||||
'^': [
|
||||
NumberType,
|
||||
[NumberType, NumberType],
|
||||
(ctx, [b, e]) => Math.pow(b.evaluate(ctx), e.evaluate(ctx))
|
||||
],
|
||||
sqrt: [NumberType, [NumberType], (ctx, [x]) => Math.sqrt(x.evaluate(ctx))],
|
||||
log10: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN10],
|
||||
ln: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx))],
|
||||
log2: [NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN2],
|
||||
sin: [NumberType, [NumberType], (ctx, [n]) => Math.sin(n.evaluate(ctx))],
|
||||
cos: [NumberType, [NumberType], (ctx, [n]) => Math.cos(n.evaluate(ctx))],
|
||||
tan: [NumberType, [NumberType], (ctx, [n]) => Math.tan(n.evaluate(ctx))],
|
||||
asin: [NumberType, [NumberType], (ctx, [n]) => Math.asin(n.evaluate(ctx))],
|
||||
acos: [NumberType, [NumberType], (ctx, [n]) => Math.acos(n.evaluate(ctx))],
|
||||
atan: [NumberType, [NumberType], (ctx, [n]) => Math.atan(n.evaluate(ctx))],
|
||||
min: [
|
||||
NumberType,
|
||||
varargs(NumberType),
|
||||
(ctx, args) => Math.min(...args.map((arg) => arg.evaluate(ctx)))
|
||||
],
|
||||
max: [
|
||||
NumberType,
|
||||
varargs(NumberType),
|
||||
(ctx, args) => Math.max(...args.map((arg) => arg.evaluate(ctx)))
|
||||
],
|
||||
abs: [NumberType, [NumberType], (ctx, [n]) => Math.abs(n.evaluate(ctx))],
|
||||
round: [
|
||||
NumberType,
|
||||
[NumberType],
|
||||
(ctx, [n]) => {
|
||||
const v = n.evaluate(ctx);
|
||||
// Javascript's Math.round() rounds towards +Infinity for halfway
|
||||
// values, even when they're negative. It's more common to round
|
||||
// away from 0 (e.g., this is what python and C++ do)
|
||||
return v < 0 ? -Math.round(-v) : Math.round(v);
|
||||
}
|
||||
],
|
||||
floor: [NumberType, [NumberType], (ctx, [n]) => Math.floor(n.evaluate(ctx))],
|
||||
ceil: [NumberType, [NumberType], (ctx, [n]) => Math.ceil(n.evaluate(ctx))],
|
||||
'filter-==': [
|
||||
BooleanType,
|
||||
[StringType, ValueType],
|
||||
(ctx, [k, v]) => ctx.properties()[(k as any).value] === (v as any).value
|
||||
],
|
||||
'filter-id-==': [BooleanType, [ValueType], (ctx, [v]) => ctx.id() === (v as any).value],
|
||||
'filter-type-==': [
|
||||
BooleanType,
|
||||
[StringType],
|
||||
(ctx, [v]) => ctx.geometryType() === (v as any).value
|
||||
],
|
||||
'filter-<': [
|
||||
BooleanType,
|
||||
[StringType, ValueType],
|
||||
(ctx, [k, v]) => {
|
||||
const a = ctx.properties()[(k as any).value];
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a < b;
|
||||
}
|
||||
],
|
||||
'filter-id-<': [
|
||||
BooleanType,
|
||||
[ValueType],
|
||||
(ctx, [v]) => {
|
||||
const a = ctx.id();
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a < b;
|
||||
}
|
||||
],
|
||||
'filter->': [
|
||||
BooleanType,
|
||||
[StringType, ValueType],
|
||||
(ctx, [k, v]) => {
|
||||
const a = ctx.properties()[(k as any).value];
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a > b;
|
||||
}
|
||||
],
|
||||
'filter-id->': [
|
||||
BooleanType,
|
||||
[ValueType],
|
||||
(ctx, [v]) => {
|
||||
const a = ctx.id();
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a > b;
|
||||
}
|
||||
],
|
||||
'filter-<=': [
|
||||
BooleanType,
|
||||
[StringType, ValueType],
|
||||
(ctx, [k, v]) => {
|
||||
const a = ctx.properties()[(k as any).value];
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a <= b;
|
||||
}
|
||||
],
|
||||
'filter-id-<=': [
|
||||
BooleanType,
|
||||
[ValueType],
|
||||
(ctx, [v]) => {
|
||||
const a = ctx.id();
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a <= b;
|
||||
}
|
||||
],
|
||||
'filter->=': [
|
||||
BooleanType,
|
||||
[StringType, ValueType],
|
||||
(ctx, [k, v]) => {
|
||||
const a = ctx.properties()[(k as any).value];
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a >= b;
|
||||
}
|
||||
],
|
||||
'filter-id->=': [
|
||||
BooleanType,
|
||||
[ValueType],
|
||||
(ctx, [v]) => {
|
||||
const a = ctx.id();
|
||||
const b = (v as any).value;
|
||||
return typeof a === typeof b && a >= b;
|
||||
}
|
||||
],
|
||||
'filter-has': [BooleanType, [ValueType], (ctx, [k]) => (k as any).value in ctx.properties()],
|
||||
'filter-has-id': [BooleanType, [], (ctx) => ctx.id() !== null && ctx.id() !== undefined],
|
||||
'filter-type-in': [
|
||||
BooleanType,
|
||||
[array(StringType)],
|
||||
(ctx, [v]) => (v as any).value.indexOf(ctx.geometryType()) >= 0
|
||||
],
|
||||
'filter-id-in': [
|
||||
BooleanType,
|
||||
[array(ValueType)],
|
||||
(ctx, [v]) => (v as any).value.indexOf(ctx.id()) >= 0
|
||||
],
|
||||
'filter-in-small': [
|
||||
BooleanType,
|
||||
[StringType, array(ValueType)],
|
||||
// assumes v is an array literal
|
||||
(ctx, [k, v]) => (v as any).value.indexOf(ctx.properties()[(k as any).value]) >= 0
|
||||
],
|
||||
'filter-in-large': [
|
||||
BooleanType,
|
||||
[StringType, array(ValueType)],
|
||||
// assumes v is a array literal with values sorted in ascending order and of a single type
|
||||
(ctx, [k, v]) =>
|
||||
binarySearch(
|
||||
ctx.properties()[(k as any).value],
|
||||
(v as any).value,
|
||||
0,
|
||||
(v as any).value.length - 1
|
||||
)
|
||||
],
|
||||
all: {
|
||||
type: BooleanType,
|
||||
overloads: [
|
||||
[[BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) && b.evaluate(ctx)],
|
||||
[
|
||||
varargs(BooleanType),
|
||||
(ctx, args) => {
|
||||
for (const arg of args) {
|
||||
if (!arg.evaluate(ctx)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
any: {
|
||||
type: BooleanType,
|
||||
overloads: [
|
||||
[[BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) || b.evaluate(ctx)],
|
||||
[
|
||||
varargs(BooleanType),
|
||||
(ctx, args) => {
|
||||
for (const arg of args) {
|
||||
if (arg.evaluate(ctx)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
'!': [BooleanType, [BooleanType], (ctx, [b]) => !b.evaluate(ctx)],
|
||||
'is-supported-script': [
|
||||
BooleanType,
|
||||
[StringType],
|
||||
// At parse time this will always return true, so we need to exclude this expression with isGlobalPropertyConstant
|
||||
(ctx, [s]) => {
|
||||
const isSupportedScript = ctx.globals && ctx.globals.isSupportedScript;
|
||||
if (isSupportedScript) {
|
||||
return isSupportedScript(s.evaluate(ctx));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
],
|
||||
upcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toUpperCase()],
|
||||
downcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase()],
|
||||
concat: [
|
||||
StringType,
|
||||
varargs(ValueType),
|
||||
(ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
|
||||
],
|
||||
split: [
|
||||
array(StringType),
|
||||
[StringType, StringType],
|
||||
(ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
|
||||
],
|
||||
join: [
|
||||
StringType,
|
||||
[array(StringType), StringType],
|
||||
(ctx, [arr, delim]) => arr.evaluate(ctx).join(delim.evaluate(ctx))
|
||||
],
|
||||
'resolved-locale': [
|
||||
StringType,
|
||||
[CollatorType],
|
||||
(ctx, [collator]) => collator.evaluate(ctx).resolvedLocale()
|
||||
]
|
||||
});
|
||||
|
||||
function stringifySignature(signature: Signature): string {
|
||||
if (Array.isArray(signature)) {
|
||||
return `(${signature.map(typeToString).join(', ')})`;
|
||||
} else {
|
||||
return `(${typeToString(signature.type)}...)`;
|
||||
}
|
||||
}
|
||||
|
||||
function isExpressionConstant(expression: Expression) {
|
||||
if (expression instanceof Var) {
|
||||
return isExpressionConstant(expression.boundExpression);
|
||||
} else if (expression instanceof CompoundExpression && expression.name === 'error') {
|
||||
return false;
|
||||
} else if (expression instanceof CollatorExpression) {
|
||||
// Although the results of a Collator expression with fixed arguments
|
||||
// generally shouldn't change between executions, we can't serialize them
|
||||
// as constant expressions because results change based on environment.
|
||||
return false;
|
||||
} else if (expression instanceof Within) {
|
||||
return false;
|
||||
} else if (expression instanceof Distance) {
|
||||
return false;
|
||||
} else if (expression instanceof GlobalState) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTypeAnnotation = expression instanceof Coercion || expression instanceof Assertion;
|
||||
|
||||
let childrenConstant = true;
|
||||
expression.eachChild((child) => {
|
||||
// We can _almost_ assume that if `expressions` children are constant,
|
||||
// they would already have been evaluated to Literal values when they
|
||||
// were parsed. Type annotations are the exception, because they might
|
||||
// have been inferred and added after a child was parsed.
|
||||
|
||||
// So we recurse into isConstant() for the children of type annotations,
|
||||
// but otherwise simply check whether they are Literals.
|
||||
if (isTypeAnnotation) {
|
||||
childrenConstant = childrenConstant && isExpressionConstant(child);
|
||||
} else {
|
||||
childrenConstant = childrenConstant && child instanceof Literal;
|
||||
}
|
||||
});
|
||||
if (!childrenConstant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
isFeatureConstant(expression) &&
|
||||
isGlobalPropertyConstant(expression, [
|
||||
'zoom',
|
||||
'heatmap-density',
|
||||
'elevation',
|
||||
'line-progress',
|
||||
'accumulated',
|
||||
'is-supported-script'
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function isFeatureConstant(e: Expression) {
|
||||
if (e instanceof CompoundExpression) {
|
||||
if (e.name === 'get' && e.args.length === 1) {
|
||||
return false;
|
||||
} else if (e.name === 'feature-state') {
|
||||
return false;
|
||||
} else if (e.name === 'has' && e.args.length === 1) {
|
||||
return false;
|
||||
} else if (e.name === 'properties' || e.name === 'geometry-type' || e.name === 'id') {
|
||||
return false;
|
||||
} else if (/^filter-/.test(e.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof Within) {
|
||||
return false;
|
||||
}
|
||||
if (e instanceof Distance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result = true;
|
||||
e.eachChild((arg) => {
|
||||
if (result && !isFeatureConstant(arg)) {
|
||||
result = false;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function isStateConstant(e: Expression) {
|
||||
if (e instanceof CompoundExpression) {
|
||||
if (e.name === 'feature-state') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let result = true;
|
||||
e.eachChild((arg) => {
|
||||
if (result && !isStateConstant(arg)) {
|
||||
result = false;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function isGlobalPropertyConstant(e: Expression, properties: Array<string>) {
|
||||
if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) {
|
||||
return false;
|
||||
}
|
||||
let result = true;
|
||||
e.eachChild((arg) => {
|
||||
if (result && !isGlobalPropertyConstant(arg, properties)) {
|
||||
result = false;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export {isFeatureConstant, isGlobalPropertyConstant, isStateConstant, isExpressionConstant};
|
||||
111
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/assertion.ts
generated
vendored
Normal file
111
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/assertion.ts
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ObjectType,
|
||||
ValueType,
|
||||
StringType,
|
||||
NumberType,
|
||||
BooleanType,
|
||||
checkSubtype,
|
||||
typeToString,
|
||||
array
|
||||
} from '../types';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
import {typeOf} from '../values';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
const types = {
|
||||
string: StringType,
|
||||
number: NumberType,
|
||||
boolean: BooleanType,
|
||||
object: ObjectType
|
||||
};
|
||||
|
||||
export class Assertion implements Expression {
|
||||
type: Type;
|
||||
args: Array<Expression>;
|
||||
|
||||
constructor(type: Type, args: Array<Expression>) {
|
||||
this.type = type;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 2) return context.error('Expected at least one argument.') as null;
|
||||
|
||||
let i = 1;
|
||||
let type;
|
||||
|
||||
const name: string = args[0] as any;
|
||||
if (name === 'array') {
|
||||
let itemType;
|
||||
if (args.length > 2) {
|
||||
const type = args[1];
|
||||
if (typeof type !== 'string' || !(type in types) || type === 'object')
|
||||
return context.error(
|
||||
'The item type argument of "array" must be one of string, number, boolean',
|
||||
1
|
||||
) as null;
|
||||
itemType = types[type];
|
||||
i++;
|
||||
} else {
|
||||
itemType = ValueType;
|
||||
}
|
||||
|
||||
let N;
|
||||
if (args.length > 3) {
|
||||
if (
|
||||
args[2] !== null &&
|
||||
(typeof args[2] !== 'number' || args[2] < 0 || args[2] !== Math.floor(args[2]))
|
||||
) {
|
||||
return context.error(
|
||||
'The length argument to "array" must be a positive integer literal',
|
||||
2
|
||||
) as null;
|
||||
}
|
||||
N = args[2];
|
||||
i++;
|
||||
}
|
||||
|
||||
type = array(itemType, N);
|
||||
} else {
|
||||
if (!types[name]) throw new Error(`Types doesn't contain name = ${name}`);
|
||||
type = types[name];
|
||||
}
|
||||
|
||||
const parsed = [];
|
||||
for (; i < args.length; i++) {
|
||||
const input = context.parse(args[i], i, ValueType);
|
||||
if (!input) return null;
|
||||
parsed.push(input);
|
||||
}
|
||||
|
||||
return new Assertion(type, parsed);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
for (let i = 0; i < this.args.length; i++) {
|
||||
const value = this.args[i].evaluate(ctx);
|
||||
const error = checkSubtype(this.type, typeOf(value));
|
||||
if (!error) {
|
||||
return value;
|
||||
} else if (i === this.args.length - 1) {
|
||||
throw new RuntimeError(
|
||||
`Expected value to be of type ${typeToString(this.type)}, but found ${typeToString(typeOf(value))} instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
this.args.forEach(fn);
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return this.args.every((arg) => arg.outputDefined());
|
||||
}
|
||||
}
|
||||
64
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/at.ts
generated
vendored
Normal file
64
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/at.ts
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import {array, ValueType, NumberType} from '../types';
|
||||
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type, ArrayType} from '../types';
|
||||
import type {Value} from '../values';
|
||||
|
||||
export class At implements Expression {
|
||||
type: Type;
|
||||
index: Expression;
|
||||
input: Expression;
|
||||
|
||||
constructor(type: Type, index: Expression, input: Expression) {
|
||||
this.type = type;
|
||||
this.index = index;
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 3)
|
||||
return context.error(
|
||||
`Expected 2 arguments, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
|
||||
const index = context.parse(args[1], 1, NumberType);
|
||||
const input = context.parse(args[2], 2, array(context.expectedType || ValueType));
|
||||
|
||||
if (!index || !input) return null;
|
||||
|
||||
const t: ArrayType = input.type as any;
|
||||
return new At(t.itemType, index, input);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const index = this.index.evaluate(ctx) as any as number;
|
||||
const array = this.input.evaluate(ctx) as any as Array<Value>;
|
||||
|
||||
if (index < 0) {
|
||||
throw new RuntimeError(`Array index out of bounds: ${index} < 0.`);
|
||||
}
|
||||
|
||||
if (index >= array.length) {
|
||||
throw new RuntimeError(`Array index out of bounds: ${index} > ${array.length - 1}.`);
|
||||
}
|
||||
|
||||
if (index !== Math.floor(index)) {
|
||||
throw new RuntimeError(`Array index must be an integer, but found ${index} instead.`);
|
||||
}
|
||||
|
||||
return array[index];
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.index);
|
||||
fn(this.input);
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
77
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/case.ts
generated
vendored
Normal file
77
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/case.ts
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
import {BooleanType} from '../types';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
type Branches = Array<[Expression, Expression]>;
|
||||
|
||||
export class Case implements Expression {
|
||||
type: Type;
|
||||
|
||||
branches: Branches;
|
||||
otherwise: Expression;
|
||||
|
||||
constructor(type: Type, branches: Branches, otherwise: Expression) {
|
||||
this.type = type;
|
||||
this.branches = branches;
|
||||
this.otherwise = otherwise;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 4)
|
||||
return context.error(
|
||||
`Expected at least 3 arguments, but found only ${args.length - 1}.`
|
||||
) as null;
|
||||
if (args.length % 2 !== 0)
|
||||
return context.error('Expected an odd number of arguments.') as null;
|
||||
|
||||
let outputType: Type;
|
||||
if (context.expectedType && context.expectedType.kind !== 'value') {
|
||||
outputType = context.expectedType;
|
||||
}
|
||||
|
||||
const branches = [];
|
||||
for (let i = 1; i < args.length - 1; i += 2) {
|
||||
const test = context.parse(args[i], i, BooleanType);
|
||||
if (!test) return null;
|
||||
|
||||
const result = context.parse(args[i + 1], i + 1, outputType);
|
||||
if (!result) return null;
|
||||
|
||||
branches.push([test, result]);
|
||||
|
||||
outputType = outputType || result.type;
|
||||
}
|
||||
|
||||
const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType);
|
||||
if (!otherwise) return null;
|
||||
|
||||
if (!outputType) throw new Error("Can't infer output type");
|
||||
return new Case(outputType as any, branches, otherwise);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
for (const [test, expression] of this.branches) {
|
||||
if (test.evaluate(ctx)) {
|
||||
return expression.evaluate(ctx);
|
||||
}
|
||||
}
|
||||
return this.otherwise.evaluate(ctx);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
for (const [test, expression] of this.branches) {
|
||||
fn(test);
|
||||
fn(expression);
|
||||
}
|
||||
fn(this.otherwise);
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return (
|
||||
this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined()
|
||||
);
|
||||
}
|
||||
}
|
||||
83
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/coalesce.ts
generated
vendored
Normal file
83
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/coalesce.ts
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import {checkSubtype, ValueType} from '../types';
|
||||
import {ResolvedImage} from '../types/resolved_image';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class Coalesce implements Expression {
|
||||
type: Type;
|
||||
args: Array<Expression>;
|
||||
|
||||
constructor(type: Type, args: Array<Expression>) {
|
||||
this.type = type;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 2) {
|
||||
return context.error('Expected at least one argument.') as null;
|
||||
}
|
||||
let outputType: Type = null;
|
||||
const expectedType = context.expectedType;
|
||||
if (expectedType && expectedType.kind !== 'value') {
|
||||
outputType = expectedType;
|
||||
}
|
||||
const parsedArgs = [];
|
||||
|
||||
for (const arg of args.slice(1)) {
|
||||
const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, {
|
||||
typeAnnotation: 'omit'
|
||||
});
|
||||
if (!parsed) return null;
|
||||
outputType = outputType || parsed.type;
|
||||
parsedArgs.push(parsed);
|
||||
}
|
||||
if (!outputType) throw new Error('No output type');
|
||||
|
||||
// Above, we parse arguments without inferred type annotation so that
|
||||
// they don't produce a runtime error for `null` input, which would
|
||||
// preempt the desired null-coalescing behavior.
|
||||
// Thus, if any of our arguments would have needed an annotation, we
|
||||
// need to wrap the enclosing coalesce expression with it instead.
|
||||
const needsAnnotation =
|
||||
expectedType && parsedArgs.some((arg) => checkSubtype(expectedType, arg.type));
|
||||
|
||||
return needsAnnotation
|
||||
? new Coalesce(ValueType, parsedArgs)
|
||||
: new Coalesce(outputType as any, parsedArgs);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
let result = null;
|
||||
let argCount = 0;
|
||||
let requestedImageName;
|
||||
for (const arg of this.args) {
|
||||
argCount++;
|
||||
result = arg.evaluate(ctx);
|
||||
// we need to keep track of the first requested image in a coalesce statement
|
||||
// if coalesce can't find a valid image, we return the first image name so styleimagemissing can fire
|
||||
if (result && result instanceof ResolvedImage && !result.available) {
|
||||
if (!requestedImageName) {
|
||||
requestedImageName = result.name;
|
||||
}
|
||||
result = null;
|
||||
if (argCount === this.args.length) {
|
||||
result = requestedImageName;
|
||||
}
|
||||
}
|
||||
|
||||
if (result !== null) break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
this.args.forEach(fn);
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return this.args.every((arg) => arg.outputDefined());
|
||||
}
|
||||
}
|
||||
184
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/coercion.ts
generated
vendored
Normal file
184
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/coercion.ts
generated
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types';
|
||||
import {valueToString, validateRGBA} from '../values';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
import {Formatted} from '../types/formatted';
|
||||
import {ResolvedImage} from '../types/resolved_image';
|
||||
import {Color} from '../types/color';
|
||||
import {Padding} from '../types/padding';
|
||||
import {NumberArray} from '../types/number_array';
|
||||
import {ColorArray} from '../types/color_array';
|
||||
import {VariableAnchorOffsetCollection} from '../types/variable_anchor_offset_collection';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
const types = {
|
||||
'to-boolean': BooleanType,
|
||||
'to-color': ColorType,
|
||||
'to-number': NumberType,
|
||||
'to-string': StringType
|
||||
};
|
||||
|
||||
/**
|
||||
* Special form for error-coalescing coercion expressions "to-number",
|
||||
* "to-color". Since these coercions can fail at runtime, they accept multiple
|
||||
* arguments, only evaluating one at a time until one succeeds.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export class Coercion implements Expression {
|
||||
type: Type;
|
||||
args: Array<Expression>;
|
||||
|
||||
constructor(type: Type, args: Array<Expression>) {
|
||||
this.type = type;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 2) return context.error('Expected at least one argument.') as null;
|
||||
|
||||
const name: string = args[0] as any;
|
||||
if (!types[name])
|
||||
throw new Error(`Can't parse ${name} as it is not part of the known types`);
|
||||
if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2)
|
||||
return context.error('Expected one argument.') as null;
|
||||
|
||||
const type = types[name];
|
||||
|
||||
const parsed = [];
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const input = context.parse(args[i], i, ValueType);
|
||||
if (!input) return null;
|
||||
parsed.push(input);
|
||||
}
|
||||
|
||||
return new Coercion(type, parsed);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
switch (this.type.kind) {
|
||||
case 'boolean':
|
||||
return Boolean(this.args[0].evaluate(ctx));
|
||||
case 'color': {
|
||||
let input;
|
||||
let error;
|
||||
for (const arg of this.args) {
|
||||
input = arg.evaluate(ctx);
|
||||
error = null;
|
||||
if (input instanceof Color) {
|
||||
return input;
|
||||
} else if (typeof input === 'string') {
|
||||
const c = ctx.parseColor(input);
|
||||
if (c) return c;
|
||||
} else if (Array.isArray(input)) {
|
||||
if (input.length < 3 || input.length > 4) {
|
||||
error = `Invalid rgba value ${JSON.stringify(input)}: expected an array containing either three or four numeric values.`;
|
||||
} else {
|
||||
error = validateRGBA(input[0], input[1], input[2], input[3]);
|
||||
}
|
||||
if (!error) {
|
||||
return new Color(
|
||||
(input[0] as any) / 255,
|
||||
(input[1] as any) / 255,
|
||||
(input[2] as any) / 255,
|
||||
input[3] as any
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new RuntimeError(
|
||||
error ||
|
||||
`Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
|
||||
);
|
||||
}
|
||||
case 'padding': {
|
||||
let input;
|
||||
for (const arg of this.args) {
|
||||
input = arg.evaluate(ctx);
|
||||
|
||||
const pad = Padding.parse(input);
|
||||
if (pad) {
|
||||
return pad;
|
||||
}
|
||||
}
|
||||
throw new RuntimeError(
|
||||
`Could not parse padding from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
|
||||
);
|
||||
}
|
||||
case 'numberArray': {
|
||||
let input;
|
||||
for (const arg of this.args) {
|
||||
input = arg.evaluate(ctx);
|
||||
|
||||
const val = NumberArray.parse(input);
|
||||
if (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
throw new RuntimeError(
|
||||
`Could not parse numberArray from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
|
||||
);
|
||||
}
|
||||
case 'colorArray': {
|
||||
let input;
|
||||
for (const arg of this.args) {
|
||||
input = arg.evaluate(ctx);
|
||||
|
||||
const val = ColorArray.parse(input);
|
||||
if (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
throw new RuntimeError(
|
||||
`Could not parse colorArray from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
|
||||
);
|
||||
}
|
||||
case 'variableAnchorOffsetCollection': {
|
||||
let input;
|
||||
for (const arg of this.args) {
|
||||
input = arg.evaluate(ctx);
|
||||
|
||||
const coll = VariableAnchorOffsetCollection.parse(input);
|
||||
if (coll) {
|
||||
return coll;
|
||||
}
|
||||
}
|
||||
throw new RuntimeError(
|
||||
`Could not parse variableAnchorOffsetCollection from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
|
||||
);
|
||||
}
|
||||
case 'number': {
|
||||
let value = null;
|
||||
for (const arg of this.args) {
|
||||
value = arg.evaluate(ctx);
|
||||
if (value === null) return 0;
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) continue;
|
||||
return num;
|
||||
}
|
||||
throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`);
|
||||
}
|
||||
case 'formatted':
|
||||
// There is no explicit 'to-formatted' but this coercion can be implicitly
|
||||
// created by properties that expect the 'formatted' type.
|
||||
return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
|
||||
case 'resolvedImage':
|
||||
return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
|
||||
case 'projectionDefinition':
|
||||
return this.args[0].evaluate(ctx);
|
||||
default:
|
||||
return valueToString(this.args[0].evaluate(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
this.args.forEach(fn);
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return this.args.every((arg) => arg.outputDefined());
|
||||
}
|
||||
}
|
||||
79
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/collator.ts
generated
vendored
Normal file
79
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/collator.ts
generated
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
import {StringType, BooleanType, CollatorType} from '../types';
|
||||
import {Collator} from '../types/collator';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class CollatorExpression implements Expression {
|
||||
type: Type;
|
||||
caseSensitive: Expression;
|
||||
diacriticSensitive: Expression;
|
||||
locale: Expression | null;
|
||||
|
||||
constructor(
|
||||
caseSensitive: Expression,
|
||||
diacriticSensitive: Expression,
|
||||
locale: Expression | null
|
||||
) {
|
||||
this.type = CollatorType;
|
||||
this.locale = locale;
|
||||
this.caseSensitive = caseSensitive;
|
||||
this.diacriticSensitive = diacriticSensitive;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2) return context.error('Expected one argument.') as null;
|
||||
|
||||
const options = args[1] as any;
|
||||
if (typeof options !== 'object' || Array.isArray(options))
|
||||
return context.error('Collator options argument must be an object.') as null;
|
||||
|
||||
const caseSensitive = context.parse(
|
||||
options['case-sensitive'] === undefined ? false : options['case-sensitive'],
|
||||
1,
|
||||
BooleanType
|
||||
);
|
||||
if (!caseSensitive) return null;
|
||||
|
||||
const diacriticSensitive = context.parse(
|
||||
options['diacritic-sensitive'] === undefined ? false : options['diacritic-sensitive'],
|
||||
1,
|
||||
BooleanType
|
||||
);
|
||||
if (!diacriticSensitive) return null;
|
||||
|
||||
let locale = null;
|
||||
if (options['locale']) {
|
||||
locale = context.parse(options['locale'], 1, StringType);
|
||||
if (!locale) return null;
|
||||
}
|
||||
|
||||
return new CollatorExpression(caseSensitive, diacriticSensitive, locale);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
return new Collator(
|
||||
this.caseSensitive.evaluate(ctx),
|
||||
this.diacriticSensitive.evaluate(ctx),
|
||||
this.locale ? this.locale.evaluate(ctx) : null
|
||||
);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.caseSensitive);
|
||||
fn(this.diacriticSensitive);
|
||||
if (this.locale) {
|
||||
fn(this.locale);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
// Technically the set of possible outputs is the combinatoric set of Collators produced
|
||||
// by all possible outputs of locale/caseSensitive/diacriticSensitive
|
||||
// But for the primary use of Collators in comparison operators, we ignore the Collator's
|
||||
// possible outputs anyway, so we can get away with leaving this false for now.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
214
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/comparison.ts
generated
vendored
Normal file
214
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/comparison.ts
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
import {typeToString, ValueType, BooleanType, CollatorType} from '../types';
|
||||
import {Assertion} from './assertion';
|
||||
import {typeOf} from '../values';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
type ComparisonOperator = '==' | '!=' | '<' | '>' | '<=' | '>=';
|
||||
|
||||
function isComparableType(op: ComparisonOperator, type: Type) {
|
||||
if (op === '==' || op === '!=') {
|
||||
// equality operator
|
||||
return (
|
||||
type.kind === 'boolean' ||
|
||||
type.kind === 'string' ||
|
||||
type.kind === 'number' ||
|
||||
type.kind === 'null' ||
|
||||
type.kind === 'value'
|
||||
);
|
||||
} else {
|
||||
// ordering operator
|
||||
return type.kind === 'string' || type.kind === 'number' || type.kind === 'value';
|
||||
}
|
||||
}
|
||||
|
||||
function eq(ctx, a, b) {
|
||||
return a === b;
|
||||
}
|
||||
function neq(ctx, a, b) {
|
||||
return a !== b;
|
||||
}
|
||||
function lt(ctx, a, b) {
|
||||
return a < b;
|
||||
}
|
||||
function gt(ctx, a, b) {
|
||||
return a > b;
|
||||
}
|
||||
function lteq(ctx, a, b) {
|
||||
return a <= b;
|
||||
}
|
||||
function gteq(ctx, a, b) {
|
||||
return a >= b;
|
||||
}
|
||||
|
||||
function eqCollate(ctx, a, b, c) {
|
||||
return c.compare(a, b) === 0;
|
||||
}
|
||||
function neqCollate(ctx, a, b, c) {
|
||||
return !eqCollate(ctx, a, b, c);
|
||||
}
|
||||
function ltCollate(ctx, a, b, c) {
|
||||
return c.compare(a, b) < 0;
|
||||
}
|
||||
function gtCollate(ctx, a, b, c) {
|
||||
return c.compare(a, b) > 0;
|
||||
}
|
||||
function lteqCollate(ctx, a, b, c) {
|
||||
return c.compare(a, b) <= 0;
|
||||
}
|
||||
function gteqCollate(ctx, a, b, c) {
|
||||
return c.compare(a, b) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Special form for comparison operators, implementing the signatures:
|
||||
* - (T, T, ?Collator) => boolean
|
||||
* - (T, value, ?Collator) => boolean
|
||||
* - (value, T, ?Collator) => boolean
|
||||
*
|
||||
* For inequalities, T must be either value, string, or number. For ==/!=, it
|
||||
* can also be boolean or null.
|
||||
*
|
||||
* Equality semantics are equivalent to Javascript's strict equality (===/!==)
|
||||
* -- i.e., when the arguments' types don't match, == evaluates to false, != to
|
||||
* true.
|
||||
*
|
||||
* When types don't match in an ordering comparison, a runtime error is thrown.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function makeComparison(op: ComparisonOperator, compareBasic, compareWithCollator) {
|
||||
const isOrderComparison = op !== '==' && op !== '!=';
|
||||
|
||||
return class Comparison implements Expression {
|
||||
type: Type;
|
||||
lhs: Expression;
|
||||
rhs: Expression;
|
||||
collator: Expression;
|
||||
hasUntypedArgument: boolean;
|
||||
|
||||
constructor(lhs: Expression, rhs: Expression, collator?: Expression | null) {
|
||||
this.type = BooleanType;
|
||||
this.lhs = lhs;
|
||||
this.rhs = rhs;
|
||||
this.collator = collator;
|
||||
this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value';
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 3 && args.length !== 4)
|
||||
return context.error('Expected two or three arguments.') as null;
|
||||
|
||||
const op: ComparisonOperator = args[0] as any;
|
||||
|
||||
let lhs = context.parse(args[1], 1, ValueType);
|
||||
if (!lhs) return null;
|
||||
if (!isComparableType(op, lhs.type)) {
|
||||
return context
|
||||
.concat(1)
|
||||
.error(
|
||||
`"${op}" comparisons are not supported for type '${typeToString(lhs.type)}'.`
|
||||
) as null;
|
||||
}
|
||||
let rhs = context.parse(args[2], 2, ValueType);
|
||||
if (!rhs) return null;
|
||||
if (!isComparableType(op, rhs.type)) {
|
||||
return context
|
||||
.concat(2)
|
||||
.error(
|
||||
`"${op}" comparisons are not supported for type '${typeToString(rhs.type)}'.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (
|
||||
lhs.type.kind !== rhs.type.kind &&
|
||||
lhs.type.kind !== 'value' &&
|
||||
rhs.type.kind !== 'value'
|
||||
) {
|
||||
return context.error(
|
||||
`Cannot compare types '${typeToString(lhs.type)}' and '${typeToString(rhs.type)}'.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (isOrderComparison) {
|
||||
// typing rules specific to less/greater than operators
|
||||
if (lhs.type.kind === 'value' && rhs.type.kind !== 'value') {
|
||||
// (value, T)
|
||||
lhs = new Assertion(rhs.type, [lhs]);
|
||||
} else if (lhs.type.kind !== 'value' && rhs.type.kind === 'value') {
|
||||
// (T, value)
|
||||
rhs = new Assertion(lhs.type, [rhs]);
|
||||
}
|
||||
}
|
||||
|
||||
let collator = null;
|
||||
if (args.length === 4) {
|
||||
if (
|
||||
lhs.type.kind !== 'string' &&
|
||||
rhs.type.kind !== 'string' &&
|
||||
lhs.type.kind !== 'value' &&
|
||||
rhs.type.kind !== 'value'
|
||||
) {
|
||||
return context.error(
|
||||
'Cannot use collator to compare non-string types.'
|
||||
) as null;
|
||||
}
|
||||
collator = context.parse(args[3], 3, CollatorType);
|
||||
if (!collator) return null;
|
||||
}
|
||||
|
||||
return new Comparison(lhs, rhs, collator);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const lhs = this.lhs.evaluate(ctx);
|
||||
const rhs = this.rhs.evaluate(ctx);
|
||||
|
||||
if (isOrderComparison && this.hasUntypedArgument) {
|
||||
const lt = typeOf(lhs);
|
||||
const rt = typeOf(rhs);
|
||||
// check that type is string or number, and equal
|
||||
if (lt.kind !== rt.kind || !(lt.kind === 'string' || lt.kind === 'number')) {
|
||||
throw new RuntimeError(
|
||||
`Expected arguments for "${op}" to be (string, string) or (number, number), but found (${lt.kind}, ${rt.kind}) instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.collator && !isOrderComparison && this.hasUntypedArgument) {
|
||||
const lt = typeOf(lhs);
|
||||
const rt = typeOf(rhs);
|
||||
if (lt.kind !== 'string' || rt.kind !== 'string') {
|
||||
return compareBasic(ctx, lhs, rhs);
|
||||
}
|
||||
}
|
||||
|
||||
return this.collator
|
||||
? compareWithCollator(ctx, lhs, rhs, this.collator.evaluate(ctx))
|
||||
: compareBasic(ctx, lhs, rhs);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.lhs);
|
||||
fn(this.rhs);
|
||||
if (this.collator) {
|
||||
fn(this.collator);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const Equals = makeComparison('==', eq, eqCollate);
|
||||
export const NotEquals = makeComparison('!=', neq, neqCollate);
|
||||
export const LessThan = makeComparison('<', lt, ltCollate);
|
||||
export const GreaterThan = makeComparison('>', gt, gtCollate);
|
||||
export const LessThanOrEqual = makeComparison('<=', lteq, lteqCollate);
|
||||
export const GreaterThanOrEqual = makeComparison('>=', gteq, gteqCollate);
|
||||
781
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/distance.ts
generated
vendored
Normal file
781
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/distance.ts
generated
vendored
Normal file
@@ -0,0 +1,781 @@
|
||||
import TinyQueue from 'tinyqueue';
|
||||
import {Expression} from '../expression';
|
||||
import {ParsingContext} from '../parsing_context';
|
||||
import {NumberType, Type} from '../types';
|
||||
import {isValue} from '../values';
|
||||
import {EvaluationContext} from '../evaluation_context';
|
||||
import {
|
||||
BBox,
|
||||
boxWithinBox,
|
||||
getLngLatFromTileCoord,
|
||||
pointWithinPolygon,
|
||||
segmentIntersectSegment,
|
||||
updateBBox
|
||||
} from '../../util/geometry_util';
|
||||
import {classifyRings} from '../../util/classify_rings';
|
||||
import {CheapRuler} from '../../util/cheap_ruler';
|
||||
|
||||
type SimpleGeometry = GeoJSON.Polygon | GeoJSON.LineString | GeoJSON.Point;
|
||||
|
||||
const MinPointsSize = 100;
|
||||
const MinLinePointsSize = 50;
|
||||
|
||||
type IndexRange = [number, number];
|
||||
type DistPair = [number, IndexRange, IndexRange];
|
||||
|
||||
function compareDistPair(a: DistPair, b: DistPair): number {
|
||||
return b[0] - a[0];
|
||||
}
|
||||
|
||||
function getRangeSize(range: IndexRange) {
|
||||
return range[1] - range[0] + 1;
|
||||
}
|
||||
|
||||
function isRangeSafe(range: IndexRange, threshold: number): boolean {
|
||||
return range[1] >= range[0] && range[1] < threshold;
|
||||
}
|
||||
|
||||
function splitRange(range: IndexRange, isLine: boolean): [IndexRange, IndexRange] {
|
||||
if (range[0] > range[1]) {
|
||||
return [null, null];
|
||||
}
|
||||
const size = getRangeSize(range);
|
||||
if (isLine) {
|
||||
if (size === 2) {
|
||||
return [range, null];
|
||||
}
|
||||
const size1 = Math.floor(size / 2);
|
||||
return [
|
||||
[range[0], range[0] + size1],
|
||||
[range[0] + size1, range[1]]
|
||||
];
|
||||
}
|
||||
if (size === 1) {
|
||||
return [range, null];
|
||||
}
|
||||
const size1 = Math.floor(size / 2) - 1;
|
||||
return [
|
||||
[range[0], range[0] + size1],
|
||||
[range[0] + size1 + 1, range[1]]
|
||||
];
|
||||
}
|
||||
|
||||
function getBBox(coords: [number, number][], range: IndexRange): BBox {
|
||||
if (!isRangeSafe(range, coords.length)) {
|
||||
return [Infinity, Infinity, -Infinity, -Infinity];
|
||||
}
|
||||
|
||||
const bbox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
for (let i = range[0]; i <= range[1]; ++i) {
|
||||
updateBBox(bbox, coords[i]);
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
function getPolygonBBox(polygon: [number, number][][]): BBox {
|
||||
const bbox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
for (const ring of polygon) {
|
||||
for (const coord of ring) {
|
||||
updateBBox(bbox, coord);
|
||||
}
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
function isValidBBox(bbox: BBox): boolean {
|
||||
return (
|
||||
bbox[0] !== -Infinity &&
|
||||
bbox[1] !== -Infinity &&
|
||||
bbox[2] !== Infinity &&
|
||||
bbox[3] !== Infinity
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate the distance between two bounding boxes.
|
||||
// Calculate the delta in x and y direction, and use two fake points {0.0, 0.0}
|
||||
// and {dx, dy} to calculate the distance. Distance will be 0.0 if bounding box are overlapping.
|
||||
function bboxToBBoxDistance(bbox1: BBox, bbox2: BBox, ruler: CheapRuler): number {
|
||||
if (!isValidBBox(bbox1) || !isValidBBox(bbox2)) {
|
||||
return NaN;
|
||||
}
|
||||
let dx = 0.0;
|
||||
let dy = 0.0;
|
||||
// bbox1 in left side
|
||||
if (bbox1[2] < bbox2[0]) {
|
||||
dx = bbox2[0] - bbox1[2];
|
||||
}
|
||||
// bbox1 in right side
|
||||
if (bbox1[0] > bbox2[2]) {
|
||||
dx = bbox1[0] - bbox2[2];
|
||||
}
|
||||
// bbox1 in above side
|
||||
if (bbox1[1] > bbox2[3]) {
|
||||
dy = bbox1[1] - bbox2[3];
|
||||
}
|
||||
// bbox1 in down side
|
||||
if (bbox1[3] < bbox2[1]) {
|
||||
dy = bbox2[1] - bbox1[3];
|
||||
}
|
||||
return ruler.distance([0.0, 0.0], [dx, dy]);
|
||||
}
|
||||
|
||||
function pointToLineDistance(
|
||||
point: [number, number],
|
||||
line: [number, number][],
|
||||
ruler: CheapRuler
|
||||
): number {
|
||||
const nearestPoint = ruler.pointOnLine(line, point);
|
||||
return ruler.distance(point, nearestPoint.point);
|
||||
}
|
||||
|
||||
function segmentToSegmentDistance(
|
||||
p1: [number, number],
|
||||
p2: [number, number],
|
||||
q1: [number, number],
|
||||
q2: [number, number],
|
||||
ruler: CheapRuler
|
||||
): number {
|
||||
const dist1 = Math.min(
|
||||
pointToLineDistance(p1, [q1, q2], ruler),
|
||||
pointToLineDistance(p2, [q1, q2], ruler)
|
||||
);
|
||||
const dist2 = Math.min(
|
||||
pointToLineDistance(q1, [p1, p2], ruler),
|
||||
pointToLineDistance(q2, [p1, p2], ruler)
|
||||
);
|
||||
return Math.min(dist1, dist2);
|
||||
}
|
||||
|
||||
function lineToLineDistance(
|
||||
line1: [number, number][],
|
||||
range1: IndexRange,
|
||||
line2: [number, number][],
|
||||
range2: IndexRange,
|
||||
ruler: CheapRuler
|
||||
): number {
|
||||
const rangeSafe = isRangeSafe(range1, line1.length) && isRangeSafe(range2, line2.length);
|
||||
if (!rangeSafe) {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
let dist = Infinity;
|
||||
for (let i = range1[0]; i < range1[1]; ++i) {
|
||||
const p1 = line1[i];
|
||||
const p2 = line1[i + 1];
|
||||
for (let j = range2[0]; j < range2[1]; ++j) {
|
||||
const q1 = line2[j];
|
||||
const q2 = line2[j + 1];
|
||||
if (segmentIntersectSegment(p1, p2, q1, q2)) {
|
||||
return 0.0;
|
||||
}
|
||||
dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler));
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function pointsToPointsDistance(
|
||||
points1: [number, number][],
|
||||
range1: IndexRange,
|
||||
points2: [number, number][],
|
||||
range2: IndexRange,
|
||||
ruler: CheapRuler
|
||||
): number {
|
||||
const rangeSafe = isRangeSafe(range1, points1.length) && isRangeSafe(range2, points2.length);
|
||||
if (!rangeSafe) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
let dist = Infinity;
|
||||
for (let i = range1[0]; i <= range1[1]; ++i) {
|
||||
for (let j = range2[0]; j <= range2[1]; ++j) {
|
||||
dist = Math.min(dist, ruler.distance(points1[i], points2[j]));
|
||||
if (dist === 0.0) {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function pointToPolygonDistance(
|
||||
point: [number, number],
|
||||
polygon: [number, number][][],
|
||||
ruler: CheapRuler
|
||||
): number {
|
||||
if (pointWithinPolygon(point, polygon, true)) {
|
||||
return 0.0;
|
||||
}
|
||||
let dist = Infinity;
|
||||
for (const ring of polygon) {
|
||||
const front = ring[0];
|
||||
const back = ring[ring.length - 1];
|
||||
if (front !== back) {
|
||||
dist = Math.min(dist, pointToLineDistance(point, [back, front], ruler));
|
||||
if (dist === 0.0) {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
const nearestPoint = ruler.pointOnLine(ring, point);
|
||||
dist = Math.min(dist, ruler.distance(point, nearestPoint.point));
|
||||
if (dist === 0.0) {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function lineToPolygonDistance(
|
||||
line: [number, number][],
|
||||
range: IndexRange,
|
||||
polygon: [number, number][][],
|
||||
ruler: CheapRuler
|
||||
): number {
|
||||
if (!isRangeSafe(range, line.length)) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
for (let i = range[0]; i <= range[1]; ++i) {
|
||||
if (pointWithinPolygon(line[i], polygon, true)) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
let dist = Infinity;
|
||||
for (let i = range[0]; i < range[1]; ++i) {
|
||||
const p1 = line[i];
|
||||
const p2 = line[i + 1];
|
||||
for (const ring of polygon) {
|
||||
for (let j = 0, len = ring.length, k = len - 1; j < len; k = j++) {
|
||||
const q1 = ring[k];
|
||||
const q2 = ring[j];
|
||||
if (segmentIntersectSegment(p1, p2, q1, q2)) {
|
||||
return 0.0;
|
||||
}
|
||||
dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler));
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function polygonIntersect(poly1: [number, number][][], poly2: [number, number][][]): boolean {
|
||||
for (const ring of poly1) {
|
||||
for (const point of ring) {
|
||||
if (pointWithinPolygon(point, poly2, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function polygonToPolygonDistance(
|
||||
polygon1: [number, number][][],
|
||||
polygon2: [number, number][][],
|
||||
ruler,
|
||||
currentMiniDist = Infinity
|
||||
): number {
|
||||
const bbox1 = getPolygonBBox(polygon1);
|
||||
const bbox2 = getPolygonBBox(polygon2);
|
||||
if (
|
||||
currentMiniDist !== Infinity &&
|
||||
bboxToBBoxDistance(bbox1, bbox2, ruler) >= currentMiniDist
|
||||
) {
|
||||
return currentMiniDist;
|
||||
}
|
||||
|
||||
if (boxWithinBox(bbox1, bbox2)) {
|
||||
if (polygonIntersect(polygon1, polygon2)) {
|
||||
return 0.0;
|
||||
}
|
||||
} else if (polygonIntersect(polygon2, polygon1)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let dist = Infinity;
|
||||
for (const ring1 of polygon1) {
|
||||
for (let i = 0, len1 = ring1.length, l = len1 - 1; i < len1; l = i++) {
|
||||
const p1 = ring1[l];
|
||||
const p2 = ring1[i];
|
||||
for (const ring2 of polygon2) {
|
||||
for (let j = 0, len2 = ring2.length, k = len2 - 1; j < len2; k = j++) {
|
||||
const q1 = ring2[k];
|
||||
const q2 = ring2[j];
|
||||
if (segmentIntersectSegment(p1, p2, q1, q2)) {
|
||||
return 0.0;
|
||||
}
|
||||
dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function updateQueue(
|
||||
distQueue: TinyQueue<DistPair>,
|
||||
miniDist: number,
|
||||
ruler: CheapRuler,
|
||||
points: [number, number][],
|
||||
polyBBox: BBox,
|
||||
rangeA?: IndexRange
|
||||
) {
|
||||
if (!rangeA) {
|
||||
return;
|
||||
}
|
||||
const tempDist = bboxToBBoxDistance(getBBox(points, rangeA), polyBBox, ruler);
|
||||
// Insert new pair to the queue if the bbox distance is less than
|
||||
// miniDist, The pair with biggest distance will be at the top
|
||||
if (tempDist < miniDist) {
|
||||
distQueue.push([tempDist, rangeA, [0, 0]]);
|
||||
}
|
||||
}
|
||||
|
||||
function updateQueueTwoSets(
|
||||
distQueue: TinyQueue<DistPair>,
|
||||
miniDist: number,
|
||||
ruler: CheapRuler,
|
||||
pointSet1: [number, number][],
|
||||
pointSet2: [number, number][],
|
||||
range1?: IndexRange,
|
||||
range2?: IndexRange
|
||||
) {
|
||||
if (!range1 || !range2) {
|
||||
return;
|
||||
}
|
||||
const tempDist = bboxToBBoxDistance(
|
||||
getBBox(pointSet1, range1),
|
||||
getBBox(pointSet2, range2),
|
||||
ruler
|
||||
);
|
||||
// Insert new pair to the queue if the bbox distance is less than
|
||||
// miniDist, The pair with biggest distance will be at the top
|
||||
if (tempDist < miniDist) {
|
||||
distQueue.push([tempDist, range1, range2]);
|
||||
}
|
||||
}
|
||||
|
||||
// Divide and conquer, the time complexity is O(n*lgn), faster than Brute force
|
||||
// O(n*n) Most of the time, use index for in-place processing.
|
||||
function pointsToPolygonDistance(
|
||||
points: [number, number][],
|
||||
isLine: boolean,
|
||||
polygon: [number, number][][],
|
||||
ruler: CheapRuler,
|
||||
currentMiniDist = Infinity
|
||||
) {
|
||||
let miniDist = Math.min(ruler.distance(points[0], polygon[0][0]), currentMiniDist);
|
||||
if (miniDist === 0.0) {
|
||||
return miniDist;
|
||||
}
|
||||
|
||||
const distQueue = new TinyQueue<DistPair>(
|
||||
[[0, [0, points.length - 1], [0, 0]]],
|
||||
compareDistPair
|
||||
);
|
||||
|
||||
const polyBBox = getPolygonBBox(polygon);
|
||||
while (distQueue.length > 0) {
|
||||
const distPair = distQueue.pop();
|
||||
if (distPair[0] >= miniDist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const range = distPair[1];
|
||||
|
||||
// In case the set size are relatively small, we could use brute-force directly
|
||||
const threshold = isLine ? MinLinePointsSize : MinPointsSize;
|
||||
if (getRangeSize(range) <= threshold) {
|
||||
if (!isRangeSafe(range, points.length)) {
|
||||
return NaN;
|
||||
}
|
||||
if (isLine) {
|
||||
const tempDist = lineToPolygonDistance(points, range, polygon, ruler);
|
||||
if (isNaN(tempDist) || tempDist === 0.0) {
|
||||
return tempDist;
|
||||
}
|
||||
miniDist = Math.min(miniDist, tempDist);
|
||||
} else {
|
||||
for (let i = range[0]; i <= range[1]; ++i) {
|
||||
const tempDist = pointToPolygonDistance(points[i], polygon, ruler);
|
||||
miniDist = Math.min(miniDist, tempDist);
|
||||
if (miniDist === 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const newRangesA = splitRange(range, isLine);
|
||||
|
||||
updateQueue(distQueue, miniDist, ruler, points, polyBBox, newRangesA[0]);
|
||||
updateQueue(distQueue, miniDist, ruler, points, polyBBox, newRangesA[1]);
|
||||
}
|
||||
}
|
||||
return miniDist;
|
||||
}
|
||||
|
||||
function pointSetToPointSetDistance(
|
||||
pointSet1: [number, number][],
|
||||
isLine1: boolean,
|
||||
pointSet2: [number, number][],
|
||||
isLine2: boolean,
|
||||
ruler: CheapRuler,
|
||||
currentMiniDist = Infinity
|
||||
): number {
|
||||
let miniDist = Math.min(currentMiniDist, ruler.distance(pointSet1[0], pointSet2[0]));
|
||||
if (miniDist === 0.0) {
|
||||
return miniDist;
|
||||
}
|
||||
|
||||
const distQueue = new TinyQueue<DistPair>(
|
||||
[[0, [0, pointSet1.length - 1], [0, pointSet2.length - 1]]],
|
||||
compareDistPair
|
||||
);
|
||||
|
||||
while (distQueue.length > 0) {
|
||||
const distPair = distQueue.pop();
|
||||
if (distPair[0] >= miniDist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rangeA = distPair[1];
|
||||
const rangeB = distPair[2];
|
||||
const threshold1 = isLine1 ? MinLinePointsSize : MinPointsSize;
|
||||
const threshold2 = isLine2 ? MinLinePointsSize : MinPointsSize;
|
||||
|
||||
// In case the set size are relatively small, we could use brute-force directly
|
||||
if (getRangeSize(rangeA) <= threshold1 && getRangeSize(rangeB) <= threshold2) {
|
||||
if (!isRangeSafe(rangeA, pointSet1.length) && isRangeSafe(rangeB, pointSet2.length)) {
|
||||
return NaN;
|
||||
}
|
||||
let tempDist: number;
|
||||
if (isLine1 && isLine2) {
|
||||
tempDist = lineToLineDistance(pointSet1, rangeA, pointSet2, rangeB, ruler);
|
||||
miniDist = Math.min(miniDist, tempDist);
|
||||
} else if (isLine1 && !isLine2) {
|
||||
const sublibe = pointSet1.slice(rangeA[0], rangeA[1] + 1);
|
||||
for (let i = rangeB[0]; i <= rangeB[1]; ++i) {
|
||||
tempDist = pointToLineDistance(pointSet2[i], sublibe, ruler);
|
||||
miniDist = Math.min(miniDist, tempDist);
|
||||
if (miniDist === 0.0) {
|
||||
return miniDist;
|
||||
}
|
||||
}
|
||||
} else if (!isLine1 && isLine2) {
|
||||
const sublibe = pointSet2.slice(rangeB[0], rangeB[1] + 1);
|
||||
for (let i = rangeA[0]; i <= rangeA[1]; ++i) {
|
||||
tempDist = pointToLineDistance(pointSet1[i], sublibe, ruler);
|
||||
miniDist = Math.min(miniDist, tempDist);
|
||||
if (miniDist === 0.0) {
|
||||
return miniDist;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tempDist = pointsToPointsDistance(pointSet1, rangeA, pointSet2, rangeB, ruler);
|
||||
miniDist = Math.min(miniDist, tempDist);
|
||||
}
|
||||
} else {
|
||||
const newRangesA = splitRange(rangeA, isLine1);
|
||||
const newRangesB = splitRange(rangeB, isLine2);
|
||||
updateQueueTwoSets(
|
||||
distQueue,
|
||||
miniDist,
|
||||
ruler,
|
||||
pointSet1,
|
||||
pointSet2,
|
||||
newRangesA[0],
|
||||
newRangesB[0]
|
||||
);
|
||||
updateQueueTwoSets(
|
||||
distQueue,
|
||||
miniDist,
|
||||
ruler,
|
||||
pointSet1,
|
||||
pointSet2,
|
||||
newRangesA[0],
|
||||
newRangesB[1]
|
||||
);
|
||||
updateQueueTwoSets(
|
||||
distQueue,
|
||||
miniDist,
|
||||
ruler,
|
||||
pointSet1,
|
||||
pointSet2,
|
||||
newRangesA[1],
|
||||
newRangesB[0]
|
||||
);
|
||||
updateQueueTwoSets(
|
||||
distQueue,
|
||||
miniDist,
|
||||
ruler,
|
||||
pointSet1,
|
||||
pointSet2,
|
||||
newRangesA[1],
|
||||
newRangesB[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
return miniDist;
|
||||
}
|
||||
|
||||
function pointToGeometryDistance(ctx: EvaluationContext, geometries: SimpleGeometry[]) {
|
||||
const tilePoints = ctx.geometry();
|
||||
const pointPosition = tilePoints
|
||||
.flat()
|
||||
.map((p) => getLngLatFromTileCoord([p.x, p.y], ctx.canonical) as [number, number]);
|
||||
if (tilePoints.length === 0) {
|
||||
return NaN;
|
||||
}
|
||||
const ruler = new CheapRuler(pointPosition[0][1]);
|
||||
let dist = Infinity;
|
||||
for (const geometry of geometries) {
|
||||
switch (geometry.type) {
|
||||
case 'Point':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointSetToPointSetDistance(
|
||||
pointPosition,
|
||||
false,
|
||||
[geometry.coordinates as [number, number]],
|
||||
false,
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'LineString':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointSetToPointSetDistance(
|
||||
pointPosition,
|
||||
false,
|
||||
geometry.coordinates as [number, number][],
|
||||
true,
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'Polygon':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointsToPolygonDistance(
|
||||
pointPosition,
|
||||
false,
|
||||
geometry.coordinates as [number, number][][],
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (dist === 0.0) {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function lineStringToGeometryDistance(ctx: EvaluationContext, geometries: SimpleGeometry[]) {
|
||||
const tileLine = ctx.geometry();
|
||||
const linePositions = tileLine
|
||||
.flat()
|
||||
.map((p) => getLngLatFromTileCoord([p.x, p.y], ctx.canonical) as [number, number]);
|
||||
if (tileLine.length === 0) {
|
||||
return NaN;
|
||||
}
|
||||
const ruler = new CheapRuler(linePositions[0][1]);
|
||||
let dist = Infinity;
|
||||
for (const geometry of geometries) {
|
||||
switch (geometry.type) {
|
||||
case 'Point':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointSetToPointSetDistance(
|
||||
linePositions,
|
||||
true,
|
||||
[geometry.coordinates as [number, number]],
|
||||
false,
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'LineString':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointSetToPointSetDistance(
|
||||
linePositions,
|
||||
true,
|
||||
geometry.coordinates as [number, number][],
|
||||
true,
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'Polygon':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointsToPolygonDistance(
|
||||
linePositions,
|
||||
true,
|
||||
geometry.coordinates as [number, number][][],
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (dist === 0.0) {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function polygonToGeometryDistance(ctx: EvaluationContext, geometries: SimpleGeometry[]) {
|
||||
const tilePolygon = ctx.geometry();
|
||||
if (tilePolygon.length === 0 || tilePolygon[0].length === 0) {
|
||||
return NaN;
|
||||
}
|
||||
const polygons = classifyRings(tilePolygon, 0).map((polygon) => {
|
||||
return polygon.map((ring) => {
|
||||
return ring.map(
|
||||
(p) => getLngLatFromTileCoord([p.x, p.y], ctx.canonical) as [number, number]
|
||||
);
|
||||
});
|
||||
});
|
||||
const ruler = new CheapRuler(polygons[0][0][0][1]);
|
||||
let dist = Infinity;
|
||||
for (const geometry of geometries) {
|
||||
for (const polygon of polygons) {
|
||||
switch (geometry.type) {
|
||||
case 'Point':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointsToPolygonDistance(
|
||||
[geometry.coordinates as [number, number]],
|
||||
false,
|
||||
polygon,
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'LineString':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
pointsToPolygonDistance(
|
||||
geometry.coordinates as [number, number][],
|
||||
true,
|
||||
polygon,
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'Polygon':
|
||||
dist = Math.min(
|
||||
dist,
|
||||
polygonToPolygonDistance(
|
||||
polygon,
|
||||
geometry.coordinates as [number, number][][],
|
||||
ruler,
|
||||
dist
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (dist === 0.0) {
|
||||
return dist;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
|
||||
function toSimpleGeometry(
|
||||
geometry: Exclude<GeoJSON.Geometry, GeoJSON.GeometryCollection>
|
||||
): SimpleGeometry[] {
|
||||
if (geometry.type === 'MultiPolygon') {
|
||||
return geometry.coordinates.map((polygon) => {
|
||||
return {
|
||||
type: 'Polygon',
|
||||
coordinates: polygon
|
||||
};
|
||||
});
|
||||
}
|
||||
if (geometry.type === 'MultiLineString') {
|
||||
return geometry.coordinates.map((lineString) => {
|
||||
return {
|
||||
type: 'LineString',
|
||||
coordinates: lineString
|
||||
};
|
||||
});
|
||||
}
|
||||
if (geometry.type === 'MultiPoint') {
|
||||
return geometry.coordinates.map((point) => {
|
||||
return {
|
||||
type: 'Point',
|
||||
coordinates: point
|
||||
};
|
||||
});
|
||||
}
|
||||
return [geometry];
|
||||
}
|
||||
|
||||
export class Distance implements Expression {
|
||||
type: Type;
|
||||
geojson: GeoJSON.GeoJSON;
|
||||
geometries: SimpleGeometry[];
|
||||
|
||||
constructor(geojson: GeoJSON.GeoJSON, geometries: SimpleGeometry[]) {
|
||||
this.type = NumberType;
|
||||
this.geojson = geojson;
|
||||
this.geometries = geometries;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2)
|
||||
return context.error(
|
||||
`'distance' expression requires exactly one argument, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
if (isValue(args[1])) {
|
||||
const geojson = args[1] as any;
|
||||
if (geojson.type === 'FeatureCollection') {
|
||||
return new Distance(
|
||||
geojson,
|
||||
geojson.features.map((feature) => toSimpleGeometry(feature.geometry)).flat()
|
||||
);
|
||||
} else if (geojson.type === 'Feature') {
|
||||
return new Distance(geojson, toSimpleGeometry(geojson.geometry));
|
||||
} else if ('type' in geojson && 'coordinates' in geojson) {
|
||||
return new Distance(geojson, toSimpleGeometry(geojson));
|
||||
}
|
||||
}
|
||||
return context.error(
|
||||
"'distance' expression requires valid geojson object that contains polygon geometry type."
|
||||
) as null;
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
if (ctx.geometry() != null && ctx.canonicalID() != null) {
|
||||
if (ctx.geometryType() === 'Point') {
|
||||
return pointToGeometryDistance(ctx, this.geometries);
|
||||
} else if (ctx.geometryType() === 'LineString') {
|
||||
return lineStringToGeometryDistance(ctx, this.geometries);
|
||||
} else if (ctx.geometryType() === 'Polygon') {
|
||||
return polygonToGeometryDistance(ctx, this.geometries);
|
||||
}
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
|
||||
eachChild() {}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
177
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/format.ts
generated
vendored
Normal file
177
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/format.ts
generated
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
import {
|
||||
NumberType,
|
||||
ValueType,
|
||||
FormattedType,
|
||||
array,
|
||||
StringType,
|
||||
ColorType,
|
||||
ResolvedImageType
|
||||
} from '../types';
|
||||
import {
|
||||
Formatted,
|
||||
FormattedSection,
|
||||
VERTICAL_ALIGN_OPTIONS,
|
||||
VerticalAlign
|
||||
} from '../types/formatted';
|
||||
import {valueToString, typeOf} from '../values';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
type FormattedSectionExpression = {
|
||||
// Content of a section may be Image expression or other
|
||||
// type of expression that is coercable to 'string'.
|
||||
content: Expression;
|
||||
scale: Expression | null;
|
||||
font: Expression | null;
|
||||
textColor: Expression | null;
|
||||
verticalAlign: Expression | null;
|
||||
};
|
||||
|
||||
export class FormatExpression implements Expression {
|
||||
type: Type;
|
||||
sections: Array<FormattedSectionExpression>;
|
||||
|
||||
constructor(sections: Array<FormattedSectionExpression>) {
|
||||
this.type = FormattedType;
|
||||
this.sections = sections;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 2) {
|
||||
return context.error('Expected at least one argument.') as null;
|
||||
}
|
||||
|
||||
const firstArg = args[1];
|
||||
if (!Array.isArray(firstArg) && typeof firstArg === 'object') {
|
||||
return context.error('First argument must be an image or text section.') as null;
|
||||
}
|
||||
|
||||
const sections: Array<FormattedSectionExpression> = [];
|
||||
let nextTokenMayBeObject = false;
|
||||
for (let i = 1; i <= args.length - 1; ++i) {
|
||||
const arg = args[i] as any;
|
||||
|
||||
if (nextTokenMayBeObject && typeof arg === 'object' && !Array.isArray(arg)) {
|
||||
nextTokenMayBeObject = false;
|
||||
|
||||
let scale = null;
|
||||
if (arg['font-scale']) {
|
||||
scale = context.parse(arg['font-scale'], 1, NumberType);
|
||||
if (!scale) return null;
|
||||
}
|
||||
|
||||
let font = null;
|
||||
if (arg['text-font']) {
|
||||
font = context.parse(arg['text-font'], 1, array(StringType));
|
||||
if (!font) return null;
|
||||
}
|
||||
|
||||
let textColor = null;
|
||||
if (arg['text-color']) {
|
||||
textColor = context.parse(arg['text-color'], 1, ColorType);
|
||||
if (!textColor) return null;
|
||||
}
|
||||
|
||||
let verticalAlign = null;
|
||||
if (arg['vertical-align']) {
|
||||
if (
|
||||
typeof arg['vertical-align'] === 'string' &&
|
||||
!VERTICAL_ALIGN_OPTIONS.includes(arg['vertical-align'] as VerticalAlign)
|
||||
) {
|
||||
return context.error(
|
||||
`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${arg['vertical-align']}' instead.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
verticalAlign = context.parse(arg['vertical-align'], 1, StringType);
|
||||
if (!verticalAlign) return null;
|
||||
}
|
||||
|
||||
const lastExpression = sections[sections.length - 1];
|
||||
lastExpression.scale = scale;
|
||||
lastExpression.font = font;
|
||||
lastExpression.textColor = textColor;
|
||||
lastExpression.verticalAlign = verticalAlign;
|
||||
} else {
|
||||
const content = context.parse(args[i], 1, ValueType);
|
||||
if (!content) return null;
|
||||
|
||||
const kind = content.type.kind;
|
||||
if (
|
||||
kind !== 'string' &&
|
||||
kind !== 'value' &&
|
||||
kind !== 'null' &&
|
||||
kind !== 'resolvedImage'
|
||||
)
|
||||
return context.error(
|
||||
"Formatted text type must be 'string', 'value', 'image' or 'null'."
|
||||
) as null;
|
||||
|
||||
nextTokenMayBeObject = true;
|
||||
sections.push({
|
||||
content,
|
||||
scale: null,
|
||||
font: null,
|
||||
textColor: null,
|
||||
verticalAlign: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new FormatExpression(sections);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const evaluateSection = (section) => {
|
||||
const evaluatedContent = section.content.evaluate(ctx);
|
||||
if (typeOf(evaluatedContent) === ResolvedImageType) {
|
||||
return new FormattedSection(
|
||||
'',
|
||||
evaluatedContent,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null
|
||||
);
|
||||
}
|
||||
|
||||
return new FormattedSection(
|
||||
valueToString(evaluatedContent),
|
||||
null,
|
||||
section.scale ? section.scale.evaluate(ctx) : null,
|
||||
section.font ? section.font.evaluate(ctx).join(',') : null,
|
||||
section.textColor ? section.textColor.evaluate(ctx) : null,
|
||||
section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null
|
||||
);
|
||||
};
|
||||
|
||||
return new Formatted(this.sections.map(evaluateSection));
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
for (const section of this.sections) {
|
||||
fn(section.content);
|
||||
if (section.scale) {
|
||||
fn(section.scale);
|
||||
}
|
||||
if (section.font) {
|
||||
fn(section.font);
|
||||
}
|
||||
if (section.textColor) {
|
||||
fn(section.textColor);
|
||||
}
|
||||
if (section.verticalAlign) {
|
||||
fn(section.verticalAlign);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
// Technically the combinatoric set of all children
|
||||
// Usually, this.text will be undefined anyway
|
||||
return false;
|
||||
}
|
||||
}
|
||||
51
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/global_state.ts
generated
vendored
Normal file
51
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/global_state.ts
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import {Type, ValueType} from '../types';
|
||||
import type {Expression} from '../expression';
|
||||
import {ParsingContext} from '../parsing_context';
|
||||
import {EvaluationContext} from '../evaluation_context';
|
||||
import {getOwn} from '../../util/get_own';
|
||||
|
||||
export class GlobalState implements Expression {
|
||||
type: Type;
|
||||
key: string;
|
||||
|
||||
constructor(key: string) {
|
||||
this.type = ValueType;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2) {
|
||||
return context.error(
|
||||
`Expected 1 argument, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
const key = args[1];
|
||||
|
||||
if (key === undefined || key === null) {
|
||||
return context.error('Global state property must be defined.') as null;
|
||||
}
|
||||
|
||||
if (typeof key !== 'string') {
|
||||
return context.error(
|
||||
`Global state property must be string, but found ${typeof args[1]} instead.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
return new GlobalState(key);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const globalState = ctx.globals?.globalState;
|
||||
|
||||
if (!globalState || Object.keys(globalState).length === 0) return null;
|
||||
|
||||
return getOwn(globalState, this.key);
|
||||
}
|
||||
|
||||
eachChild() {}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
47
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/image.ts
generated
vendored
Normal file
47
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/image.ts
generated
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import {ResolvedImageType, StringType} from '../types';
|
||||
import {ResolvedImage} from '../types/resolved_image';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class ImageExpression implements Expression {
|
||||
type: Type;
|
||||
input: Expression;
|
||||
|
||||
constructor(input: Expression) {
|
||||
this.type = ResolvedImageType;
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2) {
|
||||
return context.error('Expected two arguments.') as null;
|
||||
}
|
||||
|
||||
const name = context.parse(args[1], 1, StringType);
|
||||
if (!name) return context.error('No image name provided.') as null;
|
||||
|
||||
return new ImageExpression(name);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const evaluatedImageName = this.input.evaluate(ctx);
|
||||
|
||||
const value = ResolvedImage.fromString(evaluatedImageName);
|
||||
if (value && ctx.availableImages)
|
||||
value.available = ctx.availableImages.indexOf(evaluatedImageName) > -1;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.input);
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
// The output of image is determined by the list of available images in the evaluation context
|
||||
return false;
|
||||
}
|
||||
}
|
||||
81
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/in.ts
generated
vendored
Normal file
81
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/in.ts
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
BooleanType,
|
||||
StringType,
|
||||
ValueType,
|
||||
NullType,
|
||||
typeToString,
|
||||
NumberType,
|
||||
isValidType,
|
||||
isValidNativeType
|
||||
} from '../types';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
import {typeOf} from '../values';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class In implements Expression {
|
||||
type: Type;
|
||||
needle: Expression;
|
||||
haystack: Expression;
|
||||
|
||||
constructor(needle: Expression, haystack: Expression) {
|
||||
this.type = BooleanType;
|
||||
this.needle = needle;
|
||||
this.haystack = haystack;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 3) {
|
||||
return context.error(
|
||||
`Expected 2 arguments, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
const needle = context.parse(args[1], 1, ValueType);
|
||||
|
||||
const haystack = context.parse(args[2], 2, ValueType);
|
||||
|
||||
if (!needle || !haystack) return null;
|
||||
|
||||
if (!isValidType(needle.type, [BooleanType, StringType, NumberType, NullType, ValueType])) {
|
||||
return context.error(
|
||||
`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(needle.type)} instead`
|
||||
) as null;
|
||||
}
|
||||
|
||||
return new In(needle, haystack);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const needle = this.needle.evaluate(ctx) as any;
|
||||
const haystack = this.haystack.evaluate(ctx) as any;
|
||||
|
||||
if (!haystack) return false;
|
||||
|
||||
if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) {
|
||||
throw new RuntimeError(
|
||||
`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(typeOf(needle))} instead.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidNativeType(haystack, ['string', 'array'])) {
|
||||
throw new RuntimeError(
|
||||
`Expected second argument to be of type array or string, but found ${typeToString(typeOf(haystack))} instead.`
|
||||
);
|
||||
}
|
||||
|
||||
return haystack.indexOf(needle) >= 0;
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.needle);
|
||||
fn(this.haystack);
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
73
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/index.ts
generated
vendored
Normal file
73
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/index.ts
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
import {Let} from './let';
|
||||
import {Var} from './var';
|
||||
import {Literal} from './literal';
|
||||
import {Assertion} from './assertion';
|
||||
import {Coercion} from './coercion';
|
||||
import {At} from './at';
|
||||
import {In} from './in';
|
||||
import {IndexOf} from './index_of';
|
||||
import {Match} from './match';
|
||||
import {Case} from './case';
|
||||
import {Slice} from './slice';
|
||||
import {Step} from './step';
|
||||
import {Interpolate} from './interpolate';
|
||||
import {Coalesce} from './coalesce';
|
||||
import {
|
||||
Equals,
|
||||
NotEquals,
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThanOrEqual
|
||||
} from './comparison';
|
||||
import {CollatorExpression} from './collator';
|
||||
import {NumberFormat} from './number_format';
|
||||
import {FormatExpression} from './format';
|
||||
import {ImageExpression} from './image';
|
||||
import {Length} from './length';
|
||||
import {Within} from './within';
|
||||
import {Distance} from './distance';
|
||||
import {GlobalState} from './global_state';
|
||||
|
||||
import type {ExpressionRegistry} from '../expression';
|
||||
|
||||
export const expressions: ExpressionRegistry = {
|
||||
// special forms
|
||||
'==': Equals,
|
||||
'!=': NotEquals,
|
||||
'>': GreaterThan,
|
||||
'<': LessThan,
|
||||
'>=': GreaterThanOrEqual,
|
||||
'<=': LessThanOrEqual,
|
||||
array: Assertion,
|
||||
at: At,
|
||||
boolean: Assertion,
|
||||
case: Case,
|
||||
coalesce: Coalesce,
|
||||
collator: CollatorExpression,
|
||||
format: FormatExpression,
|
||||
image: ImageExpression,
|
||||
in: In,
|
||||
'index-of': IndexOf,
|
||||
interpolate: Interpolate,
|
||||
'interpolate-hcl': Interpolate,
|
||||
'interpolate-lab': Interpolate,
|
||||
length: Length,
|
||||
let: Let,
|
||||
literal: Literal,
|
||||
match: Match,
|
||||
number: Assertion,
|
||||
'number-format': NumberFormat,
|
||||
object: Assertion,
|
||||
slice: Slice,
|
||||
step: Step,
|
||||
string: Assertion,
|
||||
'to-boolean': Coercion,
|
||||
'to-color': Coercion,
|
||||
'to-number': Coercion,
|
||||
'to-string': Coercion,
|
||||
var: Var,
|
||||
within: Within,
|
||||
distance: Distance,
|
||||
'global-state': GlobalState
|
||||
};
|
||||
102
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/index_of.ts
generated
vendored
Normal file
102
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/index_of.ts
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
BooleanType,
|
||||
StringType,
|
||||
ValueType,
|
||||
NullType,
|
||||
typeToString,
|
||||
NumberType,
|
||||
isValidType,
|
||||
isValidNativeType
|
||||
} from '../types';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
import {typeOf} from '../values';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class IndexOf implements Expression {
|
||||
type: Type;
|
||||
needle: Expression;
|
||||
haystack: Expression;
|
||||
fromIndex: Expression;
|
||||
|
||||
constructor(needle: Expression, haystack: Expression, fromIndex?: Expression) {
|
||||
this.type = NumberType;
|
||||
this.needle = needle;
|
||||
this.haystack = haystack;
|
||||
this.fromIndex = fromIndex;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length <= 2 || args.length >= 5) {
|
||||
return context.error(
|
||||
`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
const needle = context.parse(args[1], 1, ValueType);
|
||||
|
||||
const haystack = context.parse(args[2], 2, ValueType);
|
||||
|
||||
if (!needle || !haystack) return null;
|
||||
if (!isValidType(needle.type, [BooleanType, StringType, NumberType, NullType, ValueType])) {
|
||||
return context.error(
|
||||
`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(needle.type)} instead`
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (args.length === 4) {
|
||||
const fromIndex = context.parse(args[3], 3, NumberType);
|
||||
if (!fromIndex) return null;
|
||||
return new IndexOf(needle, haystack, fromIndex);
|
||||
} else {
|
||||
return new IndexOf(needle, haystack);
|
||||
}
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const needle = this.needle.evaluate(ctx) as any;
|
||||
const haystack = this.haystack.evaluate(ctx) as any;
|
||||
|
||||
if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) {
|
||||
throw new RuntimeError(
|
||||
`Expected first argument to be of type boolean, string, number or null, but found ${typeToString(typeOf(needle))} instead.`
|
||||
);
|
||||
}
|
||||
|
||||
let fromIndex;
|
||||
if (this.fromIndex) {
|
||||
fromIndex = this.fromIndex.evaluate(ctx) as number;
|
||||
}
|
||||
|
||||
if (isValidNativeType(haystack, ['string'])) {
|
||||
const rawIndex = haystack.indexOf(needle, fromIndex);
|
||||
if (rawIndex === -1) {
|
||||
return -1;
|
||||
} else {
|
||||
// The index may be affected by surrogate pairs, so get the length of the preceding substring.
|
||||
return [...haystack.slice(0, rawIndex)].length;
|
||||
}
|
||||
} else if (isValidNativeType(haystack, ['array'])) {
|
||||
return haystack.indexOf(needle, fromIndex);
|
||||
} else {
|
||||
throw new RuntimeError(
|
||||
`Expected second argument to be of type array or string, but found ${typeToString(typeOf(haystack))} instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.needle);
|
||||
fn(this.haystack);
|
||||
if (this.fromIndex) {
|
||||
fn(this.fromIndex);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
359
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/interpolate.ts
generated
vendored
Normal file
359
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/interpolate.ts
generated
vendored
Normal file
@@ -0,0 +1,359 @@
|
||||
import UnitBezier from '@mapbox/unitbezier';
|
||||
|
||||
import {
|
||||
array,
|
||||
ArrayType,
|
||||
ColorType,
|
||||
ColorTypeT,
|
||||
NumberType,
|
||||
NumberTypeT,
|
||||
PaddingType,
|
||||
PaddingTypeT,
|
||||
NumberArrayTypeT,
|
||||
ColorArrayTypeT,
|
||||
VariableAnchorOffsetCollectionType,
|
||||
VariableAnchorOffsetCollectionTypeT,
|
||||
typeToString,
|
||||
verifyType,
|
||||
ProjectionDefinitionType,
|
||||
ColorArrayType,
|
||||
NumberArrayType
|
||||
} from '../types';
|
||||
import {findStopLessThanOrEqualTo} from '../stops';
|
||||
import {Color} from '../types/color';
|
||||
import {interpolateArray, interpolateNumber} from '../../util/interpolate-primitives';
|
||||
import {Padding} from '../types/padding';
|
||||
import {ColorArray} from '../types/color_array';
|
||||
import {NumberArray} from '../types/number_array';
|
||||
import {VariableAnchorOffsetCollection} from '../types/variable_anchor_offset_collection';
|
||||
import {ProjectionDefinition} from '../types/projection_definition';
|
||||
|
||||
import type {Stops} from '../stops';
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {ProjectionDefinitionTypeT, Type} from '../types';
|
||||
|
||||
export type InterpolationType =
|
||||
| {
|
||||
name: 'linear';
|
||||
}
|
||||
| {
|
||||
name: 'exponential';
|
||||
base: number;
|
||||
}
|
||||
| {
|
||||
name: 'cubic-bezier';
|
||||
controlPoints: [number, number, number, number];
|
||||
};
|
||||
type InterpolatedValueType =
|
||||
| NumberTypeT
|
||||
| ColorTypeT
|
||||
| ProjectionDefinitionTypeT
|
||||
| PaddingTypeT
|
||||
| NumberArrayTypeT
|
||||
| ColorArrayTypeT
|
||||
| VariableAnchorOffsetCollectionTypeT
|
||||
| ArrayType<NumberTypeT>;
|
||||
export class Interpolate implements Expression {
|
||||
type: InterpolatedValueType;
|
||||
|
||||
operator: 'interpolate' | 'interpolate-hcl' | 'interpolate-lab';
|
||||
interpolation: InterpolationType;
|
||||
input: Expression;
|
||||
labels: Array<number>;
|
||||
outputs: Array<Expression>;
|
||||
|
||||
constructor(
|
||||
type: InterpolatedValueType,
|
||||
operator: 'interpolate' | 'interpolate-hcl' | 'interpolate-lab',
|
||||
interpolation: InterpolationType,
|
||||
input: Expression,
|
||||
stops: Stops
|
||||
) {
|
||||
this.type = type;
|
||||
this.operator = operator;
|
||||
this.interpolation = interpolation;
|
||||
this.input = input;
|
||||
|
||||
this.labels = [];
|
||||
this.outputs = [];
|
||||
for (const [label, expression] of stops) {
|
||||
this.labels.push(label);
|
||||
this.outputs.push(expression);
|
||||
}
|
||||
}
|
||||
|
||||
static interpolationFactor(
|
||||
interpolation: InterpolationType,
|
||||
input: number,
|
||||
lower: number,
|
||||
upper: number
|
||||
) {
|
||||
let t = 0;
|
||||
if (interpolation.name === 'exponential') {
|
||||
t = exponentialInterpolation(input, interpolation.base, lower, upper);
|
||||
} else if (interpolation.name === 'linear') {
|
||||
t = exponentialInterpolation(input, 1, lower, upper);
|
||||
} else if (interpolation.name === 'cubic-bezier') {
|
||||
const c = interpolation.controlPoints;
|
||||
const ub = new UnitBezier(c[0], c[1], c[2], c[3]);
|
||||
t = ub.solve(exponentialInterpolation(input, 1, lower, upper));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
let [operator, interpolation, input, ...rest] = args;
|
||||
|
||||
if (!Array.isArray(interpolation) || interpolation.length === 0) {
|
||||
return context.error('Expected an interpolation type expression.', 1) as null;
|
||||
}
|
||||
|
||||
if (interpolation[0] === 'linear') {
|
||||
interpolation = {name: 'linear'};
|
||||
} else if (interpolation[0] === 'exponential') {
|
||||
const base = interpolation[1];
|
||||
if (typeof base !== 'number')
|
||||
return context.error(
|
||||
'Exponential interpolation requires a numeric base.',
|
||||
1,
|
||||
1
|
||||
) as null;
|
||||
interpolation = {
|
||||
name: 'exponential',
|
||||
base
|
||||
};
|
||||
} else if (interpolation[0] === 'cubic-bezier') {
|
||||
const controlPoints = interpolation.slice(1);
|
||||
if (
|
||||
controlPoints.length !== 4 ||
|
||||
controlPoints.some((t) => typeof t !== 'number' || t < 0 || t > 1)
|
||||
) {
|
||||
return context.error(
|
||||
'Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.',
|
||||
1
|
||||
) as null;
|
||||
}
|
||||
|
||||
interpolation = {
|
||||
name: 'cubic-bezier',
|
||||
controlPoints: controlPoints as any
|
||||
};
|
||||
} else {
|
||||
return context.error(
|
||||
`Unknown interpolation type ${String(interpolation[0])}`,
|
||||
1,
|
||||
0
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (args.length - 1 < 4) {
|
||||
return context.error(
|
||||
`Expected at least 4 arguments, but found only ${args.length - 1}.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
if ((args.length - 1) % 2 !== 0) {
|
||||
return context.error('Expected an even number of arguments.') as null;
|
||||
}
|
||||
|
||||
input = context.parse(input, 2, NumberType);
|
||||
if (!input) return null;
|
||||
|
||||
const stops: Stops = [];
|
||||
|
||||
let outputType: Type = null;
|
||||
if (
|
||||
(operator === 'interpolate-hcl' || operator === 'interpolate-lab') &&
|
||||
context.expectedType != ColorArrayType
|
||||
) {
|
||||
outputType = ColorType;
|
||||
} else if (context.expectedType && context.expectedType.kind !== 'value') {
|
||||
outputType = context.expectedType;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rest.length; i += 2) {
|
||||
const label = rest[i];
|
||||
const value = rest[i + 1];
|
||||
|
||||
const labelKey = i + 3;
|
||||
const valueKey = i + 4;
|
||||
|
||||
if (typeof label !== 'number') {
|
||||
return context.error(
|
||||
'Input/output pairs for "interpolate" expressions must be defined using literal numeric values (not computed expressions) for the input values.',
|
||||
labelKey
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (stops.length && stops[stops.length - 1][0] >= label) {
|
||||
return context.error(
|
||||
'Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.',
|
||||
labelKey
|
||||
) as null;
|
||||
}
|
||||
const parsed = context.parse(value, valueKey, outputType);
|
||||
if (!parsed) return null;
|
||||
outputType = outputType || parsed.type;
|
||||
stops.push([label, parsed]);
|
||||
}
|
||||
|
||||
if (
|
||||
!verifyType(outputType, NumberType) &&
|
||||
!verifyType(outputType, ProjectionDefinitionType) &&
|
||||
!verifyType(outputType, ColorType) &&
|
||||
!verifyType(outputType, PaddingType) &&
|
||||
!verifyType(outputType, NumberArrayType) &&
|
||||
!verifyType(outputType, ColorArrayType) &&
|
||||
!verifyType(outputType, VariableAnchorOffsetCollectionType) &&
|
||||
!verifyType(outputType, array(NumberType))
|
||||
) {
|
||||
return context.error(`Type ${typeToString(outputType)} is not interpolatable.`) as null;
|
||||
}
|
||||
|
||||
return new Interpolate(
|
||||
outputType,
|
||||
operator as any,
|
||||
interpolation as InterpolationType,
|
||||
input as Expression,
|
||||
stops
|
||||
);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const labels = this.labels;
|
||||
const outputs = this.outputs;
|
||||
|
||||
if (labels.length === 1) {
|
||||
return outputs[0].evaluate(ctx);
|
||||
}
|
||||
|
||||
const value: number = this.input.evaluate(ctx);
|
||||
if (value <= labels[0]) {
|
||||
return outputs[0].evaluate(ctx);
|
||||
}
|
||||
|
||||
const stopCount = labels.length;
|
||||
if (value >= labels[stopCount - 1]) {
|
||||
return outputs[stopCount - 1].evaluate(ctx);
|
||||
}
|
||||
|
||||
const index = findStopLessThanOrEqualTo(labels, value);
|
||||
const lower = labels[index];
|
||||
const upper = labels[index + 1];
|
||||
const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);
|
||||
|
||||
const outputLower = outputs[index].evaluate(ctx);
|
||||
const outputUpper = outputs[index + 1].evaluate(ctx);
|
||||
|
||||
switch (this.operator) {
|
||||
case 'interpolate':
|
||||
switch (this.type.kind) {
|
||||
case 'number':
|
||||
return interpolateNumber(outputLower, outputUpper, t);
|
||||
case 'color':
|
||||
return Color.interpolate(outputLower, outputUpper, t);
|
||||
case 'padding':
|
||||
return Padding.interpolate(outputLower, outputUpper, t);
|
||||
case 'colorArray':
|
||||
return ColorArray.interpolate(outputLower, outputUpper, t);
|
||||
case 'numberArray':
|
||||
return NumberArray.interpolate(outputLower, outputUpper, t);
|
||||
case 'variableAnchorOffsetCollection':
|
||||
return VariableAnchorOffsetCollection.interpolate(
|
||||
outputLower,
|
||||
outputUpper,
|
||||
t
|
||||
);
|
||||
case 'array':
|
||||
return interpolateArray(outputLower, outputUpper, t);
|
||||
case 'projectionDefinition':
|
||||
return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
|
||||
}
|
||||
case 'interpolate-hcl':
|
||||
switch (this.type.kind) {
|
||||
case 'color':
|
||||
return Color.interpolate(outputLower, outputUpper, t, 'hcl');
|
||||
case 'colorArray':
|
||||
return ColorArray.interpolate(outputLower, outputUpper, t, 'hcl');
|
||||
}
|
||||
case 'interpolate-lab':
|
||||
switch (this.type.kind) {
|
||||
case 'color':
|
||||
return Color.interpolate(outputLower, outputUpper, t, 'lab');
|
||||
case 'colorArray':
|
||||
return ColorArray.interpolate(outputLower, outputUpper, t, 'lab');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.input);
|
||||
for (const expression of this.outputs) {
|
||||
fn(expression);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return this.outputs.every((out) => out.outputDefined());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ratio that can be used to interpolate between exponential function
|
||||
* stops.
|
||||
* How it works: Two consecutive stop values define a (scaled and shifted) exponential function `f(x) = a * base^x + b`, where `base` is the user-specified base,
|
||||
* and `a` and `b` are constants affording sufficient degrees of freedom to fit
|
||||
* the function to the given stops.
|
||||
*
|
||||
* Here's a bit of algebra that lets us compute `f(x)` directly from the stop
|
||||
* values without explicitly solving for `a` and `b`:
|
||||
*
|
||||
* First stop value: `f(x0) = y0 = a * base^x0 + b`
|
||||
* Second stop value: `f(x1) = y1 = a * base^x1 + b`
|
||||
* => `y1 - y0 = a(base^x1 - base^x0)`
|
||||
* => `a = (y1 - y0)/(base^x1 - base^x0)`
|
||||
*
|
||||
* Desired value: `f(x) = y = a * base^x + b`
|
||||
* => `f(x) = y0 + a * (base^x - base^x0)`
|
||||
*
|
||||
* From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a
|
||||
* little algebra:
|
||||
* ```
|
||||
* a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0)
|
||||
* = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0)
|
||||
* ```
|
||||
*
|
||||
* If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have
|
||||
* `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as
|
||||
* an interpolation factor between the two stops' output values.
|
||||
*
|
||||
* (Note: a slightly different form for `ratio`,
|
||||
* `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer
|
||||
* expensive `Math.pow()` operations.)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function exponentialInterpolation(input, base, lowerValue, upperValue) {
|
||||
const difference = upperValue - lowerValue;
|
||||
const progress = input - lowerValue;
|
||||
|
||||
if (difference === 0) {
|
||||
return 0;
|
||||
} else if (base === 1) {
|
||||
return progress / difference;
|
||||
} else {
|
||||
return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export const interpolateFactory = {
|
||||
color: Color.interpolate,
|
||||
number: interpolateNumber,
|
||||
padding: Padding.interpolate,
|
||||
numberArray: NumberArray.interpolate,
|
||||
colorArray: ColorArray.interpolate,
|
||||
variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
|
||||
array: interpolateArray
|
||||
};
|
||||
62
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/length.ts
generated
vendored
Normal file
62
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/length.ts
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import {NumberType, typeToString} from '../types';
|
||||
|
||||
import {typeOf} from '../values';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class Length implements Expression {
|
||||
type: Type;
|
||||
input: Expression;
|
||||
|
||||
constructor(input: Expression) {
|
||||
this.type = NumberType;
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2)
|
||||
return context.error(
|
||||
`Expected 1 argument, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
|
||||
const input = context.parse(args[1], 1);
|
||||
if (!input) return null;
|
||||
|
||||
if (
|
||||
input.type.kind !== 'array' &&
|
||||
input.type.kind !== 'string' &&
|
||||
input.type.kind !== 'value'
|
||||
)
|
||||
return context.error(
|
||||
`Expected argument of type string or array, but found ${typeToString(input.type)} instead.`
|
||||
) as null;
|
||||
|
||||
return new Length(input);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const input = this.input.evaluate(ctx);
|
||||
if (typeof input === 'string') {
|
||||
// The length may be affected by surrogate pairs.
|
||||
return [...input].length;
|
||||
} else if (Array.isArray(input)) {
|
||||
return input.length;
|
||||
} else {
|
||||
throw new RuntimeError(
|
||||
`Expected value to be of type string or array, but found ${typeToString(typeOf(input))} instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.input);
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
72
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/let.ts
generated
vendored
Normal file
72
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/let.ts
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
import type {Type} from '../types';
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
|
||||
export class Let implements Expression {
|
||||
type: Type;
|
||||
bindings: Array<[string, Expression]>;
|
||||
result: Expression;
|
||||
|
||||
constructor(bindings: Array<[string, Expression]>, result: Expression) {
|
||||
this.type = result.type;
|
||||
this.bindings = [].concat(bindings);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
return this.result.evaluate(ctx);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
for (const binding of this.bindings) {
|
||||
fn(binding[1]);
|
||||
}
|
||||
fn(this.result);
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 4)
|
||||
return context.error(
|
||||
`Expected at least 3 arguments, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
|
||||
const bindings: Array<[string, Expression]> = [];
|
||||
for (let i = 1; i < args.length - 1; i += 2) {
|
||||
const name = args[i];
|
||||
|
||||
if (typeof name !== 'string') {
|
||||
return context.error(
|
||||
`Expected string, but found ${typeof name} instead.`,
|
||||
i
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (/[^a-zA-Z0-9_]/.test(name)) {
|
||||
return context.error(
|
||||
"Variable names must contain only alphanumeric characters or '_'.",
|
||||
i
|
||||
) as null;
|
||||
}
|
||||
|
||||
const value = context.parse(args[i + 1], i + 1);
|
||||
if (!value) return null;
|
||||
|
||||
bindings.push([name, value]);
|
||||
}
|
||||
|
||||
const result = context.parse(
|
||||
args[args.length - 1],
|
||||
args.length - 1,
|
||||
context.expectedType,
|
||||
bindings
|
||||
);
|
||||
if (!result) return null;
|
||||
|
||||
return new Let(bindings, result);
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return this.result.outputDefined();
|
||||
}
|
||||
}
|
||||
52
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/literal.ts
generated
vendored
Normal file
52
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/literal.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import {isValue, typeOf} from '../values';
|
||||
|
||||
import type {Type} from '../types';
|
||||
import type {Value} from '../values';
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
|
||||
export class Literal implements Expression {
|
||||
type: Type;
|
||||
value: Value;
|
||||
|
||||
constructor(type: Type, value: Value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2)
|
||||
return context.error(
|
||||
`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
|
||||
if (!isValue(args[1])) return context.error('invalid value') as null;
|
||||
|
||||
const value = args[1] as any;
|
||||
let type = typeOf(value);
|
||||
|
||||
// special case: infer the item type if possible for zero-length arrays
|
||||
const expected = context.expectedType;
|
||||
if (
|
||||
type.kind === 'array' &&
|
||||
type.N === 0 &&
|
||||
expected &&
|
||||
expected.kind === 'array' &&
|
||||
(typeof expected.N !== 'number' || expected.N === 0)
|
||||
) {
|
||||
type = expected;
|
||||
}
|
||||
|
||||
return new Literal(type, value);
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
eachChild() {}
|
||||
|
||||
outputDefined() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
130
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/match.ts
generated
vendored
Normal file
130
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/match.ts
generated
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
import {typeOf} from '../values';
|
||||
import {ValueType} from '../types';
|
||||
|
||||
import type {Type} from '../types';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
|
||||
// Map input label values to output expression index
|
||||
type Cases = {
|
||||
[k in number | string]: number;
|
||||
};
|
||||
|
||||
export class Match implements Expression {
|
||||
type: Type;
|
||||
inputType: Type;
|
||||
|
||||
input: Expression;
|
||||
cases: Cases;
|
||||
outputs: Array<Expression>;
|
||||
otherwise: Expression;
|
||||
|
||||
constructor(
|
||||
inputType: Type,
|
||||
outputType: Type,
|
||||
input: Expression,
|
||||
cases: Cases,
|
||||
outputs: Array<Expression>,
|
||||
otherwise: Expression
|
||||
) {
|
||||
this.inputType = inputType;
|
||||
this.type = outputType;
|
||||
this.input = input;
|
||||
this.cases = cases;
|
||||
this.outputs = outputs;
|
||||
this.otherwise = otherwise;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length < 5)
|
||||
return context.error(
|
||||
`Expected at least 4 arguments, but found only ${args.length - 1}.`
|
||||
) as null;
|
||||
if (args.length % 2 !== 1)
|
||||
return context.error('Expected an even number of arguments.') as null;
|
||||
|
||||
let inputType;
|
||||
let outputType;
|
||||
if (context.expectedType && context.expectedType.kind !== 'value') {
|
||||
outputType = context.expectedType;
|
||||
}
|
||||
const cases = {};
|
||||
const outputs = [];
|
||||
for (let i = 2; i < args.length - 1; i += 2) {
|
||||
let labels = args[i] as unknown[];
|
||||
const value = args[i + 1];
|
||||
|
||||
if (!Array.isArray(labels)) {
|
||||
labels = [labels];
|
||||
}
|
||||
|
||||
const labelContext = context.concat(i);
|
||||
if (labels.length === 0) {
|
||||
return labelContext.error('Expected at least one branch label.') as null;
|
||||
}
|
||||
|
||||
for (const label of labels) {
|
||||
if (typeof label !== 'number' && typeof label !== 'string') {
|
||||
return labelContext.error('Branch labels must be numbers or strings.') as null;
|
||||
} else if (typeof label === 'number' && Math.abs(label) > Number.MAX_SAFE_INTEGER) {
|
||||
return labelContext.error(
|
||||
`Branch labels must be integers no larger than ${Number.MAX_SAFE_INTEGER}.`
|
||||
) as null;
|
||||
} else if (typeof label === 'number' && Math.floor(label) !== label) {
|
||||
return labelContext.error(
|
||||
'Numeric branch labels must be integer values.'
|
||||
) as null;
|
||||
} else if (!inputType) {
|
||||
inputType = typeOf(label);
|
||||
} else if (labelContext.checkSubtype(inputType, typeOf(label))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof cases[String(label)] !== 'undefined') {
|
||||
return labelContext.error('Branch labels must be unique.') as null;
|
||||
}
|
||||
|
||||
cases[String(label)] = outputs.length;
|
||||
}
|
||||
|
||||
const result = context.parse(value, i, outputType);
|
||||
if (!result) return null;
|
||||
outputType = outputType || result.type;
|
||||
outputs.push(result);
|
||||
}
|
||||
|
||||
const input = context.parse(args[1], 1, ValueType);
|
||||
if (!input) return null;
|
||||
|
||||
const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType);
|
||||
if (!otherwise) return null;
|
||||
|
||||
if (
|
||||
input.type.kind !== 'value' &&
|
||||
context.concat(1).checkSubtype(inputType as any, input.type)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Match(inputType as any, outputType as any, input, cases, outputs, otherwise);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const input = this.input.evaluate(ctx) as any;
|
||||
const output =
|
||||
(typeOf(input) === this.inputType && this.outputs[this.cases[input]]) || this.otherwise;
|
||||
return output.evaluate(ctx);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.input);
|
||||
this.outputs.forEach(fn);
|
||||
fn(this.otherwise);
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return this.outputs.every((out) => out.outputDefined()) && this.otherwise.outputDefined();
|
||||
}
|
||||
}
|
||||
141
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/number_format.ts
generated
vendored
Normal file
141
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/number_format.ts
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
import {StringType, NumberType} from '../types';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class NumberFormat implements Expression {
|
||||
type: Type;
|
||||
number: Expression;
|
||||
/**
|
||||
* BCP 47 language tag
|
||||
*/
|
||||
locale: Expression | null;
|
||||
/**
|
||||
* ISO 4217 currency code, required if style=currency
|
||||
*/
|
||||
currency: Expression | null;
|
||||
/**
|
||||
* CLDR or ECMA-402 unit specifier, required if style=unit
|
||||
*/
|
||||
unit: Expression | null;
|
||||
/**
|
||||
* @default 0
|
||||
*/
|
||||
minFractionDigits: Expression | null;
|
||||
/**
|
||||
* @default 3
|
||||
*/
|
||||
maxFractionDigits: Expression | null;
|
||||
|
||||
constructor(
|
||||
number: Expression,
|
||||
locale: Expression | null,
|
||||
currency: Expression | null,
|
||||
unit: Expression | null,
|
||||
minFractionDigits: Expression | null,
|
||||
maxFractionDigits: Expression | null
|
||||
) {
|
||||
this.type = StringType;
|
||||
this.number = number;
|
||||
this.locale = locale;
|
||||
this.currency = currency;
|
||||
this.unit = unit;
|
||||
this.minFractionDigits = minFractionDigits;
|
||||
this.maxFractionDigits = maxFractionDigits;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 3) return context.error('Expected two arguments.') as null;
|
||||
|
||||
const number = context.parse(args[1], 1, NumberType);
|
||||
if (!number) return null;
|
||||
|
||||
const options = args[2] as any;
|
||||
if (typeof options !== 'object' || Array.isArray(options))
|
||||
return context.error('NumberFormat options argument must be an object.') as null;
|
||||
|
||||
let locale = null;
|
||||
if (options['locale']) {
|
||||
locale = context.parse(options['locale'], 1, StringType);
|
||||
if (!locale) return null;
|
||||
}
|
||||
|
||||
let currency = null;
|
||||
if (options['currency']) {
|
||||
currency = context.parse(options['currency'], 1, StringType);
|
||||
if (!currency) return null;
|
||||
}
|
||||
|
||||
let unit = null;
|
||||
if (options['unit']) {
|
||||
unit = context.parse(options['unit'], 1, StringType);
|
||||
if (!unit) return null;
|
||||
}
|
||||
|
||||
if (currency && unit) {
|
||||
return context.error(
|
||||
'NumberFormat options `currency` and `unit` are mutually exclusive'
|
||||
) as null;
|
||||
}
|
||||
|
||||
let minFractionDigits = null;
|
||||
if (options['min-fraction-digits']) {
|
||||
minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
|
||||
if (!minFractionDigits) return null;
|
||||
}
|
||||
|
||||
let maxFractionDigits = null;
|
||||
if (options['max-fraction-digits']) {
|
||||
maxFractionDigits = context.parse(options['max-fraction-digits'], 1, NumberType);
|
||||
if (!maxFractionDigits) return null;
|
||||
}
|
||||
|
||||
return new NumberFormat(
|
||||
number,
|
||||
locale,
|
||||
currency,
|
||||
unit,
|
||||
minFractionDigits,
|
||||
maxFractionDigits
|
||||
);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
|
||||
style: this.currency ? 'currency' : this.unit ? 'unit' : 'decimal',
|
||||
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
|
||||
unit: this.unit ? this.unit.evaluate(ctx) : undefined,
|
||||
minimumFractionDigits: this.minFractionDigits
|
||||
? this.minFractionDigits.evaluate(ctx)
|
||||
: undefined,
|
||||
maximumFractionDigits: this.maxFractionDigits
|
||||
? this.maxFractionDigits.evaluate(ctx)
|
||||
: undefined
|
||||
}).format(this.number.evaluate(ctx));
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.number);
|
||||
if (this.locale) {
|
||||
fn(this.locale);
|
||||
}
|
||||
if (this.currency) {
|
||||
fn(this.currency);
|
||||
}
|
||||
if (this.unit) {
|
||||
fn(this.unit);
|
||||
}
|
||||
if (this.minFractionDigits) {
|
||||
fn(this.minFractionDigits);
|
||||
}
|
||||
if (this.maxFractionDigits) {
|
||||
fn(this.maxFractionDigits);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
90
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/slice.ts
generated
vendored
Normal file
90
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/slice.ts
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
ValueType,
|
||||
NumberType,
|
||||
StringType,
|
||||
array,
|
||||
typeToString,
|
||||
isValidType,
|
||||
isValidNativeType
|
||||
} from '../types';
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
import {typeOf} from '../values';
|
||||
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class Slice implements Expression {
|
||||
type: Type;
|
||||
input: Expression;
|
||||
beginIndex: Expression;
|
||||
endIndex: Expression;
|
||||
|
||||
constructor(type: Type, input: Expression, beginIndex: Expression, endIndex?: Expression) {
|
||||
this.type = type;
|
||||
this.input = input;
|
||||
this.beginIndex = beginIndex;
|
||||
this.endIndex = endIndex;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length <= 2 || args.length >= 5) {
|
||||
return context.error(
|
||||
`Expected 2 or 3 arguments, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
const input = context.parse(args[1], 1, ValueType);
|
||||
const beginIndex = context.parse(args[2], 2, NumberType);
|
||||
|
||||
if (!input || !beginIndex) return null;
|
||||
|
||||
if (!isValidType(input.type, [array(ValueType), StringType, ValueType])) {
|
||||
return context.error(
|
||||
`Expected first argument to be of type array or string, but found ${typeToString(input.type)} instead`
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (args.length === 4) {
|
||||
const endIndex = context.parse(args[3], 3, NumberType);
|
||||
if (!endIndex) return null;
|
||||
return new Slice(input.type, input, beginIndex, endIndex);
|
||||
} else {
|
||||
return new Slice(input.type, input, beginIndex);
|
||||
}
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const input = this.input.evaluate(ctx) as any;
|
||||
const beginIndex = this.beginIndex.evaluate(ctx) as number;
|
||||
|
||||
let endIndex;
|
||||
if (this.endIndex) {
|
||||
endIndex = this.endIndex.evaluate(ctx) as number;
|
||||
}
|
||||
|
||||
if (isValidNativeType(input, ['string'])) {
|
||||
// Indices may be affected by surrogate pairs.
|
||||
return [...input].slice(beginIndex, endIndex).join('');
|
||||
} else if (isValidNativeType(input, ['array'])) {
|
||||
return input.slice(beginIndex, endIndex);
|
||||
} else {
|
||||
throw new RuntimeError(
|
||||
`Expected first argument to be of type array or string, but found ${typeToString(typeOf(input))} instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.input);
|
||||
fn(this.beginIndex);
|
||||
if (this.endIndex) {
|
||||
fn(this.endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
113
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/step.ts
generated
vendored
Normal file
113
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/step.ts
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
import {NumberType} from '../types';
|
||||
|
||||
import {findStopLessThanOrEqualTo} from '../stops';
|
||||
|
||||
import type {Stops} from '../stops';
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import type {Type} from '../types';
|
||||
|
||||
export class Step implements Expression {
|
||||
type: Type;
|
||||
|
||||
input: Expression;
|
||||
labels: Array<number>;
|
||||
outputs: Array<Expression>;
|
||||
|
||||
constructor(type: Type, input: Expression, stops: Stops) {
|
||||
this.type = type;
|
||||
this.input = input;
|
||||
|
||||
this.labels = [];
|
||||
this.outputs = [];
|
||||
for (const [label, expression] of stops) {
|
||||
this.labels.push(label);
|
||||
this.outputs.push(expression);
|
||||
}
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length - 1 < 4) {
|
||||
return context.error(
|
||||
`Expected at least 4 arguments, but found only ${args.length - 1}.`
|
||||
) as null;
|
||||
}
|
||||
|
||||
if ((args.length - 1) % 2 !== 0) {
|
||||
return context.error('Expected an even number of arguments.') as null;
|
||||
}
|
||||
|
||||
const input = context.parse(args[1], 1, NumberType);
|
||||
if (!input) return null;
|
||||
|
||||
const stops: Stops = [];
|
||||
|
||||
let outputType: Type = null;
|
||||
if (context.expectedType && context.expectedType.kind !== 'value') {
|
||||
outputType = context.expectedType;
|
||||
}
|
||||
|
||||
for (let i = 1; i < args.length; i += 2) {
|
||||
const label = i === 1 ? -Infinity : args[i];
|
||||
const value = args[i + 1];
|
||||
|
||||
const labelKey = i;
|
||||
const valueKey = i + 1;
|
||||
|
||||
if (typeof label !== 'number') {
|
||||
return context.error(
|
||||
'Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values.',
|
||||
labelKey
|
||||
) as null;
|
||||
}
|
||||
|
||||
if (stops.length && stops[stops.length - 1][0] >= label) {
|
||||
return context.error(
|
||||
'Input/output pairs for "step" expressions must be arranged with input values in strictly ascending order.',
|
||||
labelKey
|
||||
) as null;
|
||||
}
|
||||
|
||||
const parsed = context.parse(value, valueKey, outputType);
|
||||
if (!parsed) return null;
|
||||
outputType = outputType || parsed.type;
|
||||
stops.push([label, parsed]);
|
||||
}
|
||||
|
||||
return new Step(outputType, input, stops);
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
const labels = this.labels;
|
||||
const outputs = this.outputs;
|
||||
|
||||
if (labels.length === 1) {
|
||||
return outputs[0].evaluate(ctx);
|
||||
}
|
||||
|
||||
const value = this.input.evaluate(ctx) as any as number;
|
||||
if (value <= labels[0]) {
|
||||
return outputs[0].evaluate(ctx);
|
||||
}
|
||||
|
||||
const stopCount = labels.length;
|
||||
if (value >= labels[stopCount - 1]) {
|
||||
return outputs[stopCount - 1].evaluate(ctx);
|
||||
}
|
||||
|
||||
const index = findStopLessThanOrEqualTo(labels, value);
|
||||
return outputs[index].evaluate(ctx);
|
||||
}
|
||||
|
||||
eachChild(fn: (_: Expression) => void) {
|
||||
fn(this.input);
|
||||
for (const expression of this.outputs) {
|
||||
fn(expression);
|
||||
}
|
||||
}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return this.outputs.every((out) => out.outputDefined());
|
||||
}
|
||||
}
|
||||
43
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/var.ts
generated
vendored
Normal file
43
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/var.ts
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import type {Type} from '../types';
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
|
||||
export class Var implements Expression {
|
||||
type: Type;
|
||||
name: string;
|
||||
boundExpression: Expression;
|
||||
|
||||
constructor(name: string, boundExpression: Expression) {
|
||||
this.type = boundExpression.type;
|
||||
this.name = name;
|
||||
this.boundExpression = boundExpression;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2 || typeof args[1] !== 'string')
|
||||
return context.error(
|
||||
"'var' expression requires exactly one string literal argument."
|
||||
) as null;
|
||||
|
||||
const name = args[1];
|
||||
if (!context.scope.has(name)) {
|
||||
return context.error(
|
||||
`Unknown variable "${name}". Make sure "${name}" has been bound in an enclosing "let" expression before using it.`,
|
||||
1
|
||||
) as null;
|
||||
}
|
||||
|
||||
return new Var(name, context.scope.get(name));
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
return this.boundExpression.evaluate(ctx);
|
||||
}
|
||||
|
||||
eachChild() {}
|
||||
|
||||
outputDefined() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
250
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/within.ts
generated
vendored
Normal file
250
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/definitions/within.ts
generated
vendored
Normal file
@@ -0,0 +1,250 @@
|
||||
import {isValue} from '../values';
|
||||
import type {Type} from '../types';
|
||||
import {BooleanType} from '../types';
|
||||
import type {Expression} from '../expression';
|
||||
import type {ParsingContext} from '../parsing_context';
|
||||
import type {EvaluationContext} from '../evaluation_context';
|
||||
import {ICanonicalTileID} from '../../tiles_and_coordinates';
|
||||
import {
|
||||
BBox,
|
||||
EXTENT,
|
||||
boxWithinBox,
|
||||
getTileCoordinates,
|
||||
lineStringWithinPolygon,
|
||||
lineStringWithinPolygons,
|
||||
pointWithinPolygon,
|
||||
pointWithinPolygons,
|
||||
updateBBox
|
||||
} from '../../util/geometry_util';
|
||||
import {Point2D} from '../../point2d';
|
||||
|
||||
type GeoJSONPolygons = GeoJSON.Polygon | GeoJSON.MultiPolygon;
|
||||
|
||||
function getTilePolygon(
|
||||
coordinates: GeoJSON.Position[][],
|
||||
bbox: BBox,
|
||||
canonical: ICanonicalTileID
|
||||
) {
|
||||
const polygon = [];
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
const ring = [];
|
||||
for (let j = 0; j < coordinates[i].length; j++) {
|
||||
const coord = getTileCoordinates(coordinates[i][j], canonical);
|
||||
updateBBox(bbox, coord);
|
||||
ring.push(coord);
|
||||
}
|
||||
polygon.push(ring);
|
||||
}
|
||||
return polygon;
|
||||
}
|
||||
|
||||
function getTilePolygons(
|
||||
coordinates: GeoJSON.Position[][][],
|
||||
bbox: BBox,
|
||||
canonical: ICanonicalTileID
|
||||
) {
|
||||
const polygons = [];
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
const polygon = getTilePolygon(coordinates[i], bbox, canonical);
|
||||
polygons.push(polygon);
|
||||
}
|
||||
return polygons;
|
||||
}
|
||||
|
||||
function updatePoint(p: GeoJSON.Position, bbox: BBox, polyBBox: BBox, worldSize: number) {
|
||||
if (p[0] < polyBBox[0] || p[0] > polyBBox[2]) {
|
||||
const halfWorldSize = worldSize * 0.5;
|
||||
let shift =
|
||||
p[0] - polyBBox[0] > halfWorldSize
|
||||
? -worldSize
|
||||
: polyBBox[0] - p[0] > halfWorldSize
|
||||
? worldSize
|
||||
: 0;
|
||||
if (shift === 0) {
|
||||
shift =
|
||||
p[0] - polyBBox[2] > halfWorldSize
|
||||
? -worldSize
|
||||
: polyBBox[2] - p[0] > halfWorldSize
|
||||
? worldSize
|
||||
: 0;
|
||||
}
|
||||
p[0] += shift;
|
||||
}
|
||||
updateBBox(bbox, p);
|
||||
}
|
||||
|
||||
function resetBBox(bbox: BBox) {
|
||||
bbox[0] = bbox[1] = Infinity;
|
||||
bbox[2] = bbox[3] = -Infinity;
|
||||
}
|
||||
|
||||
function getTilePoints(
|
||||
geometry: Point2D[][],
|
||||
pointBBox: BBox,
|
||||
polyBBox: BBox,
|
||||
canonical: ICanonicalTileID
|
||||
): [number, number][] {
|
||||
const worldSize = Math.pow(2, canonical.z) * EXTENT;
|
||||
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
|
||||
const tilePoints: [number, number][] = [];
|
||||
for (const points of geometry) {
|
||||
for (const point of points) {
|
||||
const p: [number, number] = [point.x + shifts[0], point.y + shifts[1]];
|
||||
updatePoint(p, pointBBox, polyBBox, worldSize);
|
||||
tilePoints.push(p);
|
||||
}
|
||||
}
|
||||
return tilePoints;
|
||||
}
|
||||
|
||||
function getTileLines(
|
||||
geometry: Point2D[][],
|
||||
lineBBox: BBox,
|
||||
polyBBox: BBox,
|
||||
canonical: ICanonicalTileID
|
||||
): [number, number][][] {
|
||||
const worldSize = Math.pow(2, canonical.z) * EXTENT;
|
||||
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
|
||||
const tileLines: [number, number][][] = [];
|
||||
for (const line of geometry) {
|
||||
const tileLine: [number, number][] = [];
|
||||
for (const point of line) {
|
||||
const p: [number, number] = [point.x + shifts[0], point.y + shifts[1]];
|
||||
updateBBox(lineBBox, p);
|
||||
tileLine.push(p);
|
||||
}
|
||||
tileLines.push(tileLine);
|
||||
}
|
||||
if (lineBBox[2] - lineBBox[0] <= worldSize / 2) {
|
||||
resetBBox(lineBBox);
|
||||
for (const line of tileLines) {
|
||||
for (const p of line) {
|
||||
updatePoint(p, lineBBox, polyBBox, worldSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tileLines;
|
||||
}
|
||||
|
||||
function pointsWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
|
||||
const pointBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
const polyBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
|
||||
const canonical = ctx.canonicalID();
|
||||
|
||||
if (polygonGeometry.type === 'Polygon') {
|
||||
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
|
||||
const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical);
|
||||
if (!boxWithinBox(pointBBox, polyBBox)) return false;
|
||||
|
||||
for (const point of tilePoints) {
|
||||
if (!pointWithinPolygon(point, tilePolygon)) return false;
|
||||
}
|
||||
}
|
||||
if (polygonGeometry.type === 'MultiPolygon') {
|
||||
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
|
||||
const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical);
|
||||
if (!boxWithinBox(pointBBox, polyBBox)) return false;
|
||||
|
||||
for (const point of tilePoints) {
|
||||
if (!pointWithinPolygons(point, tilePolygons)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function linesWithinPolygons(ctx: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
|
||||
const lineBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
const polyBBox: BBox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
|
||||
const canonical = ctx.canonicalID();
|
||||
|
||||
if (polygonGeometry.type === 'Polygon') {
|
||||
const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical);
|
||||
const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical);
|
||||
if (!boxWithinBox(lineBBox, polyBBox)) return false;
|
||||
|
||||
for (const line of tileLines) {
|
||||
if (!lineStringWithinPolygon(line, tilePolygon)) return false;
|
||||
}
|
||||
}
|
||||
if (polygonGeometry.type === 'MultiPolygon') {
|
||||
const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical);
|
||||
const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical);
|
||||
if (!boxWithinBox(lineBBox, polyBBox)) return false;
|
||||
|
||||
for (const line of tileLines) {
|
||||
if (!lineStringWithinPolygons(line, tilePolygons)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class Within implements Expression {
|
||||
type: Type;
|
||||
geojson: GeoJSON.GeoJSON;
|
||||
geometries: GeoJSONPolygons;
|
||||
|
||||
constructor(geojson: GeoJSON.GeoJSON, geometries: GeoJSONPolygons) {
|
||||
this.type = BooleanType;
|
||||
this.geojson = geojson;
|
||||
this.geometries = geometries;
|
||||
}
|
||||
|
||||
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
|
||||
if (args.length !== 2)
|
||||
return context.error(
|
||||
`'within' expression requires exactly one argument, but found ${args.length - 1} instead.`
|
||||
) as null;
|
||||
if (isValue(args[1])) {
|
||||
const geojson = args[1] as any;
|
||||
if (geojson.type === 'FeatureCollection') {
|
||||
const polygonsCoords: GeoJSON.Position[][][] = [];
|
||||
for (const polygon of geojson.features) {
|
||||
const {type, coordinates} = polygon.geometry;
|
||||
if (type === 'Polygon') {
|
||||
polygonsCoords.push(coordinates);
|
||||
}
|
||||
if (type === 'MultiPolygon') {
|
||||
polygonsCoords.push(...coordinates);
|
||||
}
|
||||
}
|
||||
if (polygonsCoords.length) {
|
||||
const multipolygonWrapper: GeoJSON.MultiPolygon = {
|
||||
type: 'MultiPolygon',
|
||||
coordinates: polygonsCoords
|
||||
};
|
||||
return new Within(geojson, multipolygonWrapper);
|
||||
}
|
||||
} else if (geojson.type === 'Feature') {
|
||||
const type = geojson.geometry.type;
|
||||
if (type === 'Polygon' || type === 'MultiPolygon') {
|
||||
return new Within(geojson, geojson.geometry);
|
||||
}
|
||||
} else if (geojson.type === 'Polygon' || geojson.type === 'MultiPolygon') {
|
||||
return new Within(geojson, geojson);
|
||||
}
|
||||
}
|
||||
return context.error(
|
||||
"'within' expression requires valid geojson object that contains polygon geometry type."
|
||||
) as null;
|
||||
}
|
||||
|
||||
evaluate(ctx: EvaluationContext) {
|
||||
if (ctx.geometry() != null && ctx.canonicalID() != null) {
|
||||
if (ctx.geometryType() === 'Point') {
|
||||
return pointsWithinPolygons(ctx, this.geometries);
|
||||
} else if (ctx.geometryType() === 'LineString') {
|
||||
return linesWithinPolygons(ctx, this.geometries);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
eachChild() {}
|
||||
|
||||
outputDefined(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
60
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/evaluation_context.ts
generated
vendored
Normal file
60
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/evaluation_context.ts
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
import type {FormattedSection} from './types/formatted';
|
||||
import type {GlobalProperties, Feature, FeatureState} from './index';
|
||||
import {ICanonicalTileID} from '../tiles_and_coordinates';
|
||||
import {Color} from './types/color';
|
||||
|
||||
const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
|
||||
|
||||
export class EvaluationContext {
|
||||
globals: GlobalProperties;
|
||||
feature: Feature;
|
||||
featureState: FeatureState;
|
||||
formattedSection: FormattedSection;
|
||||
availableImages: Array<string>;
|
||||
canonical: ICanonicalTileID;
|
||||
|
||||
_parseColorCache: Map<string, Color>;
|
||||
|
||||
constructor() {
|
||||
this.globals = null;
|
||||
this.feature = null;
|
||||
this.featureState = null;
|
||||
this.formattedSection = null;
|
||||
this._parseColorCache = new Map<string, Color>();
|
||||
this.availableImages = null;
|
||||
this.canonical = null;
|
||||
}
|
||||
|
||||
id() {
|
||||
return this.feature && 'id' in this.feature ? this.feature.id : null;
|
||||
}
|
||||
|
||||
geometryType() {
|
||||
return this.feature
|
||||
? typeof this.feature.type === 'number'
|
||||
? geometryTypes[this.feature.type]
|
||||
: this.feature.type
|
||||
: null;
|
||||
}
|
||||
|
||||
geometry() {
|
||||
return this.feature && 'geometry' in this.feature ? this.feature.geometry : null;
|
||||
}
|
||||
|
||||
canonicalID() {
|
||||
return this.canonical;
|
||||
}
|
||||
|
||||
properties() {
|
||||
return (this.feature && this.feature.properties) || {};
|
||||
}
|
||||
|
||||
parseColor(input: string): Color {
|
||||
let cached = this._parseColorCache.get(input);
|
||||
if (!cached) {
|
||||
cached = Color.parse(input);
|
||||
this._parseColorCache.set(input, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
910
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/expression.test-d.ts
generated
vendored
Normal file
910
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/expression.test-d.ts
generated
vendored
Normal file
@@ -0,0 +1,910 @@
|
||||
import {describe, expectTypeOf, test} from 'vitest';
|
||||
import type {ExpressionInputType, ExpressionSpecification} from '../types.g';
|
||||
|
||||
describe('Distance expression', () => {
|
||||
describe('Invalid expression', () => {
|
||||
test('missing geometry typecheck', () => {
|
||||
expectTypeOf<['distance']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('invalid geometry typecheck', () => {
|
||||
expectTypeOf<['distance', {type: 'Nope!'}]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('expression as geometry typecheck', () => {
|
||||
expectTypeOf<
|
||||
['distance', ['literal', {type: 'MultiPoint'; coordinates: [[3, 3], [3, 4]]}]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid expression', () => {
|
||||
test('multi point geometry typecheck', () => {
|
||||
expectTypeOf<
|
||||
['distance', {type: 'MultiPoint'; coordinates: [[3, 3], [3, 4]]}]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('multi line geometry typecheck', () => {
|
||||
expectTypeOf<
|
||||
['distance', {type: 'MultiLineString'; coordinates: [[[3, 3], [3, 4]]]}]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('multi polygon geometry typecheck', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'distance',
|
||||
{
|
||||
type: 'MultiPolygon';
|
||||
coordinates: [[[[3, 3], [3, 4], [4, 4], [4, 3], [3, 3]]]];
|
||||
}
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"array" expression', () => {
|
||||
test('type requires an expression as the input value', () => {
|
||||
expectTypeOf<['array', 1, 2, 3]>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['array', [1, 2, 3]]>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['array', 'number', [1, 2, 3]]>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['array', 'number', 3, [1, 2, 3]]>().not.toExtend<ExpressionSpecification>();
|
||||
|
||||
expectTypeOf<['array', ['literal', []]]>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['array', 'number', ['literal', [1, 2, 3]]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['array', 'number', typeof Number.MAX_SAFE_INTEGER, ['get', 'arr']]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires either "string", "number", or "boolean" as the asserted type', () => {
|
||||
expectTypeOf<['array', 0, ['literal', []]]>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['array', '0', ['literal', []]]>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['array', ['literal', 'number'], ['literal', []]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
|
||||
expectTypeOf<['array', 'string', ['literal', []]]>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['array', 'number', ['literal', []]]>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['array', 'boolean', ['literal', []]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a number literal as the asserted length', () => {
|
||||
expectTypeOf<
|
||||
['array', 'string', '0', ['literal', []]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['array', 'string', ['literal', 0], ['literal', []]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
|
||||
expectTypeOf<['array', 'string', 0, ['literal', []]]>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['array', 'string', 2, ['literal', ['one', 'two']]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"format" expression', () => {
|
||||
test('type rejects bare string arrays in the "text-font" style override', () => {
|
||||
expectTypeOf<
|
||||
['format', 'foo', {'text-font': ['Helvetica', 'Arial']}]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which scales text', () => {
|
||||
expectTypeOf<
|
||||
['format', ['get', 'title'], {'font-scale': 0.8}]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires either "bottom", "center", or "top" as the vertical alignment', () => {
|
||||
expectTypeOf<
|
||||
['format', 'foo', {'vertical-align': 'middle'}]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which aligns a text section vertically', () => {
|
||||
expectTypeOf<
|
||||
['format', 'foo', {'vertical-align': 'top'}]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which aligns an image vertically', () => {
|
||||
expectTypeOf<
|
||||
['format', ['image', 'bar'], {'vertical-align': 'bottom'}]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which applies multiple style overrides', () => {
|
||||
expectTypeOf<
|
||||
['format', 'foo', {'font-scale': 0.8; 'text-color': '#fff'}]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which applies default styles with an empty overrides object', () => {
|
||||
expectTypeOf<['format', ['downcase', 'BaR'], {}]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"image" expression', () => {
|
||||
test('type requires a string as the image name argument', () => {
|
||||
expectTypeOf<['image', true]>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<['image', 123]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which returns an image with a string literal as the image name', () => {
|
||||
expectTypeOf<['image', 'foo']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts an expression which returns an image with an expression as the image name', () => {
|
||||
expectTypeOf<['image', ['concat', 'foo', 'bar']]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"typeof" expression', () => {
|
||||
test('type requires a value argument', () => {
|
||||
expectTypeOf<['typeof']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a second argument', () => {
|
||||
expectTypeOf<['typeof', true, 42]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which returns a string describing the type of the given literal value', () => {
|
||||
expectTypeOf<['typeof', true]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which returns a string describing the type of the given expression value', () => {
|
||||
expectTypeOf<
|
||||
['typeof', ['concat', 'foo', ['to-string', 0]]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"feature-state" expression', () => {
|
||||
test('type accepts expression which retrieves the feature state with a string literal argument', () => {
|
||||
expectTypeOf<['feature-state', 'foo']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which retrieves the feature state with an expression argument', () => {
|
||||
expectTypeOf<['feature-state', ['get', 'feat-prop']]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"get" expression', () => {
|
||||
test('type requires an expression as the object argument if provided', () => {
|
||||
expectTypeOf<['get', 'prop', {prop: 4}]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which retrieves a property value from the given object argument', () => {
|
||||
expectTypeOf<['get', 'prop', ['literal', {prop: 4}]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"global-state" expression', () => {
|
||||
test('type requires a property argument', () => {
|
||||
expectTypeOf<['global-state']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string literal as the property argument', () => {
|
||||
expectTypeOf<
|
||||
['global-state', ['concat', 'pr', 'op']]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a second argument', () => {
|
||||
expectTypeOf<['global-state', 'foo', 'bar']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which evaluates a global state property', () => {
|
||||
expectTypeOf<['global-state', 'foo']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"has" expression', () => {
|
||||
test('type requires an expression as the object argument if provided', () => {
|
||||
expectTypeOf<['has', 'prop', {prop: 4}]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which checks whether a property exists in the given object argument', () => {
|
||||
expectTypeOf<['has', 'prop', ['literal', {prop: 4}]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"at" expression', () => {
|
||||
test('type accepts expression which retrieves the item at the specified index in the given array', () => {
|
||||
expectTypeOf<['at', 2, ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"in" expression', () => {
|
||||
test('type requires a needle', () => {
|
||||
expectTypeOf<['in']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a haystack', () => {
|
||||
expectTypeOf<['in', 'a']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a third argument', () => {
|
||||
expectTypeOf<['in', 'a', 'abc', 1]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string or array as the haystack', () => {
|
||||
expectTypeOf<['in', 't', true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds a substring in a string', () => {
|
||||
expectTypeOf<['in', 'b', 'abc']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds a non-literal substring in a string', () => {
|
||||
expectTypeOf<
|
||||
['in', ['downcase', 'C'], ['concat', 'ab', 'cd']]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds an element in an array', () => {
|
||||
expectTypeOf<['in', 2, ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds a non-literal element in an array', () => {
|
||||
expectTypeOf<
|
||||
['in', ['*', 2, 5], ['literal', [1, 10, 100]]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"index-of" expression', () => {
|
||||
test('type requires a needle', () => {
|
||||
expectTypeOf<['index-of']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a haystack', () => {
|
||||
expectTypeOf<['index-of', 'a']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a fourth argument', () => {
|
||||
expectTypeOf<['index-of', 'a', 'abc', 1, 8]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string or array as the haystack', () => {
|
||||
expectTypeOf<['index-of', 't', true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds a substring in a string', () => {
|
||||
expectTypeOf<['index-of', 'b', 'abc']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds a non-literal substring in a string', () => {
|
||||
expectTypeOf<
|
||||
['index-of', ['downcase', 'C'], ['concat', 'ab', 'cd']]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which starts looking for the substring at a start index', () => {
|
||||
expectTypeOf<['index-of', 'a', 'abc', 1]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which starts looking for the substring at a non-literal start index', () => {
|
||||
expectTypeOf<['index-of', 'c', 'abc', ['-', 0, 1]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds an element in an array', () => {
|
||||
expectTypeOf<['index-of', 2, ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which finds a non-literal element in an array', () => {
|
||||
expectTypeOf<
|
||||
['index-of', ['*', 2, 5], ['literal', [1, 10, 100]]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which starts looking for the element at a start index', () => {
|
||||
expectTypeOf<
|
||||
['index-of', 1, ['literal', [1, 2, 3]], 1]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which starts looking for the element at a non-literal start index', () => {
|
||||
expectTypeOf<
|
||||
['index-of', 2, ['literal', [1, 2, 3]], ['+', 0, -1, 2]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"length" expression', () => {
|
||||
test('type requires an argument', () => {
|
||||
expectTypeOf<['length']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string or array as the argument', () => {
|
||||
expectTypeOf<['length', true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a second argument', () => {
|
||||
expectTypeOf<['length', 'abc', 'def']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which measures a string', () => {
|
||||
expectTypeOf<['length', 'abc']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which measures an array', () => {
|
||||
expectTypeOf<['length', ['literal', [1, 2, 3]]]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"slice" expression', () => {
|
||||
test('type requires an input argument', () => {
|
||||
expectTypeOf<['slice']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a start index argument', () => {
|
||||
expectTypeOf<['slice', 'abc']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a fourth argument', () => {
|
||||
expectTypeOf<['slice', 'abc', 0, 1, 8]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string or array as the input argument', () => {
|
||||
expectTypeOf<['slice', true, 0]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a number as the start index argument', () => {
|
||||
expectTypeOf<['slice', 'abc', true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which slices a string', () => {
|
||||
expectTypeOf<['slice', 'abc', 1]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which slices a string by a given range', () => {
|
||||
expectTypeOf<['slice', 'abc', 1, 1]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which slices an array', () => {
|
||||
expectTypeOf<['slice', ['literal', [1, 2, 3]], 1]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which slices an array by a given range', () => {
|
||||
expectTypeOf<['slice', ['literal', [1, 2, 3]], 1, 1]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"split" expression', () => {
|
||||
test('type requires an input argument', () => {
|
||||
expectTypeOf<['split']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a separator argument', () => {
|
||||
expectTypeOf<['split', '1+2+3']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string as the input argument', () => {
|
||||
expectTypeOf<['split', true, '+']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string as the separator argument', () => {
|
||||
expectTypeOf<['split', '1+2+3', true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which splits a string', () => {
|
||||
expectTypeOf<['split', '1+2+3', '+']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"join" expression', () => {
|
||||
test('type requires an input argument', () => {
|
||||
expectTypeOf<['join']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a separator argument', () => {
|
||||
expectTypeOf<
|
||||
['join', ['literal', ['1', '2', '3']]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type requires a string as the separator argument', () => {
|
||||
expectTypeOf<
|
||||
['join', ['literal', ['1', '2', '3']], true]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which joins an array', () => {
|
||||
expectTypeOf<
|
||||
['join', ['literal', ['1', '2', '3']], '+']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('comparison expressions', () => {
|
||||
describe('"!=" expression', () => {
|
||||
test('type accepts expression which compares against literal null value', () => {
|
||||
expectTypeOf<
|
||||
['!=', null, ['get', 'nonexistent-prop']]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('allows the third argument to be a collator', () => {
|
||||
expectTypeOf<
|
||||
['!=', null, ['get', 'nonexistent-prop'], ['collator', {locale: 'mi-NZ'}]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('allows the third argument to be a var', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'let',
|
||||
'myVariable',
|
||||
['collator', {'diacritic-sensitive': true}],
|
||||
['!=', 'münchen', 'munchen', ['var', 'myVariable']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('does not allow the third argument to be any other expression type', () => {
|
||||
expectTypeOf<
|
||||
['!=', 'münchen', 'munchen', ['!', true]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
describe('"==" expression', () => {
|
||||
test('type accepts expression which compares expression input against literal input', () => {
|
||||
expectTypeOf<
|
||||
['==', ['get', 'MILITARYAIRPORT'], 1]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which compares against literal null value', () => {
|
||||
expectTypeOf<
|
||||
['==', null, ['get', 'nonexistent-prop']]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('allows the third argument to be a collator', () => {
|
||||
expectTypeOf<
|
||||
['==', null, ['get', 'nonexistent-prop'], ['collator', {locale: 'mi-NZ'}]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('allows the third argument to be a var', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'let',
|
||||
'myVariable',
|
||||
['collator', {'diacritic-sensitive': true}],
|
||||
['==', 'münchen', 'munchen', ['var', 'myVariable']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('does not allow the third argument to be any other expression type', () => {
|
||||
expectTypeOf<
|
||||
['==', 'münchen', 'munchen', ['!', true]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
describe('"<" expression', () => {
|
||||
test('type rejects boolean input', () => {
|
||||
expectTypeOf<['<', -1, true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('allows the third argument to be a collator', () => {
|
||||
expectTypeOf<
|
||||
['<', 'ä', 'a', ['collator', {locale: 'sv'}]]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('allows the third argument to be a var', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'let',
|
||||
'myVariable',
|
||||
['collator', {locale: 'sv'}],
|
||||
['<', 'ä', 'a', ['var', 'myVariable']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('does not allow the third argument to be any other expression type', () => {
|
||||
expectTypeOf<
|
||||
['<', 'ä', 'a', ['get', 'prop']]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
describe('"<=" expression', () => {
|
||||
test('type rejects boolean input', () => {
|
||||
expectTypeOf<['<=', 0, true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
describe('">" expression', () => {
|
||||
test('type rejects boolean input', () => {
|
||||
expectTypeOf<['>', 1, true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
describe('">=" expression', () => {
|
||||
test('type rejects boolean input', () => {
|
||||
expectTypeOf<['>=', 1, true]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"any" expression', () => {
|
||||
test('type accepts expression which has no arguments', () => {
|
||||
expectTypeOf<['any']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"case" expression', () => {
|
||||
test('type accepts expression which returns the string output of the first matching condition', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'case',
|
||||
['==', ['get', 'CAPITAL'], 1],
|
||||
'city-capital',
|
||||
['>=', ['get', 'POPULATION'], 1000000],
|
||||
'city-1M',
|
||||
['>=', ['get', 'POPULATION'], 500000],
|
||||
'city-500k',
|
||||
['>=', ['get', 'POPULATION'], 100000],
|
||||
'city-100k',
|
||||
'city'
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which returns the evaluated output of the first matching condition', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'case',
|
||||
['has', 'point_count'],
|
||||
['interpolate', ['linear'], ['get', 'point_count'], 2, '#ccc', 10, '#444'],
|
||||
['has', 'priorityValue'],
|
||||
['interpolate', ['linear'], ['get', 'priorityValue'], 0, '#ff9', 1, '#f66'],
|
||||
'#fcaf3e'
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which has literal null output', () => {
|
||||
expectTypeOf<
|
||||
['case', false, ['get', 'prop'], true, null, 'fallback']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which has literal null fallback', () => {
|
||||
expectTypeOf<['case', false, ['get', 'prop'], null]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"match" expression', () => {
|
||||
test('type requires label to be string literal, number literal, string literal array, or number literal array', () => {
|
||||
expectTypeOf<
|
||||
['match', 4, true, 'matched', 'fallback']
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['match', 4, [true], 'matched', 'fallback']
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['match', 4, [4, '4'], 'matched', 'fallback']
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['match', 4, ['literal', [4]], 'matched', 'fallback']
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which matches number input against number label', () => {
|
||||
expectTypeOf<
|
||||
['match', 2, [0], 'o1', 1, 'o2', 2, 'o3', 'fallback']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which matches string input against string label', () => {
|
||||
expectTypeOf<
|
||||
['match', 'c', 'a', 'o1', ['b'], 'o2', 'c', 'o3', 'fallback']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which matches number input against number array label', () => {
|
||||
expectTypeOf<
|
||||
['match', 2, 0, 'o1', [1, 2, 3], 'o2', 'fallback']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which matches string input against string array label', () => {
|
||||
expectTypeOf<
|
||||
['match', 'c', 'a', 'o1', ['b', 'c', 'd'], 'o2', 'fallback']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which has a non-literal input', () => {
|
||||
expectTypeOf<
|
||||
['match', ['get', 'TYPE'], ['ADIZ', 'AMA', 'AWY'], true, false]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which has an expression output', () => {
|
||||
expectTypeOf<
|
||||
['match', ['get', 'id'], 'exampleID', ['get', 'iconNameFocused'], ['get', 'iconName']]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which has literal null output', () => {
|
||||
expectTypeOf<
|
||||
['match', 1, 0, ['get', 'prop'], 1, null, 'fallback']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"within" expression', () => {
|
||||
test('type requires a GeoJSON input', () => {
|
||||
expectTypeOf<['within']>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects an expression as input', () => {
|
||||
expectTypeOf<
|
||||
['within', ['literal', {type: 'Polygon'; coordinates: []}]]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a second argument', () => {
|
||||
expectTypeOf<
|
||||
['within', {type: 'Polygon'; coordinates: []}, 'second arg']
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which checks if feature fully contained within input GeoJSON geometry', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'within',
|
||||
{
|
||||
type: 'Polygon';
|
||||
coordinates: [[[0, 0], [0, 5], [5, 5], [5, 0], [0, 0]]];
|
||||
}
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolation expressions', () => {
|
||||
describe('linear interpolation type', () => {
|
||||
test('type works with "interpolate" expression', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['zoom'], 0, 10, 1, 20]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('exponential interpolation type', () => {
|
||||
test('type requires a number literal as the base argument', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['exponential', ['+', 0.1, 0.4]], ['zoom'], 0, 10, 1, 100]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type works with "interpolate" expression', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['exponential', 1.1], ['zoom'], 0, 10, 1, 20]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cubic-bezier interpolation type', () => {
|
||||
test('type requires four numeric literal control point arguments', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate',
|
||||
['cubic-bezier', 0.4, 0, ['literal', 0.6], 1],
|
||||
['zoom'],
|
||||
2,
|
||||
0,
|
||||
8,
|
||||
100
|
||||
]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type rejects a fifth control point argument', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['cubic-bezier', 0.4, 0, 0.6, 1, 0.8], ['zoom'], 2, 0, 8, 100]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type works with "interpolate" expression', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['cubic-bezier', 0.4, 0, 0.6, 1], ['zoom'], 0, 0, 10, 100]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"interpolate" expression', () => {
|
||||
test('type requires stop outputs to be a number, color, number array, color array, or projection', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['zoom'], 0, false, 2, 1024]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
[10, 20, 30],
|
||||
0.5,
|
||||
[20, 30, 40],
|
||||
1,
|
||||
[30, 40, 50]
|
||||
]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['zoom'], 0, {prop: 'foo'}, 2, {prop: 'bar'}]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates with feature property input', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['get', 'point_count'],
|
||||
2,
|
||||
['/', 2, ['get', 'point_count']],
|
||||
10,
|
||||
['*', 4, ['get', 'point_count']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between number outputs', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['zoom'], 0, 0, 0.5, ['*', 2, 5], 1, 100]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between color outputs', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['zoom'], 2, 'white', 4, 'black']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between number array outputs', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8,
|
||||
['literal', [2, 3]],
|
||||
10,
|
||||
['literal', [4, 5]]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between color array outputs', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8,
|
||||
['literal', ['white', 'black']],
|
||||
10,
|
||||
['literal', ['black', 'white']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between projection outputs', () => {
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['zoom'], 8, 'vertical-perspective', 10, 'mercator']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"interpolate-hcl" expression', () => {
|
||||
test('type requires stop outputs to be a color', () => {
|
||||
expectTypeOf<
|
||||
['interpolate-hcl', ['linear'], ['zoom'], 0, false, 2, 1024]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate-hcl',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
[10, 20, 30],
|
||||
0.5,
|
||||
[20, 30, 40],
|
||||
1,
|
||||
[30, 40, 50]
|
||||
]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['interpolate-hcl', ['linear'], ['zoom'], 0, {prop: 'foo'}, 2, {prop: 'bar'}]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between color outputs', () => {
|
||||
expectTypeOf<
|
||||
['interpolate-hcl', ['linear'], ['zoom'], 2, 'white', 4, 'black']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between color array outputs', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate-hcl',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8,
|
||||
['literal', ['white', 'black']],
|
||||
10,
|
||||
['literal', ['black', 'white']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between non-literal color array outputs', () => {
|
||||
// eslint-disable-next-line
|
||||
const obj = {'colors-8': ['white', 'black'], 'colors-10': ['black', 'white']};
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate-hcl',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8,
|
||||
['get', 'colors-8', ['literal', typeof obj]],
|
||||
10,
|
||||
['get', 'colors-10', ['literal', typeof obj]]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"interpolate-lab" expression', () => {
|
||||
test('type requires stop outputs to be a color', () => {
|
||||
expectTypeOf<
|
||||
['interpolate-lab', ['linear'], ['zoom'], 0, false, 2, 1024]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate-lab',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
0,
|
||||
[10, 20, 30],
|
||||
0.5,
|
||||
[20, 30, 40],
|
||||
1,
|
||||
[30, 40, 50]
|
||||
]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['interpolate-lab', ['linear'], ['zoom'], 0, {prop: 'foo'}, 2, {prop: 'bar'}]
|
||||
>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between color outputs', () => {
|
||||
expectTypeOf<
|
||||
['interpolate-lab', ['linear'], ['zoom'], 2, 'white', 4, 'black']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between color array outputs', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate-lab',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8,
|
||||
['literal', ['white', 'black']],
|
||||
10,
|
||||
['literal', ['black', 'white']]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which interpolates between non-literal color array outputs', () => {
|
||||
// eslint-disable-next-line
|
||||
const obj = {'colors-8': ['white', 'black'], 'colors-10': ['black', 'white']};
|
||||
expectTypeOf<
|
||||
[
|
||||
'interpolate-lab',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8,
|
||||
['get', 'colors-8', ['literal', typeof obj]],
|
||||
10,
|
||||
['get', 'colors-10', ['literal', typeof obj]]
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('"step" expression', () => {
|
||||
test('type accepts expression which outputs stepped numbers', () => {
|
||||
expectTypeOf<
|
||||
['step', ['get', 'point_count'], 0.6, 50, 0.7, 200, 0.8]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which outputs stepped colors', () => {
|
||||
expectTypeOf<
|
||||
['step', ['get', 'point_count'], '#ddd', 50, '#eee', 200, '#fff']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which outputs stepped projections', () => {
|
||||
expectTypeOf<
|
||||
['step', ['zoom'], 'vertical-perspective', 10, 'mercator']
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which outputs stepped multi-input projections', () => {
|
||||
expectTypeOf<
|
||||
[
|
||||
'step',
|
||||
['zoom'],
|
||||
['literal', ['vertical-perspective', 'mercator', 0.5]],
|
||||
10,
|
||||
'mercator'
|
||||
]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('"e" expression', () => {
|
||||
test('type rejects any arguments', () => {
|
||||
expectTypeOf<['e', 2]>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
test('type accepts expression which returns the mathematical constant e', () => {
|
||||
expectTypeOf<['e']>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonexistent operators', () => {
|
||||
test('ExpressionSpecification type does not contain "ExpressionSpecification" expression', () => {
|
||||
type ExpressionSpecificationExpression = Extract<
|
||||
ExpressionSpecification,
|
||||
['ExpressionSpecification', ...any[]]
|
||||
>;
|
||||
expectTypeOf<ExpressionSpecificationExpression>().not.toExtend<ExpressionSpecification>();
|
||||
});
|
||||
});
|
||||
|
||||
test('ExpressionSpecification type supports common variable insertion patterns', () => {
|
||||
// Checks the ability for the ExpressionSpecification type to allow arguments to be provided via constants (as opposed to in-line).
|
||||
// As in most cases the styling is read from JSON, these are rather optional tests.
|
||||
// eslint-disable-next-line
|
||||
const colorStops = [0, 'red', 0.5, 'green', 1, 'blue'];
|
||||
expectTypeOf<
|
||||
['interpolate', ['linear'], ['line-progress'], ...typeof colorStops]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['interpolate-hcl', ['linear'], ['line-progress'], ...typeof colorStops]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
expectTypeOf<
|
||||
['interpolate-lab', ['linear'], ['line-progress'], ...typeof colorStops]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
// eslint-disable-next-line
|
||||
const [firstOutput, ...steps] = ['#df2d43', 50, '#df2d43', 200, '#df2d43'];
|
||||
expectTypeOf<
|
||||
['step', ['get', 'point_count'], typeof firstOutput, ...typeof steps]
|
||||
>().toExtend<ExpressionSpecification>();
|
||||
// eslint-disable-next-line
|
||||
const strings = ['first', 'second', 'third'];
|
||||
expectTypeOf<['concat', ...typeof strings]>().toExtend<ExpressionSpecification>();
|
||||
// eslint-disable-next-line
|
||||
const values: (ExpressionInputType | ExpressionSpecification)[] = [
|
||||
['get', 'name'],
|
||||
['get', 'code'],
|
||||
'NONE'
|
||||
]; // type is necessary!
|
||||
expectTypeOf<['coalesce', ...typeof values]>().toExtend<ExpressionSpecification>();
|
||||
});
|
||||
199
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/expression.test.ts
generated
vendored
Normal file
199
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/expression.test.ts
generated
vendored
Normal file
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
createPropertyExpression,
|
||||
Feature,
|
||||
GlobalProperties,
|
||||
StylePropertyExpression
|
||||
} from '../expression';
|
||||
import {expressions} from './definitions';
|
||||
import v8 from '../reference/v8.json' with {type: 'json'};
|
||||
import {createExpression, StylePropertySpecification} from '..';
|
||||
import {ExpressionParsingError} from './parsing_error';
|
||||
import {VariableAnchorOffsetCollection} from './types/variable_anchor_offset_collection';
|
||||
import {describe, test, expect, vi} from 'vitest';
|
||||
|
||||
// filter out internal "error" and "filter-*" expressions from definition list
|
||||
const filterExpressionRegex = /filter-/;
|
||||
const definitionList = Object.keys(expressions)
|
||||
.filter((expression) => {
|
||||
return expression !== 'error' && !filterExpressionRegex.exec(expression);
|
||||
})
|
||||
.sort();
|
||||
|
||||
test('v8.json includes all definitions from style-spec', () => {
|
||||
const v8List = Object.keys(v8.expression_name.values);
|
||||
const v8SupportedList = v8List.filter((expression) => {
|
||||
//filter out expressions that are not supported in GL-JS
|
||||
return !!v8.expression_name.values[expression]['sdk-support']['basic functionality']['js'];
|
||||
});
|
||||
expect(definitionList).toEqual(v8SupportedList.sort());
|
||||
});
|
||||
|
||||
describe('createPropertyExpression', () => {
|
||||
test('prohibits non-interpolable properties from using an "interpolate" expression', () => {
|
||||
const {result, value} = createPropertyExpression(
|
||||
['interpolate', ['linear'], ['zoom'], 0, 0, 10, 10],
|
||||
{
|
||||
type: 'number',
|
||||
'property-type': 'data-constant',
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: ['zoom']
|
||||
}
|
||||
} as StylePropertySpecification
|
||||
);
|
||||
expect(result).toBe('error');
|
||||
expect(value as ExpressionParsingError[]).toHaveLength(1);
|
||||
expect(value[0].message).toBe(
|
||||
'"interpolate" expressions cannot be used with this property'
|
||||
);
|
||||
});
|
||||
|
||||
test('sets globalStateRefs', () => {
|
||||
const {value} = createPropertyExpression(
|
||||
[
|
||||
'case',
|
||||
['>', ['global-state', 'stateKey'], 0],
|
||||
100,
|
||||
['global-state', 'anotherStateKey']
|
||||
],
|
||||
{
|
||||
type: 'number',
|
||||
'property-type': 'data-driven',
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: ['zoom', 'feature']
|
||||
}
|
||||
} as any as StylePropertySpecification
|
||||
) as {value: StylePropertyExpression};
|
||||
|
||||
expect(value.globalStateRefs).toEqual(new Set(['stateKey', 'anotherStateKey']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluate expression', () => {
|
||||
test('silently falls back to default for nullish values', () => {
|
||||
const {value} = createPropertyExpression(['global-state', 'x'], {
|
||||
type: null,
|
||||
default: 42,
|
||||
'property-type': 'data-driven',
|
||||
transition: false
|
||||
}) as {value: StylePropertyExpression};
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate({globalState: {x: 5}, zoom: 10} as GlobalProperties)).toBe(5);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
|
||||
expect(value.evaluate({globalState: {}, zoom: 10} as GlobalProperties)).toBe(42);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('global state as expression property', () => {
|
||||
const {value} = createPropertyExpression(
|
||||
['global-state', 'x'],
|
||||
{
|
||||
type: null,
|
||||
default: 42,
|
||||
'property-type': 'data-driven',
|
||||
transition: false
|
||||
},
|
||||
{x: 5}
|
||||
) as {value: StylePropertyExpression};
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate({globalState: {x: 15}, zoom: 10} as GlobalProperties)).toBe(5);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('global state as expression property of zoom dependent expression', () => {
|
||||
const {value} = createPropertyExpression(
|
||||
['interpolate', ['linear'], ['zoom'], 10, ['global-state', 'x'], 20, 50],
|
||||
{
|
||||
type: 'number',
|
||||
default: 42,
|
||||
'property-type': 'data-driven',
|
||||
expression: {
|
||||
interpolated: true,
|
||||
parameters: ['zoom']
|
||||
}
|
||||
} as StylePropertySpecification,
|
||||
{x: 5}
|
||||
) as {value: StylePropertyExpression};
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate({globalState: {x: 15}, zoom: 10} as GlobalProperties)).toBe(5);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('warns and falls back to default for invalid enum values', () => {
|
||||
const {value} = createPropertyExpression(['get', 'x'], {
|
||||
type: 'enum',
|
||||
values: {a: {}, b: {}, c: {}},
|
||||
default: 'a',
|
||||
'property-type': 'data-driven',
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: ['zoom', 'feature']
|
||||
}
|
||||
} as any as StylePropertySpecification) as {value: StylePropertyExpression};
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.kind).toBe('source');
|
||||
|
||||
expect(
|
||||
value.evaluate({} as GlobalProperties, {properties: {x: 'b'}} as any as Feature)
|
||||
).toBe('b');
|
||||
expect(
|
||||
value.evaluate({} as GlobalProperties, {properties: {x: 'invalid'}} as any as Feature)
|
||||
).toBe('a');
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Expected value to be one of "a", "b", "c", but found "invalid" instead.'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns for invalid variableAnchorOffsetCollection values', () => {
|
||||
const {value} = createPropertyExpression(['get', 'x'], {
|
||||
type: 'variableAnchorOffsetCollection',
|
||||
'property-type': 'data-driven',
|
||||
transition: false,
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: ['zoom', 'feature']
|
||||
}
|
||||
}) as {value: StylePropertyExpression};
|
||||
|
||||
const warnMock = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.kind).toBe('source');
|
||||
|
||||
expect(
|
||||
value.evaluate({} as GlobalProperties, {properties: {x: 'invalid'}} as any as Feature)
|
||||
).toBeNull();
|
||||
expect(console.warn).toHaveBeenCalledTimes(2);
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
"Could not parse variableAnchorOffsetCollection from value 'invalid'"
|
||||
);
|
||||
|
||||
warnMock.mockClear();
|
||||
expect(
|
||||
value.evaluate(
|
||||
{} as GlobalProperties,
|
||||
{properties: {x: ['top', [2, 2]]}} as any as Feature
|
||||
)
|
||||
).toEqual(new VariableAnchorOffsetCollection(['top', [2, 2]]));
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonexistent operators', () => {
|
||||
test('"ExpressionSpecification" operator does not exist', () => {
|
||||
const response = createExpression(['ExpressionSpecification']);
|
||||
expect(response.result).toBe('error');
|
||||
expect((response.value as ExpressionParsingError[])[0].message).toContain(
|
||||
'Unknown expression \"ExpressionSpecification\".'
|
||||
);
|
||||
});
|
||||
});
|
||||
28
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/expression.ts
generated
vendored
Normal file
28
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/expression.ts
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import type {Type} from './types';
|
||||
import type {ParsingContext} from './parsing_context';
|
||||
import type {EvaluationContext} from './evaluation_context';
|
||||
|
||||
/**
|
||||
* Expression
|
||||
*/
|
||||
export interface Expression {
|
||||
readonly type: Type;
|
||||
evaluate(ctx: EvaluationContext): any;
|
||||
eachChild(fn: (a: Expression) => void): void;
|
||||
/**
|
||||
* Statically analyze the expression, attempting to enumerate possible outputs. Returns
|
||||
* false if the complete set of outputs is statically undecidable, otherwise true.
|
||||
*/
|
||||
outputDefined(): boolean;
|
||||
}
|
||||
|
||||
export type ExpressionParser = (
|
||||
args: ReadonlyArray<unknown>,
|
||||
context: ParsingContext
|
||||
) => Expression;
|
||||
export type ExpressionRegistration = {
|
||||
new (...args: any): Expression;
|
||||
} & {
|
||||
readonly parse: ExpressionParser;
|
||||
};
|
||||
export type ExpressionRegistry = {[_: string]: ExpressionRegistration};
|
||||
172
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/index.test.ts
generated
vendored
Normal file
172
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/index.test.ts
generated
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
import {normalizePropertyExpression, StyleExpression} from '.';
|
||||
import {StylePropertySpecification} from '..';
|
||||
import {Color} from './types/color';
|
||||
import {ColorArray} from './types/color_array';
|
||||
import {NumberArray} from './types/number_array';
|
||||
import {Padding} from './types/padding';
|
||||
import type {Expression} from './expression';
|
||||
import {describe, test, expect, vi} from 'vitest';
|
||||
|
||||
function stylePropertySpecification(type): StylePropertySpecification {
|
||||
return {
|
||||
type: type,
|
||||
'property-type': 'constant',
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: []
|
||||
},
|
||||
transition: false
|
||||
};
|
||||
}
|
||||
|
||||
describe('normalizePropertyExpression expressions', () => {
|
||||
test('normalizePropertyExpression<ColorArray>', () => {
|
||||
const expression = normalizePropertyExpression<ColorArray>(
|
||||
['literal', ['#FF0000', 'black']],
|
||||
stylePropertySpecification('colorArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([Color.red, Color.black]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<ColorArray> single value', () => {
|
||||
const expression = normalizePropertyExpression<ColorArray>(
|
||||
['literal', '#FF0000'],
|
||||
stylePropertySpecification('colorArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([Color.red]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<NumberArray>', () => {
|
||||
const expression = normalizePropertyExpression<NumberArray>(
|
||||
['literal', [1, 2]],
|
||||
stylePropertySpecification('numberArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<NumberArray> single value', () => {
|
||||
const expression = normalizePropertyExpression<NumberArray>(
|
||||
['literal', 1],
|
||||
stylePropertySpecification('numberArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<Padding>', () => {
|
||||
const expression = normalizePropertyExpression<Padding>(
|
||||
['literal', [1, 2]],
|
||||
stylePropertySpecification('padding')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1, 2, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizePropertyExpression objects', () => {
|
||||
test('normalizePropertyExpression<ColorArray>', () => {
|
||||
const expression = normalizePropertyExpression<ColorArray>(
|
||||
ColorArray.parse(['#FF0000', 'black']),
|
||||
stylePropertySpecification('colorArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([Color.red, Color.black]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<ColorArray> single value', () => {
|
||||
const expression = normalizePropertyExpression<ColorArray>(
|
||||
ColorArray.parse('#FF0000'),
|
||||
stylePropertySpecification('colorArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([Color.red]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<NumberArray>', () => {
|
||||
const expression = normalizePropertyExpression<NumberArray>(
|
||||
NumberArray.parse([1, 2]),
|
||||
stylePropertySpecification('numberArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<NumberArray> single value', () => {
|
||||
const expression = normalizePropertyExpression<NumberArray>(
|
||||
NumberArray.parse(1),
|
||||
stylePropertySpecification('numberArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<Padding>', () => {
|
||||
const expression = normalizePropertyExpression<Padding>(
|
||||
Padding.parse([1, 2]),
|
||||
stylePropertySpecification('padding')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1, 2, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizePropertyExpression raw values', () => {
|
||||
test('normalizePropertyExpression<ColorArray>', () => {
|
||||
const expression = normalizePropertyExpression<ColorArray>(
|
||||
['#FF0000', 'black'] as any,
|
||||
stylePropertySpecification('colorArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([Color.red, Color.black]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<ColorArray> single value', () => {
|
||||
const expression = normalizePropertyExpression<ColorArray>(
|
||||
'#FF0000' as any,
|
||||
stylePropertySpecification('colorArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([Color.red]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<NumberArray>', () => {
|
||||
const expression = normalizePropertyExpression<NumberArray>(
|
||||
[1, 2] as any,
|
||||
stylePropertySpecification('numberArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<NumberArray> single value', () => {
|
||||
const expression = normalizePropertyExpression<NumberArray>(
|
||||
1 as any,
|
||||
stylePropertySpecification('numberArray')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1]);
|
||||
});
|
||||
|
||||
test('normalizePropertyExpression<Padding>', () => {
|
||||
const expression = normalizePropertyExpression<Padding>(
|
||||
[1, 2] as any,
|
||||
stylePropertySpecification('padding')
|
||||
);
|
||||
expect(expression.evaluate({zoom: 0}).values).toEqual([1, 2, 1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StyleExpressions', () => {
|
||||
test('ignore random fields when adding global state ', () => {
|
||||
const expression = {
|
||||
evaluate: vi.fn()
|
||||
} as any as Expression;
|
||||
const styleExpression = new StyleExpression(
|
||||
expression,
|
||||
{
|
||||
type: null,
|
||||
default: 42,
|
||||
'property-type': 'data-driven',
|
||||
transition: false
|
||||
} as StylePropertySpecification,
|
||||
{x: 5} as Record<string, any>
|
||||
);
|
||||
|
||||
styleExpression.evaluate({zoom: 10, a: 20, b: 30} as any);
|
||||
expect(expression.evaluate).toHaveBeenCalled();
|
||||
const params = (expression.evaluate as any).mock.calls[0][0].globals;
|
||||
expect(params).toHaveProperty('zoom', 10);
|
||||
expect(params).toHaveProperty('globalState', {x: 5});
|
||||
expect(params).not.toHaveProperty('a');
|
||||
expect(params).not.toHaveProperty('b');
|
||||
});
|
||||
});
|
||||
749
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/index.ts
generated
vendored
Normal file
749
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/index.ts
generated
vendored
Normal file
@@ -0,0 +1,749 @@
|
||||
import {extendBy} from '../util/extend';
|
||||
import {ExpressionParsingError} from './parsing_error';
|
||||
import {ParsingContext} from './parsing_context';
|
||||
import {EvaluationContext} from './evaluation_context';
|
||||
|
||||
import {
|
||||
CompoundExpression,
|
||||
isFeatureConstant,
|
||||
isGlobalPropertyConstant,
|
||||
isStateConstant,
|
||||
isExpressionConstant
|
||||
} from './compound_expression';
|
||||
|
||||
import {Step} from './definitions/step';
|
||||
import {Interpolate} from './definitions/interpolate';
|
||||
import {Coalesce} from './definitions/coalesce';
|
||||
import {Let} from './definitions/let';
|
||||
import {expressions} from './definitions';
|
||||
|
||||
import {RuntimeError} from './runtime_error';
|
||||
import {success, error} from '../util/result';
|
||||
import {
|
||||
supportsPropertyExpression,
|
||||
supportsZoomExpression,
|
||||
supportsInterpolation
|
||||
} from '../util/properties';
|
||||
|
||||
import {
|
||||
ColorType,
|
||||
StringType,
|
||||
NumberType,
|
||||
BooleanType,
|
||||
ValueType,
|
||||
FormattedType,
|
||||
PaddingType,
|
||||
ResolvedImageType,
|
||||
VariableAnchorOffsetCollectionType,
|
||||
array,
|
||||
type Type,
|
||||
type EvaluationKind,
|
||||
ProjectionDefinitionType,
|
||||
NumberArrayType,
|
||||
ColorArrayType
|
||||
} from './types';
|
||||
import type {Value} from './values';
|
||||
import type {Expression} from './expression';
|
||||
import {type StylePropertySpecification} from '..';
|
||||
import type {Result} from '../util/result';
|
||||
import type {InterpolationType} from './definitions/interpolate';
|
||||
import type {
|
||||
PaddingSpecification,
|
||||
NumberArraySpecification,
|
||||
ColorArraySpecification,
|
||||
PropertyValueSpecification,
|
||||
VariableAnchorOffsetCollectionSpecification
|
||||
} from '../types.g';
|
||||
import type {FormattedSection} from './types/formatted';
|
||||
import type {Point2D} from '../point2d';
|
||||
|
||||
import {ICanonicalTileID} from '../tiles_and_coordinates';
|
||||
import {isFunction, createFunction} from '../function';
|
||||
import {Color} from './types/color';
|
||||
import {Padding} from './types/padding';
|
||||
import {NumberArray} from './types/number_array';
|
||||
import {ColorArray} from './types/color_array';
|
||||
import {VariableAnchorOffsetCollection} from './types/variable_anchor_offset_collection';
|
||||
import {ProjectionDefinition} from './types/projection_definition';
|
||||
import {GlobalState} from './definitions/global_state';
|
||||
|
||||
export type Feature = {
|
||||
readonly type:
|
||||
| 0
|
||||
| 1
|
||||
| 2
|
||||
| 3
|
||||
| 'Unknown'
|
||||
| 'Point'
|
||||
| 'MultiPoint'
|
||||
| 'LineString'
|
||||
| 'MultiLineString'
|
||||
| 'Polygon'
|
||||
| 'MultiPolygon';
|
||||
readonly id?: any;
|
||||
readonly properties: {[_: string]: any};
|
||||
readonly patterns?: {
|
||||
[_: string]: {
|
||||
min: string;
|
||||
mid: string;
|
||||
max: string;
|
||||
};
|
||||
};
|
||||
readonly dashes?: {
|
||||
[_: string]: {
|
||||
min: string;
|
||||
mid: string;
|
||||
max: string;
|
||||
};
|
||||
};
|
||||
readonly geometry?: Array<Array<Point2D>>;
|
||||
};
|
||||
|
||||
export type FeatureState = {[_: string]: any};
|
||||
|
||||
export type GlobalProperties = Readonly<{
|
||||
zoom: number;
|
||||
heatmapDensity?: number;
|
||||
elevation?: number;
|
||||
lineProgress?: number;
|
||||
isSupportedScript?: (_: string) => boolean;
|
||||
accumulated?: Value;
|
||||
globalState?: Record<string, any>;
|
||||
}>;
|
||||
|
||||
export class StyleExpression {
|
||||
expression: Expression;
|
||||
|
||||
_evaluator: EvaluationContext;
|
||||
_defaultValue: Value;
|
||||
_warningHistory: {[key: string]: boolean};
|
||||
_enumValues: {[_: string]: any};
|
||||
readonly _globalState: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
expression: Expression,
|
||||
propertySpec?: StylePropertySpecification | null,
|
||||
globalState?: Record<string, any>
|
||||
) {
|
||||
this.expression = expression;
|
||||
this._warningHistory = {};
|
||||
this._evaluator = new EvaluationContext();
|
||||
this._defaultValue = propertySpec ? getDefaultValue(propertySpec) : null;
|
||||
this._enumValues =
|
||||
propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null;
|
||||
this._globalState = globalState;
|
||||
}
|
||||
|
||||
evaluateWithoutErrorHandling(
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
): any {
|
||||
if (this._globalState) {
|
||||
globals = addGlobalState(globals, this._globalState);
|
||||
}
|
||||
this._evaluator.globals = globals;
|
||||
this._evaluator.feature = feature;
|
||||
this._evaluator.featureState = featureState;
|
||||
this._evaluator.canonical = canonical;
|
||||
this._evaluator.availableImages = availableImages || null;
|
||||
this._evaluator.formattedSection = formattedSection;
|
||||
|
||||
return this.expression.evaluate(this._evaluator);
|
||||
}
|
||||
|
||||
evaluate(
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
): any {
|
||||
if (this._globalState) {
|
||||
globals = addGlobalState(globals, this._globalState);
|
||||
}
|
||||
this._evaluator.globals = globals;
|
||||
this._evaluator.feature = feature || null;
|
||||
this._evaluator.featureState = featureState || null;
|
||||
this._evaluator.canonical = canonical;
|
||||
this._evaluator.availableImages = availableImages || null;
|
||||
this._evaluator.formattedSection = formattedSection || null;
|
||||
|
||||
try {
|
||||
const val = this.expression.evaluate(this._evaluator);
|
||||
if (val === null || val === undefined || (typeof val === 'number' && val !== val)) {
|
||||
return this._defaultValue;
|
||||
}
|
||||
if (this._enumValues && !(val in this._enumValues)) {
|
||||
throw new RuntimeError(
|
||||
`Expected value to be one of ${Object.keys(this._enumValues)
|
||||
.map((v) => JSON.stringify(v))
|
||||
.join(', ')}, but found ${JSON.stringify(val)} instead.`
|
||||
);
|
||||
}
|
||||
return val;
|
||||
} catch (e) {
|
||||
if (!this._warningHistory[e.message]) {
|
||||
this._warningHistory[e.message] = true;
|
||||
if (typeof console !== 'undefined') {
|
||||
console.warn(e.message);
|
||||
}
|
||||
}
|
||||
return this._defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isExpression(expression: unknown) {
|
||||
return (
|
||||
Array.isArray(expression) &&
|
||||
expression.length > 0 &&
|
||||
typeof expression[0] === 'string' &&
|
||||
expression[0] in expressions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and typecheck the given style spec JSON expression. If
|
||||
* options.defaultValue is provided, then the resulting StyleExpression's
|
||||
* `evaluate()` method will handle errors by logging a warning (once per
|
||||
* message) and returning the default value. Otherwise, it will throw
|
||||
* evaluation errors.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export function createExpression(
|
||||
expression: unknown,
|
||||
propertySpec?: StylePropertySpecification | null,
|
||||
globalState?: Record<string, any>
|
||||
): Result<StyleExpression, Array<ExpressionParsingError>> {
|
||||
const parser = new ParsingContext(
|
||||
expressions,
|
||||
isExpressionConstant,
|
||||
[],
|
||||
propertySpec ? getExpectedType(propertySpec) : undefined
|
||||
);
|
||||
|
||||
// For string-valued properties, coerce to string at the top level rather than asserting.
|
||||
const parsed = parser.parse(
|
||||
expression,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
propertySpec && propertySpec.type === 'string' ? {typeAnnotation: 'coerce'} : undefined
|
||||
);
|
||||
|
||||
if (!parsed) {
|
||||
return error(parser.errors);
|
||||
}
|
||||
|
||||
return success(new StyleExpression(parsed, propertySpec, globalState));
|
||||
}
|
||||
|
||||
export class ZoomConstantExpression<Kind extends EvaluationKind> {
|
||||
kind: Kind;
|
||||
isStateDependent: boolean;
|
||||
globalStateRefs: Set<string>;
|
||||
_styleExpression: StyleExpression;
|
||||
readonly _globalState: Record<string, any>;
|
||||
|
||||
constructor(kind: Kind, expression: StyleExpression, globalState?: Record<string, any>) {
|
||||
this.kind = kind;
|
||||
this._styleExpression = expression;
|
||||
this.isStateDependent =
|
||||
kind !== ('constant' as EvaluationKind) && !isStateConstant(expression.expression);
|
||||
this.globalStateRefs = findGlobalStateRefs(expression.expression);
|
||||
this._globalState = globalState;
|
||||
}
|
||||
|
||||
evaluateWithoutErrorHandling(
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
): any {
|
||||
if (this._globalState) {
|
||||
globals = addGlobalState(globals, this._globalState);
|
||||
}
|
||||
return this._styleExpression.evaluateWithoutErrorHandling(
|
||||
globals,
|
||||
feature,
|
||||
featureState,
|
||||
canonical,
|
||||
availableImages,
|
||||
formattedSection
|
||||
);
|
||||
}
|
||||
|
||||
evaluate(
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
): any {
|
||||
if (this._globalState) {
|
||||
globals = addGlobalState(globals, this._globalState);
|
||||
}
|
||||
return this._styleExpression.evaluate(
|
||||
globals,
|
||||
feature,
|
||||
featureState,
|
||||
canonical,
|
||||
availableImages,
|
||||
formattedSection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ZoomDependentExpression<Kind extends EvaluationKind> {
|
||||
kind: Kind;
|
||||
zoomStops: Array<number>;
|
||||
isStateDependent: boolean;
|
||||
globalStateRefs: Set<string>;
|
||||
_styleExpression: StyleExpression;
|
||||
interpolationType: InterpolationType;
|
||||
readonly _globalState: Record<string, any>;
|
||||
|
||||
constructor(
|
||||
kind: Kind,
|
||||
expression: StyleExpression,
|
||||
zoomStops: Array<number>,
|
||||
interpolationType?: InterpolationType,
|
||||
globalState?: Record<string, any>
|
||||
) {
|
||||
this.kind = kind;
|
||||
this.zoomStops = zoomStops;
|
||||
this._styleExpression = expression;
|
||||
this.isStateDependent =
|
||||
kind !== ('camera' as EvaluationKind) && !isStateConstant(expression.expression);
|
||||
this.globalStateRefs = findGlobalStateRefs(expression.expression);
|
||||
this.interpolationType = interpolationType;
|
||||
this._globalState = globalState;
|
||||
}
|
||||
|
||||
evaluateWithoutErrorHandling(
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
): any {
|
||||
if (this._globalState) {
|
||||
globals = addGlobalState(globals, this._globalState);
|
||||
}
|
||||
return this._styleExpression.evaluateWithoutErrorHandling(
|
||||
globals,
|
||||
feature,
|
||||
featureState,
|
||||
canonical,
|
||||
availableImages,
|
||||
formattedSection
|
||||
);
|
||||
}
|
||||
|
||||
evaluate(
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
): any {
|
||||
if (this._globalState) {
|
||||
globals = addGlobalState(globals, this._globalState);
|
||||
}
|
||||
return this._styleExpression.evaluate(
|
||||
globals,
|
||||
feature,
|
||||
featureState,
|
||||
canonical,
|
||||
availableImages,
|
||||
formattedSection
|
||||
);
|
||||
}
|
||||
|
||||
interpolationFactor(input: number, lower: number, upper: number): number {
|
||||
if (this.interpolationType) {
|
||||
return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isZoomExpression(
|
||||
expression: any
|
||||
): expression is ZoomConstantExpression<'source'> | ZoomDependentExpression<'source'> {
|
||||
return (expression as ZoomConstantExpression<'source'>)._styleExpression !== undefined;
|
||||
}
|
||||
|
||||
export type ConstantExpression = {
|
||||
kind: 'constant';
|
||||
globalStateRefs: Set<string>;
|
||||
readonly _globalState: Record<string, any>;
|
||||
readonly evaluate: (
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>
|
||||
) => any;
|
||||
};
|
||||
|
||||
export type SourceExpression = {
|
||||
kind: 'source';
|
||||
isStateDependent: boolean;
|
||||
globalStateRefs: Set<string>;
|
||||
readonly _globalState: Record<string, any>;
|
||||
readonly evaluate: (
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
) => any;
|
||||
};
|
||||
|
||||
export type CameraExpression = {
|
||||
kind: 'camera';
|
||||
globalStateRefs: Set<string>;
|
||||
readonly _globalState: Record<string, any>;
|
||||
readonly evaluate: (
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>
|
||||
) => any;
|
||||
readonly interpolationFactor: (input: number, lower: number, upper: number) => number;
|
||||
zoomStops: Array<number>;
|
||||
interpolationType: InterpolationType;
|
||||
};
|
||||
|
||||
export type CompositeExpression = {
|
||||
kind: 'composite';
|
||||
isStateDependent: boolean;
|
||||
globalStateRefs: Set<string>;
|
||||
readonly _globalState: Record<string, any>;
|
||||
readonly evaluate: (
|
||||
globals: GlobalProperties,
|
||||
feature?: Feature,
|
||||
featureState?: FeatureState,
|
||||
canonical?: ICanonicalTileID,
|
||||
availableImages?: Array<string>,
|
||||
formattedSection?: FormattedSection
|
||||
) => any;
|
||||
readonly interpolationFactor: (input: number, lower: number, upper: number) => number;
|
||||
zoomStops: Array<number>;
|
||||
interpolationType: InterpolationType;
|
||||
};
|
||||
|
||||
export type StylePropertyExpression =
|
||||
| ConstantExpression
|
||||
| SourceExpression
|
||||
| CameraExpression
|
||||
| CompositeExpression;
|
||||
|
||||
export function createPropertyExpression(
|
||||
expressionInput: unknown,
|
||||
propertySpec: StylePropertySpecification,
|
||||
globalState?: Record<string, any>
|
||||
): Result<StylePropertyExpression, Array<ExpressionParsingError>> {
|
||||
const expression = createExpression(expressionInput, propertySpec, globalState);
|
||||
if (expression.result === 'error') {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const parsed = expression.value.expression;
|
||||
|
||||
const isFeatureConstantResult = isFeatureConstant(parsed);
|
||||
if (!isFeatureConstantResult && !supportsPropertyExpression(propertySpec)) {
|
||||
return error([new ExpressionParsingError('', 'data expressions not supported')]);
|
||||
}
|
||||
|
||||
const isZoomConstant = isGlobalPropertyConstant(parsed, ['zoom']);
|
||||
if (!isZoomConstant && !supportsZoomExpression(propertySpec)) {
|
||||
return error([new ExpressionParsingError('', 'zoom expressions not supported')]);
|
||||
}
|
||||
|
||||
const zoomCurve = findZoomCurve(parsed);
|
||||
if (!zoomCurve && !isZoomConstant) {
|
||||
return error([
|
||||
new ExpressionParsingError(
|
||||
'',
|
||||
'"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.'
|
||||
)
|
||||
]);
|
||||
} else if (zoomCurve instanceof ExpressionParsingError) {
|
||||
return error([zoomCurve]);
|
||||
} else if (zoomCurve instanceof Interpolate && !supportsInterpolation(propertySpec)) {
|
||||
return error([
|
||||
new ExpressionParsingError(
|
||||
'',
|
||||
'"interpolate" expressions cannot be used with this property'
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
if (!zoomCurve) {
|
||||
return success(
|
||||
isFeatureConstantResult
|
||||
? (new ZoomConstantExpression(
|
||||
'constant',
|
||||
expression.value,
|
||||
globalState
|
||||
) as ConstantExpression)
|
||||
: (new ZoomConstantExpression(
|
||||
'source',
|
||||
expression.value,
|
||||
globalState
|
||||
) as SourceExpression)
|
||||
);
|
||||
}
|
||||
|
||||
const interpolationType =
|
||||
zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined;
|
||||
|
||||
return success(
|
||||
isFeatureConstantResult
|
||||
? (new ZoomDependentExpression(
|
||||
'camera',
|
||||
expression.value,
|
||||
zoomCurve.labels,
|
||||
interpolationType,
|
||||
globalState
|
||||
) as CameraExpression)
|
||||
: (new ZoomDependentExpression(
|
||||
'composite',
|
||||
expression.value,
|
||||
zoomCurve.labels,
|
||||
interpolationType,
|
||||
globalState
|
||||
) as CompositeExpression)
|
||||
);
|
||||
}
|
||||
|
||||
// serialization wrapper for old-style stop functions normalized to the
|
||||
// expression interface
|
||||
export class StylePropertyFunction<T> {
|
||||
_parameters: PropertyValueSpecification<T>;
|
||||
_specification: StylePropertySpecification;
|
||||
|
||||
kind: EvaluationKind;
|
||||
evaluate: (globals: GlobalProperties, feature?: Feature) => any;
|
||||
interpolationFactor: (input: number, lower: number, upper: number) => number;
|
||||
zoomStops: Array<number>;
|
||||
|
||||
constructor(
|
||||
parameters: PropertyValueSpecification<T>,
|
||||
specification: StylePropertySpecification
|
||||
) {
|
||||
this._parameters = parameters;
|
||||
this._specification = specification;
|
||||
extendBy(this, createFunction(this._parameters, this._specification));
|
||||
}
|
||||
|
||||
static deserialize<T>(serialized: {
|
||||
_parameters: PropertyValueSpecification<T>;
|
||||
_specification: StylePropertySpecification;
|
||||
}) {
|
||||
return new StylePropertyFunction(
|
||||
serialized._parameters,
|
||||
serialized._specification
|
||||
) as StylePropertyFunction<T>;
|
||||
}
|
||||
|
||||
static serialize<T>(input: StylePropertyFunction<T>) {
|
||||
return {
|
||||
_parameters: input._parameters,
|
||||
_specification: input._specification
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePropertyExpression<T>(
|
||||
value: PropertyValueSpecification<T>,
|
||||
specification: StylePropertySpecification,
|
||||
globalState?: Record<string, any>
|
||||
): StylePropertyExpression {
|
||||
if (isFunction(value)) {
|
||||
return new StylePropertyFunction(value, specification) as any;
|
||||
} else if (isExpression(value)) {
|
||||
const expression = createPropertyExpression(value, specification, globalState);
|
||||
if (expression.result === 'error') {
|
||||
// this should have been caught in validation
|
||||
throw new Error(expression.value.map((err) => `${err.key}: ${err.message}`).join(', '));
|
||||
}
|
||||
return expression.value;
|
||||
} else {
|
||||
let constant: any = value;
|
||||
if (specification.type === 'color' && typeof value === 'string') {
|
||||
constant = Color.parse(value);
|
||||
} else if (
|
||||
specification.type === 'padding' &&
|
||||
(typeof value === 'number' || Array.isArray(value))
|
||||
) {
|
||||
constant = Padding.parse(value as PaddingSpecification);
|
||||
} else if (
|
||||
specification.type === 'numberArray' &&
|
||||
(typeof value === 'number' || Array.isArray(value))
|
||||
) {
|
||||
constant = NumberArray.parse(value as NumberArraySpecification);
|
||||
} else if (
|
||||
specification.type === 'colorArray' &&
|
||||
(typeof value === 'string' || Array.isArray(value))
|
||||
) {
|
||||
constant = ColorArray.parse(value as ColorArraySpecification);
|
||||
} else if (
|
||||
specification.type === 'variableAnchorOffsetCollection' &&
|
||||
Array.isArray(value)
|
||||
) {
|
||||
constant = VariableAnchorOffsetCollection.parse(
|
||||
value as VariableAnchorOffsetCollectionSpecification
|
||||
);
|
||||
} else if (specification.type === 'projectionDefinition' && typeof value === 'string') {
|
||||
constant = ProjectionDefinition.parse(value);
|
||||
}
|
||||
return {
|
||||
globalStateRefs: new Set<string>(),
|
||||
_globalState: null,
|
||||
kind: 'constant',
|
||||
evaluate: () => constant
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom-dependent expressions may only use ["zoom"] as the input to a top-level "step" or "interpolate"
|
||||
// expression (collectively referred to as a "curve"). The curve may be wrapped in one or more "let" or
|
||||
// "coalesce" expressions.
|
||||
function findZoomCurve(expression: Expression): Step | Interpolate | ExpressionParsingError | null {
|
||||
let result = null;
|
||||
if (expression instanceof Let) {
|
||||
result = findZoomCurve(expression.result);
|
||||
} else if (expression instanceof Coalesce) {
|
||||
for (const arg of expression.args) {
|
||||
result = findZoomCurve(arg);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(expression instanceof Step || expression instanceof Interpolate) &&
|
||||
expression.input instanceof CompoundExpression &&
|
||||
expression.input.name === 'zoom'
|
||||
) {
|
||||
result = expression;
|
||||
}
|
||||
|
||||
if (result instanceof ExpressionParsingError) {
|
||||
return result;
|
||||
}
|
||||
|
||||
expression.eachChild((child) => {
|
||||
const childResult = findZoomCurve(child);
|
||||
if (childResult instanceof ExpressionParsingError) {
|
||||
result = childResult;
|
||||
} else if (!result && childResult) {
|
||||
result = new ExpressionParsingError(
|
||||
'',
|
||||
'"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.'
|
||||
);
|
||||
} else if (result && childResult && result !== childResult) {
|
||||
result = new ExpressionParsingError(
|
||||
'',
|
||||
'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findGlobalStateRefs(
|
||||
expression: Expression,
|
||||
results = new Set<string>()
|
||||
): Set<string> {
|
||||
if (expression instanceof GlobalState) {
|
||||
results.add(expression.key);
|
||||
}
|
||||
|
||||
expression.eachChild((childExpression) => {
|
||||
findGlobalStateRefs(childExpression, results);
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
function getExpectedType(spec: StylePropertySpecification): Type {
|
||||
const types = {
|
||||
color: ColorType,
|
||||
string: StringType,
|
||||
number: NumberType,
|
||||
enum: StringType,
|
||||
boolean: BooleanType,
|
||||
formatted: FormattedType,
|
||||
padding: PaddingType,
|
||||
numberArray: NumberArrayType,
|
||||
colorArray: ColorArrayType,
|
||||
projectionDefinition: ProjectionDefinitionType,
|
||||
resolvedImage: ResolvedImageType,
|
||||
variableAnchorOffsetCollection: VariableAnchorOffsetCollectionType
|
||||
};
|
||||
|
||||
if (spec.type === 'array') {
|
||||
return array(types[spec.value] || ValueType, spec.length);
|
||||
}
|
||||
|
||||
return types[spec.type];
|
||||
}
|
||||
|
||||
function getDefaultValue(spec: StylePropertySpecification): Value {
|
||||
if (spec.type === 'color' && isFunction(spec.default)) {
|
||||
// Special case for heatmap-color: it uses the 'default:' to define a
|
||||
// default color ramp, but createExpression expects a simple value to fall
|
||||
// back to in case of runtime errors
|
||||
return new Color(0, 0, 0, 0);
|
||||
}
|
||||
switch (spec.type) {
|
||||
case 'color':
|
||||
return Color.parse(spec.default) || null;
|
||||
case 'padding':
|
||||
return Padding.parse(spec.default) || null;
|
||||
case 'numberArray':
|
||||
return NumberArray.parse(spec.default) || null;
|
||||
case 'colorArray':
|
||||
return ColorArray.parse(spec.default) || null;
|
||||
case 'variableAnchorOffsetCollection':
|
||||
return VariableAnchorOffsetCollection.parse(spec.default) || null;
|
||||
case 'projectionDefinition':
|
||||
return ProjectionDefinition.parse(spec.default) || null;
|
||||
default:
|
||||
return spec.default === undefined ? null : spec.default;
|
||||
}
|
||||
}
|
||||
|
||||
function addGlobalState(
|
||||
globals: GlobalProperties,
|
||||
globalState: Record<string, any>
|
||||
): GlobalProperties {
|
||||
const {zoom, heatmapDensity, elevation, lineProgress, isSupportedScript, accumulated} =
|
||||
globals ?? {};
|
||||
return {
|
||||
zoom,
|
||||
heatmapDensity,
|
||||
elevation,
|
||||
lineProgress,
|
||||
isSupportedScript,
|
||||
accumulated,
|
||||
globalState
|
||||
};
|
||||
}
|
||||
237
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/parsing_context.ts
generated
vendored
Normal file
237
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/parsing_context.ts
generated
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
import {Scope} from './scope';
|
||||
import {checkSubtype} from './types';
|
||||
import {ExpressionParsingError} from './parsing_error';
|
||||
import {Literal} from './definitions/literal';
|
||||
import {Assertion} from './definitions/assertion';
|
||||
import {Coercion} from './definitions/coercion';
|
||||
import {EvaluationContext} from './evaluation_context';
|
||||
|
||||
import type {Expression, ExpressionRegistry} from './expression';
|
||||
import type {Type} from './types';
|
||||
|
||||
/**
|
||||
* State associated parsing at a given point in an expression tree.
|
||||
* @private
|
||||
*/
|
||||
export class ParsingContext {
|
||||
registry: ExpressionRegistry;
|
||||
path: Array<number>;
|
||||
key: string;
|
||||
scope: Scope;
|
||||
errors: Array<ExpressionParsingError>;
|
||||
|
||||
// The expected type of this expression. Provided only to allow Expression
|
||||
// implementations to infer argument types: Expression#parse() need not
|
||||
// check that the output type of the parsed expression matches
|
||||
// `expectedType`.
|
||||
expectedType: Type;
|
||||
|
||||
/**
|
||||
* Internal delegate to inConstant function to avoid circular dependency to CompoundExpression
|
||||
*/
|
||||
private _isConstant: (expression: Expression) => boolean;
|
||||
|
||||
constructor(
|
||||
registry: ExpressionRegistry,
|
||||
isConstantFunc: (expression: Expression) => boolean,
|
||||
path: Array<number> = [],
|
||||
expectedType?: Type | null,
|
||||
scope: Scope = new Scope(),
|
||||
errors: Array<ExpressionParsingError> = []
|
||||
) {
|
||||
this.registry = registry;
|
||||
this.path = path;
|
||||
this.key = path.map((part) => `[${part}]`).join('');
|
||||
this.scope = scope;
|
||||
this.errors = errors;
|
||||
this.expectedType = expectedType;
|
||||
this._isConstant = isConstantFunc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param expr the JSON expression to parse
|
||||
* @param index the optional argument index if this expression is an argument of a parent expression that's being parsed
|
||||
* @param options
|
||||
* @param options.omitTypeAnnotations set true to omit inferred type annotations. Caller beware: with this option set, the parsed expression's type will NOT satisfy `expectedType` if it would normally be wrapped in an inferred annotation.
|
||||
* @private
|
||||
*/
|
||||
parse(
|
||||
expr: unknown,
|
||||
index?: number,
|
||||
expectedType?: Type | null,
|
||||
bindings?: Array<[string, Expression]>,
|
||||
options: {
|
||||
typeAnnotation?: 'assert' | 'coerce' | 'omit';
|
||||
} = {}
|
||||
): Expression {
|
||||
if (index) {
|
||||
return this.concat(index, expectedType, bindings)._parse(expr, options);
|
||||
}
|
||||
return this._parse(expr, options);
|
||||
}
|
||||
|
||||
_parse(
|
||||
expr: unknown,
|
||||
options: {
|
||||
typeAnnotation?: 'assert' | 'coerce' | 'omit';
|
||||
}
|
||||
): Expression {
|
||||
if (
|
||||
expr === null ||
|
||||
typeof expr === 'string' ||
|
||||
typeof expr === 'boolean' ||
|
||||
typeof expr === 'number'
|
||||
) {
|
||||
expr = ['literal', expr];
|
||||
}
|
||||
|
||||
function annotate(parsed, type, typeAnnotation: 'assert' | 'coerce' | 'omit') {
|
||||
if (typeAnnotation === 'assert') {
|
||||
return new Assertion(type, [parsed]);
|
||||
} else if (typeAnnotation === 'coerce') {
|
||||
return new Coercion(type, [parsed]);
|
||||
} else {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(expr)) {
|
||||
if (expr.length === 0) {
|
||||
return this.error(
|
||||
'Expected an array with at least one element. If you wanted a literal array, use ["literal", []].'
|
||||
) as null;
|
||||
}
|
||||
|
||||
const op = expr[0];
|
||||
if (typeof op !== 'string') {
|
||||
this.error(
|
||||
`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`,
|
||||
0
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const Expr = this.registry[op];
|
||||
if (Expr) {
|
||||
let parsed = Expr.parse(expr, this);
|
||||
if (!parsed) return null;
|
||||
|
||||
if (this.expectedType) {
|
||||
const expected = this.expectedType;
|
||||
const actual = parsed.type;
|
||||
|
||||
// When we expect a number, string, boolean, or array but have a value, wrap it in an assertion.
|
||||
// When we expect a color or formatted string, but have a string or value, wrap it in a coercion.
|
||||
// Otherwise, we do static type-checking.
|
||||
//
|
||||
// These behaviors are overridable for:
|
||||
// * The "coalesce" operator, which needs to omit type annotations.
|
||||
// * String-valued properties (e.g. `text-field`), where coercion is more convenient than assertion.
|
||||
//
|
||||
if (
|
||||
(expected.kind === 'string' ||
|
||||
expected.kind === 'number' ||
|
||||
expected.kind === 'boolean' ||
|
||||
expected.kind === 'object' ||
|
||||
expected.kind === 'array') &&
|
||||
actual.kind === 'value'
|
||||
) {
|
||||
parsed = annotate(parsed, expected, options.typeAnnotation || 'assert');
|
||||
} else if (
|
||||
('projectionDefinition' === expected.kind &&
|
||||
['string', 'array'].includes(actual.kind)) ||
|
||||
(['color', 'formatted', 'resolvedImage'].includes(expected.kind) &&
|
||||
['value', 'string'].includes(actual.kind)) ||
|
||||
(['padding', 'numberArray'].includes(expected.kind) &&
|
||||
['value', 'number', 'array'].includes(actual.kind)) ||
|
||||
('colorArray' === expected.kind &&
|
||||
['value', 'string', 'array'].includes(actual.kind)) ||
|
||||
('variableAnchorOffsetCollection' === expected.kind &&
|
||||
['value', 'array'].includes(actual.kind))
|
||||
) {
|
||||
parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce');
|
||||
} else if (this.checkSubtype(expected, actual)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// If an expression's arguments are all literals, we can evaluate
|
||||
// it immediately and replace it with a literal value in the
|
||||
// parsed/compiled result. Expressions that expect an image should
|
||||
// not be resolved here so we can later get the available images.
|
||||
if (
|
||||
!(parsed instanceof Literal) &&
|
||||
parsed.type.kind !== 'resolvedImage' &&
|
||||
this._isConstant(parsed)
|
||||
) {
|
||||
const ec = new EvaluationContext();
|
||||
try {
|
||||
parsed = new Literal(parsed.type, parsed.evaluate(ec));
|
||||
} catch (e) {
|
||||
this.error(e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return this.error(
|
||||
`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`,
|
||||
0
|
||||
) as null;
|
||||
} else if (typeof expr === 'undefined') {
|
||||
return this.error("'undefined' value invalid. Use null instead.") as null;
|
||||
} else if (typeof expr === 'object') {
|
||||
return this.error('Bare objects invalid. Use ["literal", {...}] instead.') as null;
|
||||
} else {
|
||||
return this.error(`Expected an array, but found ${typeof expr} instead.`) as null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this context suitable for parsing the subexpression at
|
||||
* index `index`, optionally appending to 'let' binding map.
|
||||
*
|
||||
* Note that `errors` property, intended for collecting errors while
|
||||
* parsing, is copied by reference rather than cloned.
|
||||
* @private
|
||||
*/
|
||||
concat(index: number, expectedType?: Type | null, bindings?: Array<[string, Expression]>) {
|
||||
const path = typeof index === 'number' ? this.path.concat(index) : this.path;
|
||||
const scope = bindings ? this.scope.concat(bindings) : this.scope;
|
||||
return new ParsingContext(
|
||||
this.registry,
|
||||
this._isConstant,
|
||||
path,
|
||||
expectedType || null,
|
||||
scope,
|
||||
this.errors
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a parsing (or type checking) error into the `this.errors`
|
||||
* @param error The message
|
||||
* @param keys Optionally specify the source of the error at a child
|
||||
* of the current expression at `this.key`.
|
||||
* @private
|
||||
*/
|
||||
error(error: string, ...keys: Array<number>) {
|
||||
const key = `${this.key}${keys.map((k) => `[${k}]`).join('')}`;
|
||||
this.errors.push(new ExpressionParsingError(key, error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if `t` is a subtype of `expected`; otherwise returns an
|
||||
* error message and also pushes it to `this.errors`.
|
||||
* @param expected The expected type
|
||||
* @param t The actual type
|
||||
* @returns null if `t` is a subtype of `expected`; otherwise returns an error message
|
||||
*/
|
||||
checkSubtype(expected: Type, t: Type): string {
|
||||
const error = checkSubtype(expected, t);
|
||||
if (error) this.error(error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
9
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/parsing_error.ts
generated
vendored
Normal file
9
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/parsing_error.ts
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export class ExpressionParsingError extends Error {
|
||||
key: string;
|
||||
message: string;
|
||||
constructor(key: string, message: string) {
|
||||
super(message);
|
||||
this.message = message;
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
10
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/runtime_error.ts
generated
vendored
Normal file
10
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/runtime_error.ts
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export class RuntimeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'RuntimeError';
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
36
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/scope.ts
generated
vendored
Normal file
36
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/scope.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import type {Expression} from './expression';
|
||||
|
||||
/**
|
||||
* Tracks `let` bindings during expression parsing.
|
||||
* @private
|
||||
*/
|
||||
export class Scope {
|
||||
parent: Scope;
|
||||
bindings: {[_: string]: Expression};
|
||||
constructor(parent?: Scope, bindings: Array<[string, Expression]> = []) {
|
||||
this.parent = parent;
|
||||
this.bindings = {};
|
||||
for (const [name, expression] of bindings) {
|
||||
this.bindings[name] = expression;
|
||||
}
|
||||
}
|
||||
|
||||
concat(bindings: Array<[string, Expression]>) {
|
||||
return new Scope(this, bindings);
|
||||
}
|
||||
|
||||
get(name: string): Expression {
|
||||
if (this.bindings[name]) {
|
||||
return this.bindings[name];
|
||||
}
|
||||
if (this.parent) {
|
||||
return this.parent.get(name);
|
||||
}
|
||||
throw new Error(`${name} not found in scope.`);
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
if (this.bindings[name]) return true;
|
||||
return this.parent ? this.parent.has(name) : false;
|
||||
}
|
||||
}
|
||||
22
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/stops.test.ts
generated
vendored
Normal file
22
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/stops.test.ts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import {findStopLessThanOrEqualTo} from './stops';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('findStopLessThanOrEqualTo', () => {
|
||||
test('When the input > all stops it returns the last stop.', () => {
|
||||
const index = findStopLessThanOrEqualTo([0, 1, 2, 3, 4, 5, 6, 7], 8);
|
||||
expect(index).toBe(7);
|
||||
});
|
||||
|
||||
test('When more than one stop has the same value it always returns the last stop', () => {
|
||||
let index;
|
||||
|
||||
index = findStopLessThanOrEqualTo([0.5, 0.5], 0.5);
|
||||
expect(index).toBe(1);
|
||||
|
||||
index = findStopLessThanOrEqualTo([0.5, 0.5, 0.5], 0.5);
|
||||
expect(index).toBe(2);
|
||||
|
||||
index = findStopLessThanOrEqualTo([0.4, 0.5, 0.5, 0.6, 0.7], 0.5);
|
||||
expect(index).toBe(2);
|
||||
});
|
||||
});
|
||||
38
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/stops.ts
generated
vendored
Normal file
38
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/stops.ts
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
import {RuntimeError} from './runtime_error';
|
||||
|
||||
import type {Expression} from './expression';
|
||||
|
||||
export type Stops = Array<[number, Expression]>;
|
||||
|
||||
/**
|
||||
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
|
||||
* @private
|
||||
*/
|
||||
export function findStopLessThanOrEqualTo(stops: Array<number>, input: number) {
|
||||
const lastIndex = stops.length - 1;
|
||||
let lowerIndex = 0;
|
||||
let upperIndex = lastIndex;
|
||||
let currentIndex = 0;
|
||||
let currentValue, nextValue;
|
||||
|
||||
while (lowerIndex <= upperIndex) {
|
||||
currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
|
||||
currentValue = stops[currentIndex];
|
||||
nextValue = stops[currentIndex + 1];
|
||||
|
||||
if (currentValue <= input) {
|
||||
if (currentIndex === lastIndex || input < nextValue) {
|
||||
// Search complete
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
lowerIndex = currentIndex + 1;
|
||||
} else if (currentValue > input) {
|
||||
upperIndex = currentIndex - 1;
|
||||
} else {
|
||||
throw new RuntimeError('Input is not a number.');
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
211
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types.ts
generated
vendored
Normal file
211
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types.ts
generated
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
export type NullTypeT = {
|
||||
kind: 'null';
|
||||
};
|
||||
export type NumberTypeT = {
|
||||
kind: 'number';
|
||||
};
|
||||
export type StringTypeT = {
|
||||
kind: 'string';
|
||||
};
|
||||
export type BooleanTypeT = {
|
||||
kind: 'boolean';
|
||||
};
|
||||
export type ColorTypeT = {
|
||||
kind: 'color';
|
||||
};
|
||||
export type ProjectionDefinitionTypeT = {
|
||||
kind: 'projectionDefinition';
|
||||
};
|
||||
export type ObjectTypeT = {
|
||||
kind: 'object';
|
||||
};
|
||||
export type ValueTypeT = {
|
||||
kind: 'value';
|
||||
};
|
||||
export type ErrorTypeT = {
|
||||
kind: 'error';
|
||||
};
|
||||
export type CollatorTypeT = {
|
||||
kind: 'collator';
|
||||
};
|
||||
export type FormattedTypeT = {
|
||||
kind: 'formatted';
|
||||
};
|
||||
export type PaddingTypeT = {
|
||||
kind: 'padding';
|
||||
};
|
||||
export type NumberArrayTypeT = {
|
||||
kind: 'numberArray';
|
||||
};
|
||||
export type ColorArrayTypeT = {
|
||||
kind: 'colorArray';
|
||||
};
|
||||
export type ResolvedImageTypeT = {
|
||||
kind: 'resolvedImage';
|
||||
};
|
||||
export type VariableAnchorOffsetCollectionTypeT = {
|
||||
kind: 'variableAnchorOffsetCollection';
|
||||
};
|
||||
|
||||
export type EvaluationKind = 'constant' | 'source' | 'camera' | 'composite';
|
||||
|
||||
export type Type =
|
||||
| NullTypeT
|
||||
| NumberTypeT
|
||||
| StringTypeT
|
||||
| BooleanTypeT
|
||||
| ColorTypeT
|
||||
| ProjectionDefinitionTypeT
|
||||
| ObjectTypeT
|
||||
| ValueTypeT
|
||||
| ArrayType
|
||||
| ErrorTypeT
|
||||
| CollatorTypeT
|
||||
| FormattedTypeT
|
||||
| PaddingTypeT
|
||||
| NumberArrayTypeT
|
||||
| ColorArrayTypeT
|
||||
| ResolvedImageTypeT
|
||||
| VariableAnchorOffsetCollectionTypeT;
|
||||
|
||||
export interface ArrayType<T extends Type = Type> {
|
||||
kind: 'array';
|
||||
itemType: T;
|
||||
N: number;
|
||||
}
|
||||
|
||||
export type NativeType = 'number' | 'string' | 'boolean' | 'null' | 'array' | 'object';
|
||||
|
||||
export const NullType = {kind: 'null'} as NullTypeT;
|
||||
export const NumberType = {kind: 'number'} as NumberTypeT;
|
||||
export const StringType = {kind: 'string'} as StringTypeT;
|
||||
export const BooleanType = {kind: 'boolean'} as BooleanTypeT;
|
||||
export const ColorType = {kind: 'color'} as ColorTypeT;
|
||||
export const ProjectionDefinitionType = {
|
||||
kind: 'projectionDefinition'
|
||||
} as ProjectionDefinitionTypeT;
|
||||
export const ObjectType = {kind: 'object'} as ObjectTypeT;
|
||||
export const ValueType = {kind: 'value'} as ValueTypeT;
|
||||
export const ErrorType = {kind: 'error'} as ErrorTypeT;
|
||||
export const CollatorType = {kind: 'collator'} as CollatorTypeT;
|
||||
export const FormattedType = {kind: 'formatted'} as FormattedTypeT;
|
||||
export const PaddingType = {kind: 'padding'} as PaddingTypeT;
|
||||
export const ColorArrayType = {kind: 'colorArray'} as ColorArrayTypeT;
|
||||
export const NumberArrayType = {kind: 'numberArray'} as NumberArrayTypeT;
|
||||
export const ResolvedImageType = {kind: 'resolvedImage'} as ResolvedImageTypeT;
|
||||
export const VariableAnchorOffsetCollectionType = {
|
||||
kind: 'variableAnchorOffsetCollection'
|
||||
} as VariableAnchorOffsetCollectionTypeT;
|
||||
|
||||
export function array<T extends Type>(itemType: T, N?: number | null): ArrayType<T> {
|
||||
return {
|
||||
kind: 'array',
|
||||
itemType,
|
||||
N
|
||||
};
|
||||
}
|
||||
|
||||
export function typeToString(type: Type): string {
|
||||
if (type.kind === 'array') {
|
||||
const itemType = typeToString(type.itemType);
|
||||
return typeof type.N === 'number'
|
||||
? `array<${itemType}, ${type.N}>`
|
||||
: type.itemType.kind === 'value'
|
||||
? 'array'
|
||||
: `array<${itemType}>`;
|
||||
} else {
|
||||
return type.kind;
|
||||
}
|
||||
}
|
||||
|
||||
const valueMemberTypes = [
|
||||
NullType,
|
||||
NumberType,
|
||||
StringType,
|
||||
BooleanType,
|
||||
ColorType,
|
||||
ProjectionDefinitionType,
|
||||
FormattedType,
|
||||
ObjectType,
|
||||
array(ValueType),
|
||||
PaddingType,
|
||||
NumberArrayType,
|
||||
ColorArrayType,
|
||||
ResolvedImageType,
|
||||
VariableAnchorOffsetCollectionType
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns null if `t` is a subtype of `expected`; otherwise returns an
|
||||
* error message.
|
||||
* @private
|
||||
*/
|
||||
export function checkSubtype(expected: Type, t: Type): string {
|
||||
if (t.kind === 'error') {
|
||||
// Error is a subtype of every type
|
||||
return null;
|
||||
} else if (expected.kind === 'array') {
|
||||
if (
|
||||
t.kind === 'array' &&
|
||||
((t.N === 0 && t.itemType.kind === 'value') ||
|
||||
!checkSubtype(expected.itemType, t.itemType)) &&
|
||||
(typeof expected.N !== 'number' || expected.N === t.N)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
} else if (expected.kind === t.kind) {
|
||||
return null;
|
||||
} else if (expected.kind === 'value') {
|
||||
for (const memberType of valueMemberTypes) {
|
||||
if (!checkSubtype(memberType, t)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `Expected ${typeToString(expected)} but found ${typeToString(t)} instead.`;
|
||||
}
|
||||
|
||||
export function isValidType(provided: Type, allowedTypes: Array<Type>): boolean {
|
||||
return allowedTypes.some((t) => t.kind === provided.kind);
|
||||
}
|
||||
|
||||
export function isValidNativeType(provided: any, allowedTypes: Array<NativeType>): boolean {
|
||||
return allowedTypes.some((t) => {
|
||||
if (t === 'null') {
|
||||
return provided === null;
|
||||
} else if (t === 'array') {
|
||||
return Array.isArray(provided);
|
||||
} else if (t === 'object') {
|
||||
return provided && !Array.isArray(provided) && typeof provided === 'object';
|
||||
} else {
|
||||
return t === typeof provided;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify whether the specified type is of the same type as the specified sample.
|
||||
*
|
||||
* @param provided Type to verify
|
||||
* @param sample Sample type to reference
|
||||
* @returns `true` if both objects are of the same type, `false` otherwise
|
||||
* @example basic types
|
||||
* if (verifyType(outputType, ValueType)) {
|
||||
* // type narrowed to:
|
||||
* outputType.kind; // 'value'
|
||||
* }
|
||||
* @example array types
|
||||
* if (verifyType(outputType, array(NumberType))) {
|
||||
* // type narrowed to:
|
||||
* outputType.kind; // 'array'
|
||||
* outputType.itemType; // NumberTypeT
|
||||
* outputType.itemType.kind; // 'number'
|
||||
* }
|
||||
*/
|
||||
export function verifyType<T extends Type>(provided: Type, sample: T): provided is T {
|
||||
if (provided.kind === 'array' && sample.kind === 'array') {
|
||||
return provided.itemType.kind === sample.itemType.kind && typeof provided.N === 'number';
|
||||
}
|
||||
return provided.kind === sample.kind;
|
||||
}
|
||||
26
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/collator.ts
generated
vendored
Normal file
26
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/collator.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
export class Collator {
|
||||
locale: string | null;
|
||||
sensitivity: 'base' | 'accent' | 'case' | 'variant';
|
||||
collator: Intl.Collator;
|
||||
|
||||
constructor(caseSensitive: boolean, diacriticSensitive: boolean, locale: string | null) {
|
||||
if (caseSensitive) this.sensitivity = diacriticSensitive ? 'variant' : 'case';
|
||||
else this.sensitivity = diacriticSensitive ? 'accent' : 'base';
|
||||
|
||||
this.locale = locale;
|
||||
this.collator = new Intl.Collator(this.locale ? this.locale : [], {
|
||||
sensitivity: this.sensitivity,
|
||||
usage: 'search'
|
||||
});
|
||||
}
|
||||
|
||||
compare(lhs: string, rhs: string): number {
|
||||
return this.collator.compare(lhs, rhs);
|
||||
}
|
||||
|
||||
resolvedLocale(): string {
|
||||
// We create a Collator without "usage: search" because we don't want
|
||||
// the search options encoded in our result (e.g. "en-u-co-search")
|
||||
return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
|
||||
}
|
||||
}
|
||||
143
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color.test.ts
generated
vendored
Normal file
143
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color.test.ts
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
import {expectCloseToArray, expectToMatchColor} from '../../../test/lib/util';
|
||||
import {Color, isSupportedInterpolationColorSpace} from './color';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('Color class', () => {
|
||||
describe('parsing', () => {
|
||||
test('should parse valid css color strings', () => {
|
||||
expectToMatchColor(Color.parse('RED'), 'rgb(100% 0% 0% / 1)');
|
||||
expectToMatchColor(Color.parse('#f00C'), 'rgb(100% 0% 0% / .8)');
|
||||
expectToMatchColor(Color.parse('rgb(0 0 127.5 / 20%)'), 'rgb(0% 0% 50% / .2)');
|
||||
expectToMatchColor(
|
||||
Color.parse('hsl(300deg 100% 25.1% / 0.7)'),
|
||||
'rgb(50.2% 0% 50.2% / .7)'
|
||||
);
|
||||
});
|
||||
|
||||
test('should return undefined when provided with invalid CSS color string', () => {
|
||||
expect(Color.parse(undefined)).toBeUndefined();
|
||||
expect(Color.parse(null)).toBeUndefined();
|
||||
expect(Color.parse('#invalid')).toBeUndefined();
|
||||
expect(Color.parse('$123')).toBeUndefined();
|
||||
expect(Color.parse('0F91')).toBeUndefined();
|
||||
expect(Color.parse('rgb(#123)')).toBeUndefined();
|
||||
expect(Color.parse('hsl(0,0,0)')).toBeUndefined();
|
||||
expect(Color.parse('rgb(0deg,0,0)')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should accept instances of Color class', () => {
|
||||
const color = new Color(0, 0, 0, 0);
|
||||
expect(Color.parse(color)).toBe(color);
|
||||
});
|
||||
});
|
||||
|
||||
test('should keep a reference to the original color when alpha=0', () => {
|
||||
const color = new Color(0, 0, 0.5, 0, false);
|
||||
expect(color).toMatchObject({r: 0, g: 0, b: 0, a: 0});
|
||||
expect(Object.hasOwn(color, 'rgb')).toBe(true);
|
||||
expectCloseToArray(color.rgb, [0, 0, 0.5, 0]);
|
||||
});
|
||||
|
||||
test('should have static properties, black', () => {
|
||||
const color = Color.black;
|
||||
expect(color).toMatchObject({r: 0, g: 0, b: 0, a: 1});
|
||||
expectCloseToArray(color.rgb, [0, 0, 0, 1]);
|
||||
});
|
||||
|
||||
test('should not keep a reference to the original color when alpha!=0', () => {
|
||||
const color = new Color(0, 0, 0.5, 0.001, false);
|
||||
expect(color).toMatchObject({r: 0, g: 0, b: expect.closeTo(0.5 * 0.001, 5), a: 0.001});
|
||||
expect(Object.hasOwn(color, 'rgb')).toBe(false);
|
||||
});
|
||||
|
||||
test('should serialize to rgba format', () => {
|
||||
expect(`${new Color(1, 1, 0, 1, false)}`).toBe('rgba(255,255,0,1)');
|
||||
expect(`${new Color(0.2, 0, 1, 0.3, false)}`).toBe('rgba(51,0,255,0.3)');
|
||||
expect(`${new Color(1, 1, 0, 0, false)}`).toBe('rgba(255,255,0,0)');
|
||||
expect(`${Color.parse('purple')}`).toBe('rgba(128,0,128,1)');
|
||||
expect(`${Color.parse('rgba(26,207,26,.73)')}`).toBe('rgba(26,207,26,0.73)');
|
||||
expect(`${Color.parse('rgba(26,207,26,0)')}`).toBe('rgba(26,207,26,0)');
|
||||
});
|
||||
|
||||
describe('interpolation color space', () => {
|
||||
test('should recognize supported interpolation color spaces', () => {
|
||||
expect(isSupportedInterpolationColorSpace('rgb')).toBe(true);
|
||||
expect(isSupportedInterpolationColorSpace('hcl')).toBe(true);
|
||||
expect(isSupportedInterpolationColorSpace('lab')).toBe(true);
|
||||
});
|
||||
|
||||
test('should ignore invalid interpolation color spaces', () => {
|
||||
expect(isSupportedInterpolationColorSpace('sRGB')).toBe(false);
|
||||
expect(isSupportedInterpolationColorSpace('HCL')).toBe(false);
|
||||
expect(isSupportedInterpolationColorSpace('LCH')).toBe(false);
|
||||
expect(isSupportedInterpolationColorSpace('LAB')).toBe(false);
|
||||
expect(isSupportedInterpolationColorSpace('interpolate')).toBe(false);
|
||||
expect(isSupportedInterpolationColorSpace('interpolate-hcl')).toBe(false);
|
||||
expect(isSupportedInterpolationColorSpace('interpolate-lab')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolate color', () => {
|
||||
test('should interpolate colors in "rgb" color space', () => {
|
||||
const color = Color.parse('rgba(0,0,255,1)');
|
||||
const targetColor = Color.parse('rgba(0,255,0,.6)');
|
||||
|
||||
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'rgb');
|
||||
expectToMatchColor(i11nFn(0.0), 'rgb(0% 0% 100% / 1)');
|
||||
expectToMatchColor(i11nFn(0.25), 'rgb(0% 25% 75% / 0.9)');
|
||||
expectToMatchColor(i11nFn(0.5), 'rgb(0% 50% 50% / 0.8)');
|
||||
expectToMatchColor(i11nFn(0.75), 'rgb(0% 75% 25% / 0.7)');
|
||||
expectToMatchColor(i11nFn(1.0), 'rgb(0% 100% 0% / 0.6)');
|
||||
});
|
||||
|
||||
test('should interpolate colors in "hcl" color space', () => {
|
||||
const color = Color.parse('rgba(0,0,255,1)');
|
||||
const targetColor = Color.parse('rgba(0,255,0,.6)');
|
||||
|
||||
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'hcl');
|
||||
expectToMatchColor(i11nFn(0.0), 'rgb(0% 0% 100% / 1)');
|
||||
expectToMatchColor(i11nFn(0.25), 'rgb(0% 49.37% 100% / 0.9)', 4);
|
||||
expectToMatchColor(i11nFn(0.5), 'rgb(0% 70.44% 100% / 0.8)', 4);
|
||||
expectToMatchColor(i11nFn(0.75), 'rgb(0% 87.54% 63.18% / 0.7)', 4);
|
||||
expectToMatchColor(i11nFn(1.0), 'rgb(0% 100% 0% / 0.6)');
|
||||
});
|
||||
|
||||
test('should interpolate colors in "lab" color space', () => {
|
||||
const color = Color.parse('rgba(0,0,255,1)');
|
||||
const targetColor = Color.parse('rgba(0,255,0,.6)');
|
||||
|
||||
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'lab');
|
||||
expectToMatchColor(i11nFn(0.0), 'rgb(0% 0% 100% / 1)');
|
||||
expectToMatchColor(i11nFn(0.25), 'rgb(39.64% 34.55% 83.36% / 0.9)', 4);
|
||||
expectToMatchColor(i11nFn(0.5), 'rgb(46.42% 56.82% 65.91% / 0.8)', 4);
|
||||
expectToMatchColor(i11nFn(0.75), 'rgb(41.45% 78.34% 45.62% / 0.7)', 4);
|
||||
expectToMatchColor(i11nFn(1.0), 'rgb(0% 100% 0% / 0.6)');
|
||||
});
|
||||
|
||||
test('should correctly interpolate colors with alpha=0', () => {
|
||||
const color = Color.parse('rgba(0,0,255,0)');
|
||||
const targetColor = Color.parse('rgba(0,255,0,1)');
|
||||
|
||||
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, 'rgb');
|
||||
expectToMatchColor(i11nFn(0.0), 'rgb(0% 0% 0% / 0)');
|
||||
expectToMatchColor(i11nFn(0.25), 'rgb(0% 25% 75% / 0.25)');
|
||||
expectToMatchColor(i11nFn(0.5), 'rgb(0% 50% 50% / 0.5)');
|
||||
expectToMatchColor(i11nFn(0.75), 'rgb(0% 75% 25% / 0.75)');
|
||||
expectToMatchColor(i11nFn(1.0), 'rgb(0% 100% 0% / 1)');
|
||||
});
|
||||
|
||||
test('should limit interpolation results to sRGB gamut', () => {
|
||||
const color = Color.parse('royalblue');
|
||||
const targetColor = Color.parse('cyan');
|
||||
|
||||
for (const space of ['rgb', 'hcl', 'lab'] as const) {
|
||||
const i11nFn = (t: number) => Color.interpolate(color, targetColor, t, space);
|
||||
const colorInBetween = i11nFn(0.5);
|
||||
for (const key of ['r', 'g', 'b', 'a'] as const) {
|
||||
expect(colorInBetween[key]).toBeGreaterThanOrEqual(0);
|
||||
expect(colorInBetween[key]).toBeLessThanOrEqual(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
212
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color.ts
generated
vendored
Normal file
212
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color.ts
generated
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
import {HCLColor, hclToRgb, LABColor, labToRgb, RGBColor, rgbToHcl, rgbToLab} from './color_spaces';
|
||||
import {parseCssColor} from './parse_css_color';
|
||||
import {interpolateArray, interpolateNumber} from '../../util/interpolate-primitives';
|
||||
|
||||
export type InterpolationColorSpace = 'rgb' | 'hcl' | 'lab';
|
||||
|
||||
/**
|
||||
* Checks whether the specified color space is one of the supported interpolation color spaces.
|
||||
*
|
||||
* @param colorSpace Color space key to verify.
|
||||
* @returns `true` if the specified color space is one of the supported
|
||||
* interpolation color spaces, `false` otherwise
|
||||
*/
|
||||
export function isSupportedInterpolationColorSpace(
|
||||
colorSpace: string
|
||||
): colorSpace is InterpolationColorSpace {
|
||||
return colorSpace === 'rgb' || colorSpace === 'hcl' || colorSpace === 'lab';
|
||||
}
|
||||
|
||||
/**
|
||||
* Color representation used by WebGL.
|
||||
* Defined in sRGB color space and pre-blended with alpha.
|
||||
* @private
|
||||
*/
|
||||
export class Color {
|
||||
readonly r: number;
|
||||
readonly g: number;
|
||||
readonly b: number;
|
||||
readonly a: number;
|
||||
|
||||
/**
|
||||
* @param r Red component premultiplied by `alpha` 0..1
|
||||
* @param g Green component premultiplied by `alpha` 0..1
|
||||
* @param b Blue component premultiplied by `alpha` 0..1
|
||||
* @param [alpha=1] Alpha component 0..1
|
||||
* @param [premultiplied=true] Whether the `r`, `g` and `b` values have already
|
||||
* been multiplied by alpha. If `true` nothing happens if `false` then they will
|
||||
* be multiplied automatically.
|
||||
*/
|
||||
constructor(r: number, g: number, b: number, alpha = 1, premultiplied = true) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.a = alpha;
|
||||
|
||||
if (!premultiplied) {
|
||||
this.r *= alpha;
|
||||
this.g *= alpha;
|
||||
this.b *= alpha;
|
||||
|
||||
if (!alpha) {
|
||||
// alpha = 0 erases completely rgb channels. This behavior is not desirable
|
||||
// if this particular color is later used in color interpolation.
|
||||
// Because of that, a reference to original color is saved.
|
||||
this.overwriteGetter('rgb', [r, g, b, alpha]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static black = new Color(0, 0, 0, 1);
|
||||
static white = new Color(1, 1, 1, 1);
|
||||
static transparent = new Color(0, 0, 0, 0);
|
||||
static red = new Color(1, 0, 0, 1);
|
||||
|
||||
/**
|
||||
* Parses CSS color strings and converts colors to sRGB color space if needed.
|
||||
* Officially supported color formats:
|
||||
* - keyword, e.g. 'aquamarine' or 'steelblue'
|
||||
* - hex (with 3, 4, 6 or 8 digits), e.g. '#f0f' or '#e9bebea9'
|
||||
* - rgb and rgba, e.g. 'rgb(0,240,120)' or 'rgba(0%,94%,47%,0.1)' or 'rgb(0 240 120 / .3)'
|
||||
* - hsl and hsla, e.g. 'hsl(0,0%,83%)' or 'hsla(0,0%,83%,.5)' or 'hsl(0 0% 83% / 20%)'
|
||||
*
|
||||
* @param input CSS color string to parse.
|
||||
* @returns A `Color` instance, or `undefined` if the input is not a valid color string.
|
||||
*/
|
||||
static parse(input: Color | string | undefined | null): Color | undefined {
|
||||
// in zoom-and-property function input could be an instance of Color class
|
||||
if (input instanceof Color) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (typeof input !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const rgba = parseCssColor(input);
|
||||
if (rgba) {
|
||||
return new Color(...rgba, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in color interpolation and by 'to-rgba' expression.
|
||||
*
|
||||
* @returns Gien color, with reversed alpha blending, in sRGB color space.
|
||||
*/
|
||||
get rgb(): RGBColor {
|
||||
const {r, g, b, a} = this;
|
||||
const f = a || Infinity; // reverse alpha blending factor
|
||||
return this.overwriteGetter('rgb', [r / f, g / f, b / f, a]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in color interpolation.
|
||||
*
|
||||
* @returns Gien color, with reversed alpha blending, in HCL color space.
|
||||
*/
|
||||
get hcl(): HCLColor {
|
||||
return this.overwriteGetter('hcl', rgbToHcl(this.rgb));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in color interpolation.
|
||||
*
|
||||
* @returns Gien color, with reversed alpha blending, in LAB color space.
|
||||
*/
|
||||
get lab(): LABColor {
|
||||
return this.overwriteGetter('lab', rgbToLab(this.rgb));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy getter pattern. When getter is called for the first time lazy value
|
||||
* is calculated and then overwrites getter function in given object instance.
|
||||
*
|
||||
* @example:
|
||||
* const redColor = Color.parse('red');
|
||||
* let x = redColor.hcl; // this will invoke `get hcl()`, which will calculate
|
||||
* // the value of red in HCL space and invoke this `overwriteGetter` function
|
||||
* // which in turn will set a field with a key 'hcl' in the `redColor` object.
|
||||
* // In other words it will override `get hcl()` from its `Color` prototype
|
||||
* // with its own property: hcl = [calculated red value in hcl].
|
||||
* let y = redColor.hcl; // next call will no longer invoke getter but simply
|
||||
* // return the previously calculated value
|
||||
* x === y; // true - `x` is exactly the same object as `y`
|
||||
*
|
||||
* @param getterKey Getter key
|
||||
* @param lazyValue Lazily calculated value to be memoized by current instance
|
||||
* @private
|
||||
*/
|
||||
private overwriteGetter<T>(getterKey: string, lazyValue: T): T {
|
||||
Object.defineProperty(this, getterKey, {value: lazyValue});
|
||||
return lazyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by 'to-string' expression.
|
||||
*
|
||||
* @returns Serialized color in format `rgba(r,g,b,a)`
|
||||
* where r,g,b are numbers within 0..255 and alpha is number within 1..0
|
||||
*
|
||||
* @example
|
||||
* var purple = new Color.parse('purple');
|
||||
* purple.toString; // = "rgba(128,0,128,1)"
|
||||
* var translucentGreen = new Color.parse('rgba(26, 207, 26, .73)');
|
||||
* translucentGreen.toString(); // = "rgba(26,207,26,0.73)"
|
||||
*/
|
||||
toString(): string {
|
||||
const [r, g, b, a] = this.rgb;
|
||||
return `rgba(${[r, g, b].map((n) => Math.round(n * 255)).join(',')},${a})`;
|
||||
}
|
||||
|
||||
static interpolate(
|
||||
from: Color,
|
||||
to: Color,
|
||||
t: number,
|
||||
spaceKey: InterpolationColorSpace = 'rgb'
|
||||
): Color {
|
||||
switch (spaceKey) {
|
||||
case 'rgb': {
|
||||
const [r, g, b, alpha] = interpolateArray(from.rgb, to.rgb, t);
|
||||
return new Color(r, g, b, alpha, false);
|
||||
}
|
||||
case 'hcl': {
|
||||
const [hue0, chroma0, light0, alphaF] = from.hcl;
|
||||
const [hue1, chroma1, light1, alphaT] = to.hcl;
|
||||
|
||||
// https://github.com/gka/chroma.js/blob/cd1b3c0926c7a85cbdc3b1453b3a94006de91a92/src/interpolator/_hsx.js
|
||||
let hue, chroma;
|
||||
|
||||
if (!isNaN(hue0) && !isNaN(hue1)) {
|
||||
let dh = hue1 - hue0;
|
||||
if (hue1 > hue0 && dh > 180) {
|
||||
dh -= 360;
|
||||
} else if (hue1 < hue0 && hue0 - hue1 > 180) {
|
||||
dh += 360;
|
||||
}
|
||||
hue = hue0 + t * dh;
|
||||
} else if (!isNaN(hue0)) {
|
||||
hue = hue0;
|
||||
if (light1 === 1 || light1 === 0) chroma = chroma0;
|
||||
} else if (!isNaN(hue1)) {
|
||||
hue = hue1;
|
||||
if (light0 === 1 || light0 === 0) chroma = chroma1;
|
||||
} else {
|
||||
hue = NaN;
|
||||
}
|
||||
|
||||
const [r, g, b, alpha] = hclToRgb([
|
||||
hue,
|
||||
chroma ?? interpolateNumber(chroma0, chroma1, t),
|
||||
interpolateNumber(light0, light1, t),
|
||||
interpolateNumber(alphaF, alphaT, t)
|
||||
]);
|
||||
return new Color(r, g, b, alpha, false);
|
||||
}
|
||||
case 'lab': {
|
||||
const [r, g, b, alpha] = labToRgb(interpolateArray(from.lab, to.lab, t));
|
||||
return new Color(r, g, b, alpha, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_array.test.ts
generated
vendored
Normal file
48
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_array.test.ts
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import {Color} from './color';
|
||||
import {ColorArray} from './color_array';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('ColorArray', () => {
|
||||
test('ColorArray.parse', () => {
|
||||
expect(ColorArray.parse()).toBeUndefined();
|
||||
expect(ColorArray.parse(null)).toBeUndefined();
|
||||
expect(ColorArray.parse(undefined)).toBeUndefined();
|
||||
expect(ColorArray.parse('Dennis' as any)).toBeUndefined();
|
||||
expect(ColorArray.parse('3' as any)).toBeUndefined();
|
||||
expect(ColorArray.parse('yellow').values).toEqual([Color.parse('yellow')]);
|
||||
expect(ColorArray.parse([]).values).toEqual([]);
|
||||
expect(ColorArray.parse(['yellow']).values).toEqual([Color.parse('yellow')]);
|
||||
expect(ColorArray.parse(['yellow', 'blue']).values).toEqual([
|
||||
Color.parse('yellow'),
|
||||
Color.parse('blue')
|
||||
]);
|
||||
expect(ColorArray.parse([3, 4] as any)).toBeUndefined();
|
||||
expect(ColorArray.parse(['non-color', 'words'] as any)).toBeUndefined();
|
||||
|
||||
const passThru = new ColorArray([Color.parse('yellow'), Color.parse('blue')]);
|
||||
expect(ColorArray.parse(passThru)).toBe(passThru);
|
||||
});
|
||||
|
||||
test('ColorArray#toString', () => {
|
||||
const colorArray = ColorArray.parse(['yellow', 'blue']);
|
||||
expect(colorArray.toString()).toBe('[{"r":1,"g":1,"b":0,"a":1},{"r":0,"g":0,"b":1,"a":1}]');
|
||||
});
|
||||
|
||||
test('interpolate ColorArray', () => {
|
||||
const colorArray = ColorArray.parse(['#00A0AA', '#000000']);
|
||||
const targetColorArray = ColorArray.parse(['#AA0000', '#2468AC']);
|
||||
|
||||
const i11nFn = (t: number) => ColorArray.interpolate(colorArray, targetColorArray, t);
|
||||
expect(i11nFn(0.5)).toBeInstanceOf(ColorArray);
|
||||
expect(i11nFn(0.5)).toEqual(ColorArray.parse(['#555055', '#123456']));
|
||||
});
|
||||
|
||||
test('interpolate ColorArray with mismatched lengths', () => {
|
||||
const colorArray = ColorArray.parse(['#00A0AA', '#000000']);
|
||||
const targetColorArray = ColorArray.parse('#AA0000');
|
||||
|
||||
expect(() => {
|
||||
ColorArray.interpolate(colorArray, targetColorArray, 0.5);
|
||||
}).toThrowError('colorArray: Arrays have mismatched length (2 vs. 1), cannot interpolate.');
|
||||
});
|
||||
});
|
||||
75
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_array.ts
generated
vendored
Normal file
75
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_array.ts
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
import {Color, InterpolationColorSpace} from './color';
|
||||
|
||||
/**
|
||||
* An array of colors. Create instances from
|
||||
* bare arrays or strings using the static method `ColorArray.parse`.
|
||||
* @private
|
||||
*/
|
||||
export class ColorArray {
|
||||
values: Color[];
|
||||
|
||||
constructor(values: Color[]) {
|
||||
this.values = values.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* ColorArray values
|
||||
* @param input A ColorArray value
|
||||
* @returns A `ColorArray` instance, or `undefined` if the input is not a valid ColorArray value.
|
||||
*/
|
||||
static parse(input?: string | string[] | ColorArray | null): ColorArray | undefined {
|
||||
if (input instanceof ColorArray) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Backwards compatibility (e.g. hillshade-shadow-color): bare Color is treated the same as array with single value.
|
||||
if (typeof input === 'string') {
|
||||
const parsed_val = Color.parse(input);
|
||||
if (!parsed_val) {
|
||||
return undefined;
|
||||
}
|
||||
return new ColorArray([parsed_val]);
|
||||
}
|
||||
|
||||
if (!Array.isArray(input)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const colors: Color[] = [];
|
||||
|
||||
for (const val of input) {
|
||||
if (typeof val !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const parsed_val = Color.parse(val);
|
||||
if (!parsed_val) {
|
||||
return undefined;
|
||||
}
|
||||
colors.push(parsed_val);
|
||||
}
|
||||
|
||||
return new ColorArray(colors);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this.values);
|
||||
}
|
||||
|
||||
static interpolate(
|
||||
from: ColorArray,
|
||||
to: ColorArray,
|
||||
t: number,
|
||||
spaceKey: InterpolationColorSpace = 'rgb'
|
||||
): ColorArray {
|
||||
const colors = [] as Color[];
|
||||
if (from.values.length != to.values.length) {
|
||||
throw new Error(
|
||||
`colorArray: Arrays have mismatched length (${from.values.length} vs. ${to.values.length}), cannot interpolate.`
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < from.values.length; i++) {
|
||||
colors.push(Color.interpolate(from.values[i], to.values[i], t, spaceKey));
|
||||
}
|
||||
return new ColorArray(colors);
|
||||
}
|
||||
}
|
||||
73
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_spaces.test.ts
generated
vendored
Normal file
73
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_spaces.test.ts
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
import {expectCloseToArray} from '../../../test/lib/util';
|
||||
import {hclToRgb, hslToRgb, labToRgb, rgbToHcl, rgbToLab} from './color_spaces';
|
||||
import {describe, test} from 'vitest';
|
||||
|
||||
describe('color spaces', () => {
|
||||
describe('LAB color space', () => {
|
||||
test('should convert colors from sRGB to LAB color space', () => {
|
||||
expectCloseToArray(rgbToLab([0, 0, 0, 1]), [0, 0, 0, 1]);
|
||||
expectCloseToArray(rgbToLab([1, 1, 1, 1]), [100, 0, 0, 1], 4);
|
||||
expectCloseToArray(rgbToLab([0, 1, 0, 1]), [87.82, -79.29, 80.99, 1], 2);
|
||||
expectCloseToArray(rgbToLab([0, 1, 1, 1]), [90.67, -50.67, -14.96, 1], 2);
|
||||
expectCloseToArray(rgbToLab([0, 0, 1, 1]), [29.57, 68.3, -112.03, 1], 2);
|
||||
expectCloseToArray(rgbToLab([1, 1, 0, 1]), [97.61, -15.75, 93.39, 1], 2);
|
||||
expectCloseToArray(rgbToLab([1, 0, 0, 1]), [54.29, 80.81, 69.89, 1], 2);
|
||||
});
|
||||
|
||||
test('should convert colors from LAB to sRGB color space', () => {
|
||||
expectCloseToArray(labToRgb([0, 0, 0, 1]), [0, 0, 0, 1]);
|
||||
expectCloseToArray(labToRgb([100, 0, 0, 1]), [1, 1, 1, 1]);
|
||||
expectCloseToArray(labToRgb([50, 50, 0, 1]), [0.7562, 0.3045, 0.4756, 1], 4);
|
||||
expectCloseToArray(labToRgb([70, -45, 0, 1]), [0.1079, 0.7556, 0.664, 1], 4);
|
||||
expectCloseToArray(labToRgb([70, 0, 70, 1]), [0.7663, 0.6636, 0.0558, 1], 4);
|
||||
expectCloseToArray(labToRgb([55, 0, -60, 1]), [0.1281, 0.531, 0.9276, 1], 4);
|
||||
expectCloseToArray(labToRgb([29.57, 68.3, -112.03, 1]), [0, 0, 1, 1], 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HCL color space', () => {
|
||||
test('should convert colors from sRGB to HCL color space', () => {
|
||||
expectCloseToArray(rgbToHcl([0, 0, 0, 1]), [NaN, 0, 0, 1]);
|
||||
expectCloseToArray(rgbToHcl([1, 1, 1, 1]), [NaN, 0, 100, 1], 4);
|
||||
expectCloseToArray(rgbToHcl([0, 1, 0, 1]), [134.39, 113.34, 87.82, 1], 2);
|
||||
expectCloseToArray(rgbToHcl([0, 1, 1, 1]), [196.45, 52.83, 90.67, 1], 2);
|
||||
expectCloseToArray(rgbToHcl([0, 0, 1, 1]), [301.37, 131.21, 29.57, 1], 2);
|
||||
expectCloseToArray(rgbToHcl([1, 1, 0, 1]), [99.57, 94.71, 97.61, 1], 2);
|
||||
expectCloseToArray(rgbToHcl([1, 0, 0, 1]), [40.85, 106.84, 54.29, 1], 2);
|
||||
});
|
||||
|
||||
test('should convert colors from HCL to sRGB color space', () => {
|
||||
expectCloseToArray(hclToRgb([0, 0, 0, 1]), [0, 0, 0, 1]);
|
||||
expectCloseToArray(hclToRgb([0, 0, 100, 1]), [1, 1, 1, 1]);
|
||||
expectCloseToArray(hclToRgb([0, 50, 50, 1]), [0.7562, 0.3045, 0.4756, 1], 4);
|
||||
expectCloseToArray(hclToRgb([180, 45, 70, 1]), [0.1079, 0.7556, 0.664, 1], 4);
|
||||
expectCloseToArray(hclToRgb([90, 70, 70, 1]), [0.7663, 0.6636, 0.0558, 1], 4);
|
||||
expectCloseToArray(hclToRgb([270, 60, 55, 1]), [0.1281, 0.531, 0.9276, 1], 4);
|
||||
expectCloseToArray(hclToRgb([301.37, 131.21, 29.57, 1]), [0, 0, 1, 1], 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HSL color space', () => {
|
||||
test('should convert colors from HSL to sRGB color space', () => {
|
||||
expectCloseToArray(hslToRgb([0, 0, 0, 1]), [0, 0, 0, 1]);
|
||||
expectCloseToArray(hslToRgb([0, 100, 0, 1]), [0, 0, 0, 1]);
|
||||
expectCloseToArray(hslToRgb([0, 0, 100, 1]), [1, 1, 1, 1]);
|
||||
expectCloseToArray(hslToRgb([360, 0, 0, 1]), [0, 0, 0, 1]);
|
||||
expectCloseToArray(hslToRgb([120, 100, 25, 1]), [0, 128 / 255, 0, 1], 2);
|
||||
expectCloseToArray(hslToRgb([120, 30, 50, 0]), [89 / 255, 166 / 255, 89 / 255, 0], 2);
|
||||
expectCloseToArray(
|
||||
hslToRgb([240, 25, 50, 0.1]),
|
||||
[96 / 255, 96 / 255, 159 / 255, 0.1],
|
||||
2
|
||||
);
|
||||
expectCloseToArray(
|
||||
hslToRgb([240, 50, 50, 0.8]),
|
||||
[64 / 255, 64 / 255, 191 / 255, 0.8],
|
||||
2
|
||||
);
|
||||
expectCloseToArray(hslToRgb([270, 75, 75, 1]), [191 / 255, 143 / 255, 239 / 255, 1], 2);
|
||||
expectCloseToArray(hslToRgb([300, 100, 50, 0.5]), [1, 0, 1, 0.5]);
|
||||
expectCloseToArray(hslToRgb([330, 0, 25, 0.3]), [64 / 255, 64 / 255, 64 / 255, 0.3], 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
128
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_spaces.ts
generated
vendored
Normal file
128
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/color_spaces.ts
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @param r Red component 0..1
|
||||
* @param g Green component 0..1
|
||||
* @param b Blue component 0..1
|
||||
* @param alpha Alpha component 0..1
|
||||
*/
|
||||
export type RGBColor = [r: number, g: number, b: number, alpha: number];
|
||||
|
||||
/**
|
||||
* @param h Hue as degrees 0..360
|
||||
* @param s Saturation as percentage 0..100
|
||||
* @param l Lightness as percentage 0..100
|
||||
* @param alpha Alpha component 0..1
|
||||
*/
|
||||
export type HSLColor = [h: number, s: number, l: number, alpha: number];
|
||||
|
||||
/**
|
||||
* @param h Hue as degrees 0..360
|
||||
* @param c Chroma 0..~230
|
||||
* @param l Lightness as percentage 0..100
|
||||
* @param alpha Alpha component 0..1
|
||||
*/
|
||||
export type HCLColor = [h: number, c: number, l: number, alpha: number];
|
||||
|
||||
/**
|
||||
* @param l Lightness as percentage 0..100
|
||||
* @param a A axis value -125..125
|
||||
* @param b B axis value -125..125
|
||||
* @param alpha Alpha component 0..1
|
||||
*/
|
||||
export type LABColor = [l: number, a: number, b: number, alpha: number];
|
||||
|
||||
// See https://observablehq.com/@mbostock/lab-and-rgb
|
||||
const Xn = 0.96422,
|
||||
Yn = 1,
|
||||
Zn = 0.82521,
|
||||
t0 = 4 / 29,
|
||||
t1 = 6 / 29,
|
||||
t2 = 3 * t1 * t1,
|
||||
t3 = t1 * t1 * t1,
|
||||
deg2rad = Math.PI / 180,
|
||||
rad2deg = 180 / Math.PI;
|
||||
|
||||
function constrainAngle(angle: number): number {
|
||||
angle = angle % 360;
|
||||
if (angle < 0) {
|
||||
angle += 360;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
export function rgbToLab([r, g, b, alpha]: RGBColor): LABColor {
|
||||
r = rgb2xyz(r);
|
||||
g = rgb2xyz(g);
|
||||
b = rgb2xyz(b);
|
||||
let x, z;
|
||||
const y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn);
|
||||
if (r === g && g === b) {
|
||||
x = z = y;
|
||||
} else {
|
||||
x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);
|
||||
z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);
|
||||
}
|
||||
|
||||
const l = 116 * y - 16;
|
||||
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z), alpha];
|
||||
}
|
||||
|
||||
function rgb2xyz(x: number): number {
|
||||
return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
function xyz2lab(t: number): number {
|
||||
return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0;
|
||||
}
|
||||
|
||||
export function labToRgb([l, a, b, alpha]: LABColor): RGBColor {
|
||||
let y = (l + 16) / 116,
|
||||
x = isNaN(a) ? y : y + a / 500,
|
||||
z = isNaN(b) ? y : y - b / 200;
|
||||
|
||||
y = Yn * lab2xyz(y);
|
||||
x = Xn * lab2xyz(x);
|
||||
z = Zn * lab2xyz(z);
|
||||
|
||||
return [
|
||||
xyz2rgb(3.1338561 * x - 1.6168667 * y - 0.4906146 * z), // D50 -> sRGB
|
||||
xyz2rgb(-0.9787684 * x + 1.9161415 * y + 0.033454 * z),
|
||||
xyz2rgb(0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
|
||||
alpha
|
||||
];
|
||||
}
|
||||
|
||||
function xyz2rgb(x: number): number {
|
||||
x = x <= 0.00304 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
|
||||
return x < 0 ? 0 : x > 1 ? 1 : x; // clip to 0..1 range
|
||||
}
|
||||
|
||||
function lab2xyz(t: number): number {
|
||||
return t > t1 ? t * t * t : t2 * (t - t0);
|
||||
}
|
||||
|
||||
export function rgbToHcl(rgbColor: RGBColor): HCLColor {
|
||||
const [l, a, b, alpha] = rgbToLab(rgbColor);
|
||||
const c = Math.sqrt(a * a + b * b);
|
||||
const h = Math.round(c * 10000) ? constrainAngle(Math.atan2(b, a) * rad2deg) : NaN;
|
||||
return [h, c, l, alpha];
|
||||
}
|
||||
|
||||
export function hclToRgb([h, c, l, alpha]: HCLColor): RGBColor {
|
||||
h = isNaN(h) ? 0 : h * deg2rad;
|
||||
return labToRgb([l, Math.cos(h) * c, Math.sin(h) * c, alpha]);
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
|
||||
export function hslToRgb([h, s, l, alpha]: HSLColor): RGBColor {
|
||||
h = constrainAngle(h);
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
function f(n) {
|
||||
const k = (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||
}
|
||||
|
||||
return [f(0), f(8), f(4), alpha];
|
||||
}
|
||||
63
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/formatted.ts
generated
vendored
Normal file
63
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/formatted.ts
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
import type {Color} from '../../expression/types/color';
|
||||
import type {ResolvedImage} from '../types/resolved_image';
|
||||
|
||||
export const VERTICAL_ALIGN_OPTIONS = ['bottom', 'center', 'top'] as const;
|
||||
export type VerticalAlign = (typeof VERTICAL_ALIGN_OPTIONS)[number];
|
||||
|
||||
export class FormattedSection {
|
||||
text: string;
|
||||
image: ResolvedImage | null;
|
||||
scale: number | null;
|
||||
fontStack: string | null;
|
||||
textColor: Color | null;
|
||||
verticalAlign: VerticalAlign | null;
|
||||
|
||||
constructor(
|
||||
text: string,
|
||||
image: ResolvedImage | null,
|
||||
scale: number | null,
|
||||
fontStack: string | null,
|
||||
textColor: Color | null,
|
||||
verticalAlign: VerticalAlign | null
|
||||
) {
|
||||
this.text = text;
|
||||
this.image = image;
|
||||
this.scale = scale;
|
||||
this.fontStack = fontStack;
|
||||
this.textColor = textColor;
|
||||
this.verticalAlign = verticalAlign;
|
||||
}
|
||||
}
|
||||
|
||||
export class Formatted {
|
||||
sections: Array<FormattedSection>;
|
||||
|
||||
constructor(sections: Array<FormattedSection>) {
|
||||
this.sections = sections;
|
||||
}
|
||||
|
||||
static fromString(unformatted: string): Formatted {
|
||||
return new Formatted([new FormattedSection(unformatted, null, null, null, null, null)]);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
if (this.sections.length === 0) return true;
|
||||
return !this.sections.some(
|
||||
(section) =>
|
||||
section.text.length !== 0 || (section.image && section.image.name.length !== 0)
|
||||
);
|
||||
}
|
||||
|
||||
static factory(text: Formatted | string): Formatted {
|
||||
if (text instanceof Formatted) {
|
||||
return text;
|
||||
} else {
|
||||
return Formatted.fromString(text);
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.sections.length === 0) return '';
|
||||
return this.sections.map((section) => section.text).join('');
|
||||
}
|
||||
}
|
||||
36
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/number_array.test.ts
generated
vendored
Normal file
36
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/number_array.test.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import {NumberArray} from './number_array';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
describe('NumberArray', () => {
|
||||
test('NumberArray.parse', () => {
|
||||
expect(NumberArray.parse()).toBeUndefined();
|
||||
expect(NumberArray.parse(null)).toBeUndefined();
|
||||
expect(NumberArray.parse(undefined)).toBeUndefined();
|
||||
expect(NumberArray.parse('Dennis' as any)).toBeUndefined();
|
||||
expect(NumberArray.parse('3' as any)).toBeUndefined();
|
||||
expect(NumberArray.parse([3, '4'] as any)).toBeUndefined();
|
||||
expect(NumberArray.parse(5).values).toEqual([5]);
|
||||
expect(NumberArray.parse([]).values).toEqual([]);
|
||||
expect(NumberArray.parse([1]).values).toEqual([1]);
|
||||
expect(NumberArray.parse([1, 2]).values).toEqual([1, 2]);
|
||||
expect(NumberArray.parse([1, 2, 3]).values).toEqual([1, 2, 3]);
|
||||
expect(NumberArray.parse([1, 2, 3, 4]).values).toEqual([1, 2, 3, 4]);
|
||||
expect(NumberArray.parse([1, 2, 3, 4, 5]).values).toEqual([1, 2, 3, 4, 5]);
|
||||
|
||||
const passThru = new NumberArray([1, 2, 3, 4]);
|
||||
expect(NumberArray.parse(passThru)).toBe(passThru);
|
||||
});
|
||||
|
||||
test('NumberArray#toString', () => {
|
||||
const numberArray = new NumberArray([1, 2, 3, 4]);
|
||||
expect(numberArray.toString()).toBe('[1,2,3,4]');
|
||||
});
|
||||
|
||||
test('interpolate NumberArray', () => {
|
||||
const numberArray = new NumberArray([0, 0, 0, 0]);
|
||||
const targetNumberArray = new NumberArray([1, 2, 6, 4]);
|
||||
|
||||
const i11nFn = (t: number) => NumberArray.interpolate(numberArray, targetNumberArray, t);
|
||||
expect(i11nFn(0.5)).toBeInstanceOf(NumberArray);
|
||||
expect(i11nFn(0.5).values).toEqual([0.5, 1, 3, 2]);
|
||||
});
|
||||
});
|
||||
50
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/number_array.ts
generated
vendored
Normal file
50
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/number_array.ts
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import {interpolateArray} from '../../util/interpolate-primitives';
|
||||
|
||||
/**
|
||||
* An array of numbers. Create instances from
|
||||
* bare arrays or numeric values using the static method `NumberArray.parse`.
|
||||
* @private
|
||||
*/
|
||||
export class NumberArray {
|
||||
values: number[];
|
||||
|
||||
constructor(values: number[]) {
|
||||
this.values = values.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric NumberArray values
|
||||
* @param input A NumberArray value
|
||||
* @returns A `NumberArray` instance, or `undefined` if the input is not a valid NumberArray value.
|
||||
*/
|
||||
static parse(input?: number | number[] | NumberArray | null): NumberArray | undefined {
|
||||
if (input instanceof NumberArray) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Backwards compatibility (e.g. hillshade-illumination-direction): bare number is treated the same as array with single value.
|
||||
if (typeof input === 'number') {
|
||||
return new NumberArray([input]);
|
||||
}
|
||||
|
||||
if (!Array.isArray(input)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const val of input) {
|
||||
if (typeof val !== 'number') {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return new NumberArray(input);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this.values);
|
||||
}
|
||||
|
||||
static interpolate(from: NumberArray, to: NumberArray, t: number): NumberArray {
|
||||
return new NumberArray(interpolateArray(from.values, to.values, t));
|
||||
}
|
||||
}
|
||||
36
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/padding.test.ts
generated
vendored
Normal file
36
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/padding.test.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import {Padding} from './padding';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
describe('Padding', () => {
|
||||
test('Padding.parse', () => {
|
||||
expect(Padding.parse()).toBeUndefined();
|
||||
expect(Padding.parse(null)).toBeUndefined();
|
||||
expect(Padding.parse(undefined)).toBeUndefined();
|
||||
expect(Padding.parse('Dennis' as any)).toBeUndefined();
|
||||
expect(Padding.parse('3' as any)).toBeUndefined();
|
||||
expect(Padding.parse([])).toBeUndefined();
|
||||
expect(Padding.parse([3, '4'] as any)).toBeUndefined();
|
||||
expect(Padding.parse(5)).toEqual(new Padding([5, 5, 5, 5]));
|
||||
expect(Padding.parse([1])).toEqual(new Padding([1, 1, 1, 1]));
|
||||
expect(Padding.parse([1, 2])).toEqual(new Padding([1, 2, 1, 2]));
|
||||
expect(Padding.parse([1, 2, 3])).toEqual(new Padding([1, 2, 3, 2]));
|
||||
expect(Padding.parse([1, 2, 3, 4])).toEqual(new Padding([1, 2, 3, 4]));
|
||||
expect(Padding.parse([1, 2, 3, 4, 5])).toBeUndefined();
|
||||
|
||||
const passThru = new Padding([1, 2, 3, 4]);
|
||||
expect(Padding.parse(passThru)).toBe(passThru);
|
||||
});
|
||||
|
||||
test('Padding#toString', () => {
|
||||
const padding = new Padding([1, 2, 3, 4]);
|
||||
expect(padding.toString()).toBe('[1,2,3,4]');
|
||||
});
|
||||
|
||||
test('interpolate padding', () => {
|
||||
const padding = new Padding([0, 0, 0, 0]);
|
||||
const targetPadding = new Padding([1, 2, 6, 4]);
|
||||
|
||||
const i11nFn = (t: number) => Padding.interpolate(padding, targetPadding, t);
|
||||
expect(i11nFn(0.5)).toBeInstanceOf(Padding);
|
||||
expect(i11nFn(0.5)).toEqual(new Padding([0.5, 1, 3, 2]));
|
||||
});
|
||||
});
|
||||
69
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/padding.ts
generated
vendored
Normal file
69
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/padding.ts
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import {interpolateArray} from '../../util/interpolate-primitives';
|
||||
|
||||
/**
|
||||
* A set of four numbers representing padding around a box. Create instances from
|
||||
* bare arrays or numeric values using the static method `Padding.parse`.
|
||||
* @private
|
||||
*/
|
||||
export class Padding {
|
||||
/** Padding values are in CSS order: top, right, bottom, left */
|
||||
values: [number, number, number, number];
|
||||
|
||||
constructor(values: [number, number, number, number]) {
|
||||
this.values = values.slice() as [number, number, number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric padding values
|
||||
* @param input A padding value
|
||||
* @returns A `Padding` instance, or `undefined` if the input is not a valid padding value.
|
||||
*/
|
||||
static parse(input?: number | number[] | Padding | null): Padding | undefined {
|
||||
if (input instanceof Padding) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Backwards compatibility: bare number is treated the same as array with single value.
|
||||
// Padding applies to all four sides.
|
||||
if (typeof input === 'number') {
|
||||
return new Padding([input, input, input, input]);
|
||||
}
|
||||
|
||||
if (!Array.isArray(input)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (input.length < 1 || input.length > 4) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const val of input) {
|
||||
if (typeof val !== 'number') {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Expand shortcut properties into explicit 4-sided values
|
||||
switch (input.length) {
|
||||
case 1:
|
||||
input = [input[0], input[0], input[0], input[0]];
|
||||
break;
|
||||
case 2:
|
||||
input = [input[0], input[1], input[0], input[1]];
|
||||
break;
|
||||
case 3:
|
||||
input = [input[0], input[1], input[2], input[1]];
|
||||
break;
|
||||
}
|
||||
|
||||
return new Padding(input as [number, number, number, number]);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this.values);
|
||||
}
|
||||
|
||||
static interpolate(from: Padding, to: Padding, t: number): Padding {
|
||||
return new Padding(interpolateArray(from.values, to.values, t));
|
||||
}
|
||||
}
|
||||
369
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/parse_css_color.test.ts
generated
vendored
Normal file
369
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/parse_css_color.test.ts
generated
vendored
Normal file
@@ -0,0 +1,369 @@
|
||||
import {parseCssColor} from './parse_css_color';
|
||||
import * as colorSpacesModule from './color_spaces';
|
||||
import {RGBColor} from './color_spaces';
|
||||
import {describe, test, expect, afterEach, vi} from 'vitest';
|
||||
describe('parseCssColor', () => {
|
||||
// by changing the parse function, we can verify external css color parsers against our requirements
|
||||
const parse: (colorToParse: string) => RGBColor | undefined = parseCssColor;
|
||||
|
||||
describe('color keywords', () => {
|
||||
test('should parse valid color names', () => {
|
||||
expect(parse('white')).toEqual([1, 1, 1, 1]);
|
||||
expect(parse('black')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('RED')).toEqual([1, 0, 0, 1]);
|
||||
expect(parse('AquaMarine')).toEqual([127 / 255, 255 / 255, 212 / 255, 1]);
|
||||
expect(parse('steelblue')).toEqual([70 / 255, 130 / 255, 180 / 255, 1]);
|
||||
expect(parse('rebeccapurple')).toEqual([0.4, 0.2, 0.6, 1]);
|
||||
});
|
||||
|
||||
test('should parse "transparent" keyword as transparent black', () => {
|
||||
expect(parse('transparent')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('Transparent')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('TRANSPARENT')).toEqual([0, 0, 0, 0]);
|
||||
});
|
||||
|
||||
test('should return undefined when provided with invalid color name', () => {
|
||||
expect(parse('not a color name')).toBeUndefined();
|
||||
expect(parse('')).toBeUndefined();
|
||||
expect(parse('blak')).toBeUndefined();
|
||||
expect(parse('aqua-marine')).toBeUndefined();
|
||||
expect(parse('aqua_marine')).toBeUndefined();
|
||||
expect(parse('aqua marine')).toBeUndefined();
|
||||
expect(parse('__proto__')).toBeUndefined();
|
||||
expect(parse('valueOf')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RGB hexadecimal notations', () => {
|
||||
test('should parse valid rgb hex values', () => {
|
||||
// hex 3
|
||||
expect(parse('#fff')).toEqual([1, 1, 1, 1]);
|
||||
expect(parse('#000')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('#369')).toEqual([0.2, 0.4, 0.6, 1]);
|
||||
|
||||
// hex 4
|
||||
expect(parse('#ffff')).toEqual([1, 1, 1, 1]);
|
||||
expect(parse('#fff0')).toEqual([1, 1, 1, 0]);
|
||||
expect(parse('#0000')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('#FFFC')).toEqual([1, 1, 1, 0.8]);
|
||||
expect(parse('#234a')).toEqual([34 / 255, 51 / 255, 68 / 255, 2 / 3]);
|
||||
|
||||
// hex 6
|
||||
expect(parse('#ffffff')).toEqual([1, 1, 1, 1]);
|
||||
expect(parse('#000000')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('#008000')).toEqual([0, 128 / 255, 0, 1]);
|
||||
expect(parse('#b96710')).toEqual([185 / 255, 103 / 255, 16 / 255, 1]);
|
||||
|
||||
// hex 8
|
||||
expect(parse('#ffffffff')).toEqual([1, 1, 1, 1]);
|
||||
expect(parse('#000000ff')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('#00000000')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('#FFCc9933')).toEqual([255 / 255, 204 / 255, 153 / 255, 0.2]);
|
||||
expect(parse('#4682B466')).toEqual([70 / 255, 130 / 255, 180 / 255, 0.4]);
|
||||
});
|
||||
|
||||
test('should return undefined when provided with invalid rgb hex value', () => {
|
||||
expect(parse('#')).toBeUndefined();
|
||||
expect(parse('#f')).toBeUndefined();
|
||||
expect(parse('#ff')).toBeUndefined();
|
||||
expect(parse('#ffg')).toBeUndefined();
|
||||
expect(parse('#fffg')).toBeUndefined();
|
||||
expect(parse('#fffff')).toBeUndefined();
|
||||
expect(parse('#fffffg')).toBeUndefined();
|
||||
expect(parse('#fffffff')).toBeUndefined();
|
||||
expect(parse('#fffffffg')).toBeUndefined();
|
||||
expect(parse('#fffffffff')).toBeUndefined();
|
||||
expect(parse('fff')).toBeUndefined();
|
||||
expect(parse('# fff')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RGB functions "rgb()" and "rgba()"', () => {
|
||||
test('should parse valid rgb values', () => {
|
||||
// rgb 0..255
|
||||
expect(parse('rgb(0 51 0)')).toEqual([0, 0.2, 0, 1]);
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgb(0, 51, 0)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgb(0.0, 51.0, +0.0)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgba(0, 51, 0)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgba(0, 51, 0, 1)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgba(0, 51, 0, 100%)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgba( 0, 51, 0, 100% )'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgba( 00 ,51 ,0 ,100% )'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse(' rgb(.0 51 0 / 1)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgb(0.0 51.0 0.0 / 1.0) '));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgb(0 51 0 / 1.0)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('RGB(0 51 0 / 100%)'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgb( 0 51 0/1 )'));
|
||||
expect(parse('rgb(0 51 0)')).toEqual(parse('rgb(0 5.1e+1 0 / .1e1)'));
|
||||
expect(parse('rgb(0,0.5,1)')).toEqual([0, 0.5 / 255, 1 / 255, 1]);
|
||||
expect(parse('rgb(0,0.5,1)')).toEqual(parse('rgb(0 0.5 1)'));
|
||||
expect(parse('rgb(0,0,0,.1e-4)')).toEqual([0, 0, 0, 1e-5]);
|
||||
expect(parse('rgb(102,51,153)')).toEqual([0.4, 0.2, 0.6, 1]);
|
||||
expect(parse('rgb(26,207,26,0.5)')).toEqual([26 / 255, 207 / 255, 26 / 255, 0.5]);
|
||||
expect(parse('rgba(26,207,26,.73)')).toEqual([26 / 255, 207 / 255, 26 / 255, 0.73]);
|
||||
expect(parse('rgb(127.5 0 0)')).toEqual([0.5, 0, 0, 1]);
|
||||
expect(parse('rgb(128 0 0)')).toEqual([128 / 255, 0, 0, 1]);
|
||||
expect(parse('rgb(100 200 300)')).toEqual([100 / 255, 200 / 255, 1, 1]);
|
||||
expect(parse('rgb(-0 255 153)')).toEqual([0, 1, 0.6, 1]);
|
||||
expect(parse('rgb(-100 300 153)')).toEqual([0, 1, 0.6, 1]);
|
||||
expect(parse('rgb(-51, 306, 0)')).toEqual([0, 1, 0, 1]);
|
||||
expect(parse('rgba(0,0,0,0.1)')).toEqual([0, 0, 0, 0.1]);
|
||||
expect(parse('rgba(0,0,0,0.1)')).toEqual(parse('rgb(0 0 0 / .1)'));
|
||||
expect(parse('rgba(0,0,0,0.1)')).toEqual(parse('rgb(0 0 0 / 10%)'));
|
||||
expect(parse('rgb(0 0 0 / .0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0 0 0 / -.0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0 0 0 / -3.4e-2)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0 0 0 / -.2)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0 0 0 / -10%)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0 0 0 / 1.0)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('rgb(0 0 0 / 1.1)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('rgb(0 0 0 / 110%)')).toEqual([0, 0, 0, 1]);
|
||||
|
||||
// rgb 0%..100%
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual([0, 0.5, 0, 1]);
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgb(0%, 50%, 0%)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgba(0%, 50%, 0%, 1)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgba(0%, 50%, 0%, 100%)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgba(-0e1%,5E1%,0%,1)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgb(0% 50% 0% / 1.0)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgb(.0% 50% 0% / 100%)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgb(0.0% 50.0% 0.0% / 1)'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgb( 0% 50% 0% / 100% )'));
|
||||
expect(parse('rgb(0% 50% 0%)')).toEqual(parse('rgb(-1e-9% 50% 0% /1)'));
|
||||
expect(parse('rgb(-0% 100% 60%)')).toEqual([0, 1, 0.6, 1]);
|
||||
expect(parse('rgb(-10% 200% 60%)')).toEqual([0, 1, 0.6, 1]);
|
||||
expect(parse('rgb(100%,200%,300%)')).toEqual([1, 1, 1, 1]);
|
||||
expect(parse('rgb(128% 51% 255%)')).toEqual([1, 0.51, 1, 1]);
|
||||
expect(parse('rgba(0%,0%,0%,0.1)')).toEqual([0, 0, 0, 0.1]);
|
||||
expect(parse('rgba(0%,0%,0%,0.1)')).toEqual(parse('rgb(0% 0% 0% / .1)'));
|
||||
expect(parse('rgba(0%,0%,0%,0.1)')).toEqual(parse('rgb(0% 0% 0% / 10%)'));
|
||||
expect(parse('rgb(0% 0% 0% / .0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0% 0% 0% / -.0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0% 0% 0% / -3.4e-2)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0% 0% 0% / -.2)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0% 0% 0% / -10%)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('rgb(0% 0% 0% / 1.0)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('rgb(0% 0% 0% / 1.1)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('rgb(0% 0% 0% / 110%)')).toEqual([0, 0, 0, 1]);
|
||||
});
|
||||
|
||||
test('should return undefined when provided with invalid rgb value', () => {
|
||||
expect(parse('rgb (0,0,0)')).toBeUndefined();
|
||||
expect(parse('rgba (0,0,0,0)')).toBeUndefined();
|
||||
|
||||
const invalidArgs = [
|
||||
'10%, 50%, 0', // values must be all numbers or all percentages
|
||||
'255, 50%, 0%',
|
||||
'10%, 50%, 0, 1',
|
||||
'255, 50%, 0%, 1',
|
||||
'0 50% 255 / 1',
|
||||
'0 50% 0 / 1',
|
||||
'128 51% 255',
|
||||
'0, 0 0', // comma optional syntax requires no commas at all
|
||||
'0, 0, 0 0',
|
||||
'0, 0, 0 / 1',
|
||||
'0 0 0, 1',
|
||||
'0, 0, 0deg', // angles are not accepted in the rgb function
|
||||
'0, 0, 0, 0deg',
|
||||
'0, 0, light', // keywords are not accepted in the rgb function
|
||||
'0, 0, 0, light',
|
||||
'--1,0,0', // invalid numbers
|
||||
'+-1,0,0',
|
||||
'++1,0,0',
|
||||
'1.1.1,0,0',
|
||||
'.-1,0,0',
|
||||
'..1,0,0',
|
||||
'1e1.1,0,0',
|
||||
'1e.1,0,0',
|
||||
'--1e1,0,0',
|
||||
'+-1e1,0,0',
|
||||
'', // the rgb function requires 3 or 4 arguments
|
||||
'0',
|
||||
', 0,',
|
||||
'0, 0',
|
||||
'0, 0,',
|
||||
', 0, 0',
|
||||
'0 0 0 /',
|
||||
'0, 0, 0, 0, 0',
|
||||
'0, 0, 0, 0, 0,',
|
||||
', 0, 0, 0, 0, 0',
|
||||
'0%',
|
||||
', 0%,',
|
||||
'0%, 0%',
|
||||
'0%, 0%,',
|
||||
', 0%, 0%',
|
||||
'0%, 0%, 0%,',
|
||||
'0% 0% 0% /',
|
||||
'0%, 0%, 0%, 0%, 0%',
|
||||
'0%, 50%, 100%,',
|
||||
', 0%, 50%, 100%',
|
||||
', 0%, 50%, 100%, 100%',
|
||||
'0%, 50%, 100%, 100%,'
|
||||
];
|
||||
|
||||
for (const args of invalidArgs) {
|
||||
for (const fn of ['rgb', 'rgba']) {
|
||||
const input = `${fn}(${args})`;
|
||||
try {
|
||||
expect(parse(input)).toBeUndefined();
|
||||
} catch (error) {
|
||||
error.message = `\nInput: ${input}\n${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('HSL functions "hsl()" and "hsla()"', () => {
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test('should parse valid hsl values', () => {
|
||||
vi.spyOn(colorSpacesModule, 'hslToRgb').mockImplementation((hslColor) => hslColor);
|
||||
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual([300, 100, 25.1, 1]);
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual(
|
||||
parseCssColor('hsla(300,100%,25.1%,1)')
|
||||
);
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual(
|
||||
parseCssColor('hsla(300,100%,25.1%,100%)')
|
||||
);
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual(
|
||||
parseCssColor('hsl(300 100% 25.1%)')
|
||||
);
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual(
|
||||
parseCssColor('hsl(300 100% 25.1%/1.0)')
|
||||
);
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual(
|
||||
parseCssColor('hsl(300.0 100% 25.1% / 100%)')
|
||||
);
|
||||
expect(parseCssColor('hsl(300,100%,25.1%)')).toEqual(
|
||||
parseCssColor('hsl(300deg 100% 25.1% / 100%)')
|
||||
);
|
||||
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual([240, 0, 55, 0.2]);
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual(
|
||||
parseCssColor('hsla(240.0,0%,55%,0.2)')
|
||||
);
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual(
|
||||
parseCssColor('hsla( 240 ,.0% ,55.0% ,20% )')
|
||||
);
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual(
|
||||
parseCssColor('hsl(240 0% 55% / 0.2)')
|
||||
);
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual(
|
||||
parseCssColor('hsl(240 0% 55% / 20%)')
|
||||
);
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual(
|
||||
parseCssColor('hsl(24e1deg 0e1% 55% / 2e-1)')
|
||||
);
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.2)')).toEqual(
|
||||
parseCssColor('hsla(240 -1e-7% 55% / 2e1%)')
|
||||
);
|
||||
|
||||
expect(parseCssColor('hsl(240,0%,55%,0.9)')).toEqual([240, 0, 55, 0.9]);
|
||||
expect(parseCssColor('hsl(240,0%,55%,.0)')).toEqual([240, 0, 55, 0]);
|
||||
expect(parseCssColor('hsl(700 0% 67.3% / 100%)')).toEqual([700, 0, 67.3, 1]);
|
||||
expect(parseCssColor('Hsl( -100 -10.5% 67.3% / 100% )')).toEqual([-100, 0, 67.3, 1]);
|
||||
});
|
||||
|
||||
test('should parse valid hsl values and convert to rgb', () => {
|
||||
expect(parse('hsl(0 100% 50%)')).toEqual([1, 0, 0, 1]);
|
||||
expect(parse('hsl(240 100% 50%)')).toEqual([0, 0, 1, 1]);
|
||||
expect(parse('hsl(240 100% 25%)')).toEqual([0, 0, 0.5, 1]);
|
||||
expect(parse('hsl(273 75% 60%)')).toEqual([
|
||||
expect.closeTo(0.63),
|
||||
expect.closeTo(0.3),
|
||||
0.9,
|
||||
1
|
||||
]);
|
||||
|
||||
expect(parse('hsl(0 0% 0%)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('hsl(0 0% 0% / 0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('hsl(0 0% 0% / 0)')).toEqual(parse('hsl(0,0%,0%,+0)'));
|
||||
expect(parse('hsl(0 0% 0% / 0)')).toEqual(parse('hsla(0deg,0%,0%,-0)'));
|
||||
expect(parse('hsl(0 0% 0% / 0)')).toEqual(parse('hsla(0,0%,0%,0%)'));
|
||||
expect(parse('hsl(0 0% 0% / 0)')).toEqual(parse(' hsla(.0,.0%,.0%,.0%)'));
|
||||
expect(parse('hsl(0 0% 0% / 0)')).toEqual(parse('hsla( 0 ,0% ,0% ,.0 ) '));
|
||||
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual([0, 0.5, 0, 1]);
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsl(120.0 100.0% 25.0%)'));
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsl(120deg 100% 25%)'));
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsl(120 100% 25% / 1.0)'));
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsl(120deg 100% 25% / 1)'));
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsl(120 100% 25% / 100%)'));
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsl(120,100%,25%,100%)'));
|
||||
expect(parse('hsl(120 100% 25%)')).toEqual(parse('hsla(120deg,100%,25%,100%)'));
|
||||
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual([0, 1, 0, 0.25]);
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual(parse('HSLA(120,100%,50%,.25)'));
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual(parse('hsla(120,100%,50%,25%)'));
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual(parse('hsl(120 100% 50%/.25)'));
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual(parse('hsl(120deg 100% 50% / 25%)'));
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual(parse('hsl(480 100% 50% / 25%)'));
|
||||
expect(parse('hsl(120 100% 50% / .25)')).toEqual(parse('hsl(-240deg 100% 50% / 25%)'));
|
||||
|
||||
expect(parse('hsl(0.0 200% 50%)')).toEqual(parse('hsl(0 100% 50%)'));
|
||||
expect(parse('hsl(-0 -100% -100%)')).toEqual(parse('hsl(0 0% 0%)'));
|
||||
expect(parse('hsl(0 0% 0% / .0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('hsl(0 0% 0% / -.0)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('hsl(0 0% 0% / -3.4e-2)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('hsl(0 0% 0% / -.2)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('hsl(0 0% 0% / -10%)')).toEqual([0, 0, 0, 0]);
|
||||
expect(parse('hsl(0 0% 0% / 1.0)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('hsl(0 0% 0% / 1.1)')).toEqual([0, 0, 0, 1]);
|
||||
expect(parse('hsl(0 0% 0% / 110%)')).toEqual([0, 0, 0, 1]);
|
||||
});
|
||||
|
||||
test('should return undefined when provided with invalid hsl value', () => {
|
||||
expect(parse('hsl (0,0%,0%)')).toBeUndefined();
|
||||
expect(parse('hsla (0,0%,0%,1)')).toBeUndefined();
|
||||
|
||||
const invalidArgs = [
|
||||
'0,0%,0 %',
|
||||
'0%,0%,0%', // the first parameter of hsl/hsla must be a number or angle
|
||||
'0 deg,0%,0%',
|
||||
'10, 50%, 0', // the second and third parameters of hsl/hsla must be a percent
|
||||
'0, 0% 0%', // comma optional syntax requires no commas at all
|
||||
'0, 0% 0%, 1',
|
||||
'0,0%,light,1', // keywords are not accepted in the hsl function
|
||||
'--1,0%,0%', // invalid numbers
|
||||
'+-1,0%,0%',
|
||||
'++1,0%,0%',
|
||||
'1.1.1,0%,0%',
|
||||
'.-1,0%,0%',
|
||||
'..1,0%,0%',
|
||||
'1e1.1,0%,0%',
|
||||
'1e.1,0%,0%',
|
||||
'--1e1,0%,0%',
|
||||
'+-1e1,0%,0%',
|
||||
'', // The hsl function requires 3 or 4 arguments
|
||||
'0',
|
||||
'0 0%',
|
||||
'0, 0%,',
|
||||
', 0%, 0%',
|
||||
'0,0%,0%,1,0%',
|
||||
'0,0,0',
|
||||
'0,0%,0',
|
||||
'0,0,0%',
|
||||
'0 0% 0% /',
|
||||
'0,0%,0%,',
|
||||
', 0%,0%,0%'
|
||||
];
|
||||
|
||||
for (const args of invalidArgs) {
|
||||
for (const fn of ['hsl', 'hsla']) {
|
||||
const input = `${fn}(${args})`;
|
||||
try {
|
||||
expect(parse(input)).toBeUndefined();
|
||||
} catch (error) {
|
||||
error.message = `\nInput: ${input}\n${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
329
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/parse_css_color.ts
generated
vendored
Normal file
329
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/parse_css_color.ts
generated
vendored
Normal file
@@ -0,0 +1,329 @@
|
||||
import {getOwn} from '../../util/get_own';
|
||||
import {HSLColor, hslToRgb, RGBColor} from './color_spaces';
|
||||
|
||||
/**
|
||||
* CSS color parser compliant with CSS Color 4 Specification.
|
||||
* Supports: named colors, `transparent` keyword, all rgb hex notations,
|
||||
* rgb(), rgba(), hsl() and hsla() functions.
|
||||
* Does not round the parsed values to integers from the range 0..255.
|
||||
*
|
||||
* Syntax:
|
||||
*
|
||||
* <alpha-value> = <number> | <percentage>
|
||||
* <hue> = <number> | <angle>
|
||||
*
|
||||
* rgb() = rgb( <percentage>{3} [ / <alpha-value> ]? ) | rgb( <number>{3} [ / <alpha-value> ]? )
|
||||
* rgb() = rgb( <percentage>#{3} , <alpha-value>? ) | rgb( <number>#{3} , <alpha-value>? )
|
||||
*
|
||||
* hsl() = hsl( <hue> <percentage> <percentage> [ / <alpha-value> ]? )
|
||||
* hsl() = hsl( <hue>, <percentage>, <percentage>, <alpha-value>? )
|
||||
*
|
||||
* Caveats:
|
||||
* - <angle> - <number> with optional `deg` suffix; `grad`, `rad`, `turn` are not supported
|
||||
* - `none` keyword is not supported
|
||||
* - comments inside rgb()/hsl() are not supported
|
||||
* - legacy color syntax rgba() is supported with an identical grammar and behavior to rgb()
|
||||
* - legacy color syntax hsla() is supported with an identical grammar and behavior to hsl()
|
||||
*
|
||||
* @param input CSS color string to parse.
|
||||
* @returns Color in sRGB color space, with `red`, `green`, `blue`
|
||||
* and `alpha` channels normalized to the range 0..1,
|
||||
* or `undefined` if the input is not a valid color string.
|
||||
*/
|
||||
export function parseCssColor(input: string): RGBColor | undefined {
|
||||
input = input.toLowerCase().trim();
|
||||
|
||||
if (input === 'transparent') {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
// 'white', 'black', 'blue'
|
||||
const namedColorsMatch = getOwn(namedColors, input);
|
||||
if (namedColorsMatch) {
|
||||
const [r, g, b] = namedColorsMatch;
|
||||
return [r / 255, g / 255, b / 255, 1];
|
||||
}
|
||||
|
||||
// #f0c, #f0cf, #ff00cc, #ff00ccff
|
||||
if (input.startsWith('#')) {
|
||||
const hexRegexp = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/;
|
||||
if (hexRegexp.test(input)) {
|
||||
const step = input.length < 6 ? 1 : 2;
|
||||
let i = 1;
|
||||
return [
|
||||
parseHex(input.slice(i, (i += step))),
|
||||
parseHex(input.slice(i, (i += step))),
|
||||
parseHex(input.slice(i, (i += step))),
|
||||
parseHex(input.slice(i, i + step) || 'ff')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// rgb(128 0 0), rgb(50% 0% 0%), rgba(255,0,255,0.6), rgb(255 0 255 / 60%), rgb(100% 0% 100% /.6)
|
||||
if (input.startsWith('rgb')) {
|
||||
const rgbRegExp =
|
||||
/^rgba?\(\s*([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/;
|
||||
const rgbMatch = input.match(rgbRegExp);
|
||||
if (rgbMatch) {
|
||||
const [
|
||||
_, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
r, // <numeric>
|
||||
rp, // % (optional)
|
||||
f1, // , (optional)
|
||||
g, // <numeric>
|
||||
gp, // % (optional)
|
||||
f2, // , (optional)
|
||||
b, // <numeric>
|
||||
bp, // % (optional)
|
||||
f3, // ,|/ (optional)
|
||||
a, // <numeric> (optional)
|
||||
ap // % (optional)
|
||||
] = rgbMatch;
|
||||
|
||||
const argFormat = [f1 || ' ', f2 || ' ', f3].join('');
|
||||
if (
|
||||
argFormat === ' ' ||
|
||||
argFormat === ' /' ||
|
||||
argFormat === ',,' ||
|
||||
argFormat === ',,,'
|
||||
) {
|
||||
const valFormat = [rp, gp, bp].join('');
|
||||
const maxValue = valFormat === '%%%' ? 100 : valFormat === '' ? 255 : 0;
|
||||
if (maxValue) {
|
||||
const rgba: RGBColor = [
|
||||
clamp(+r / maxValue, 0, 1),
|
||||
clamp(+g / maxValue, 0, 1),
|
||||
clamp(+b / maxValue, 0, 1),
|
||||
a ? parseAlpha(+a, ap) : 1
|
||||
];
|
||||
if (validateNumbers(rgba)) {
|
||||
return rgba;
|
||||
}
|
||||
// invalid numbers
|
||||
}
|
||||
// values must be all numbers or all percentages
|
||||
}
|
||||
return; // comma optional syntax requires no commas at all
|
||||
}
|
||||
}
|
||||
|
||||
// hsl(120 50% 80%), hsla(120deg,50%,80%,.9), hsl(12e1 50% 80% / 90%)
|
||||
const hslRegExp =
|
||||
/^hsla?\(\s*([\de.+-]+)(?:deg)?(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/;
|
||||
const hslMatch = input.match(hslRegExp);
|
||||
if (hslMatch) {
|
||||
const [
|
||||
_, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
h, // <numeric>
|
||||
f1, // , (optional)
|
||||
s, // <numeric>
|
||||
f2, // , (optional)
|
||||
l, // <numeric>
|
||||
f3, // ,|/ (optional)
|
||||
a, // <numeric> (optional)
|
||||
ap // % (optional)
|
||||
] = hslMatch;
|
||||
|
||||
const argFormat = [f1 || ' ', f2 || ' ', f3].join('');
|
||||
if (
|
||||
argFormat === ' ' ||
|
||||
argFormat === ' /' ||
|
||||
argFormat === ',,' ||
|
||||
argFormat === ',,,'
|
||||
) {
|
||||
const hsla: HSLColor = [
|
||||
+h,
|
||||
clamp(+s, 0, 100),
|
||||
clamp(+l, 0, 100),
|
||||
a ? parseAlpha(+a, ap) : 1
|
||||
];
|
||||
if (validateNumbers(hsla)) {
|
||||
return hslToRgb(hsla);
|
||||
}
|
||||
// invalid numbers
|
||||
}
|
||||
// comma optional syntax requires no commas at all
|
||||
}
|
||||
}
|
||||
|
||||
function parseHex(hex: string): number {
|
||||
return parseInt(hex.padEnd(2, hex), 16) / 255;
|
||||
}
|
||||
|
||||
function parseAlpha(a: number, asPercentage: string | undefined): number {
|
||||
return clamp(asPercentage ? a / 100 : a, 0, 1);
|
||||
}
|
||||
|
||||
function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(min, n), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* The regular expression for numeric values is not super specific, and it may
|
||||
* happen that it will accept a value that is not a valid number. In order to
|
||||
* detect and eliminate such values this function exists.
|
||||
*
|
||||
* @param array Array of uncertain numbers.
|
||||
* @returns `true` if the specified array contains only valid numbers, `false` otherwise.
|
||||
*/
|
||||
function validateNumbers(array: number[]): boolean {
|
||||
return !array.some(Number.isNaN);
|
||||
}
|
||||
|
||||
/**
|
||||
* To generate:
|
||||
* - visit {@link https://www.w3.org/TR/css-color-4/#named-colors}
|
||||
* - run in the console:
|
||||
* @example
|
||||
* copy(`{\n${[...document.querySelector('.named-color-table tbody').children].map((tr) => `${tr.cells[2].textContent.trim()}: [${tr.cells[4].textContent.trim().split(/\s+/).join(', ')}],`).join('\n')}\n}`);
|
||||
*/
|
||||
const namedColors: Record<string, [number, number, number]> = {
|
||||
aliceblue: [240, 248, 255],
|
||||
antiquewhite: [250, 235, 215],
|
||||
aqua: [0, 255, 255],
|
||||
aquamarine: [127, 255, 212],
|
||||
azure: [240, 255, 255],
|
||||
beige: [245, 245, 220],
|
||||
bisque: [255, 228, 196],
|
||||
black: [0, 0, 0],
|
||||
blanchedalmond: [255, 235, 205],
|
||||
blue: [0, 0, 255],
|
||||
blueviolet: [138, 43, 226],
|
||||
brown: [165, 42, 42],
|
||||
burlywood: [222, 184, 135],
|
||||
cadetblue: [95, 158, 160],
|
||||
chartreuse: [127, 255, 0],
|
||||
chocolate: [210, 105, 30],
|
||||
coral: [255, 127, 80],
|
||||
cornflowerblue: [100, 149, 237],
|
||||
cornsilk: [255, 248, 220],
|
||||
crimson: [220, 20, 60],
|
||||
cyan: [0, 255, 255],
|
||||
darkblue: [0, 0, 139],
|
||||
darkcyan: [0, 139, 139],
|
||||
darkgoldenrod: [184, 134, 11],
|
||||
darkgray: [169, 169, 169],
|
||||
darkgreen: [0, 100, 0],
|
||||
darkgrey: [169, 169, 169],
|
||||
darkkhaki: [189, 183, 107],
|
||||
darkmagenta: [139, 0, 139],
|
||||
darkolivegreen: [85, 107, 47],
|
||||
darkorange: [255, 140, 0],
|
||||
darkorchid: [153, 50, 204],
|
||||
darkred: [139, 0, 0],
|
||||
darksalmon: [233, 150, 122],
|
||||
darkseagreen: [143, 188, 143],
|
||||
darkslateblue: [72, 61, 139],
|
||||
darkslategray: [47, 79, 79],
|
||||
darkslategrey: [47, 79, 79],
|
||||
darkturquoise: [0, 206, 209],
|
||||
darkviolet: [148, 0, 211],
|
||||
deeppink: [255, 20, 147],
|
||||
deepskyblue: [0, 191, 255],
|
||||
dimgray: [105, 105, 105],
|
||||
dimgrey: [105, 105, 105],
|
||||
dodgerblue: [30, 144, 255],
|
||||
firebrick: [178, 34, 34],
|
||||
floralwhite: [255, 250, 240],
|
||||
forestgreen: [34, 139, 34],
|
||||
fuchsia: [255, 0, 255],
|
||||
gainsboro: [220, 220, 220],
|
||||
ghostwhite: [248, 248, 255],
|
||||
gold: [255, 215, 0],
|
||||
goldenrod: [218, 165, 32],
|
||||
gray: [128, 128, 128],
|
||||
green: [0, 128, 0],
|
||||
greenyellow: [173, 255, 47],
|
||||
grey: [128, 128, 128],
|
||||
honeydew: [240, 255, 240],
|
||||
hotpink: [255, 105, 180],
|
||||
indianred: [205, 92, 92],
|
||||
indigo: [75, 0, 130],
|
||||
ivory: [255, 255, 240],
|
||||
khaki: [240, 230, 140],
|
||||
lavender: [230, 230, 250],
|
||||
lavenderblush: [255, 240, 245],
|
||||
lawngreen: [124, 252, 0],
|
||||
lemonchiffon: [255, 250, 205],
|
||||
lightblue: [173, 216, 230],
|
||||
lightcoral: [240, 128, 128],
|
||||
lightcyan: [224, 255, 255],
|
||||
lightgoldenrodyellow: [250, 250, 210],
|
||||
lightgray: [211, 211, 211],
|
||||
lightgreen: [144, 238, 144],
|
||||
lightgrey: [211, 211, 211],
|
||||
lightpink: [255, 182, 193],
|
||||
lightsalmon: [255, 160, 122],
|
||||
lightseagreen: [32, 178, 170],
|
||||
lightskyblue: [135, 206, 250],
|
||||
lightslategray: [119, 136, 153],
|
||||
lightslategrey: [119, 136, 153],
|
||||
lightsteelblue: [176, 196, 222],
|
||||
lightyellow: [255, 255, 224],
|
||||
lime: [0, 255, 0],
|
||||
limegreen: [50, 205, 50],
|
||||
linen: [250, 240, 230],
|
||||
magenta: [255, 0, 255],
|
||||
maroon: [128, 0, 0],
|
||||
mediumaquamarine: [102, 205, 170],
|
||||
mediumblue: [0, 0, 205],
|
||||
mediumorchid: [186, 85, 211],
|
||||
mediumpurple: [147, 112, 219],
|
||||
mediumseagreen: [60, 179, 113],
|
||||
mediumslateblue: [123, 104, 238],
|
||||
mediumspringgreen: [0, 250, 154],
|
||||
mediumturquoise: [72, 209, 204],
|
||||
mediumvioletred: [199, 21, 133],
|
||||
midnightblue: [25, 25, 112],
|
||||
mintcream: [245, 255, 250],
|
||||
mistyrose: [255, 228, 225],
|
||||
moccasin: [255, 228, 181],
|
||||
navajowhite: [255, 222, 173],
|
||||
navy: [0, 0, 128],
|
||||
oldlace: [253, 245, 230],
|
||||
olive: [128, 128, 0],
|
||||
olivedrab: [107, 142, 35],
|
||||
orange: [255, 165, 0],
|
||||
orangered: [255, 69, 0],
|
||||
orchid: [218, 112, 214],
|
||||
palegoldenrod: [238, 232, 170],
|
||||
palegreen: [152, 251, 152],
|
||||
paleturquoise: [175, 238, 238],
|
||||
palevioletred: [219, 112, 147],
|
||||
papayawhip: [255, 239, 213],
|
||||
peachpuff: [255, 218, 185],
|
||||
peru: [205, 133, 63],
|
||||
pink: [255, 192, 203],
|
||||
plum: [221, 160, 221],
|
||||
powderblue: [176, 224, 230],
|
||||
purple: [128, 0, 128],
|
||||
rebeccapurple: [102, 51, 153],
|
||||
red: [255, 0, 0],
|
||||
rosybrown: [188, 143, 143],
|
||||
royalblue: [65, 105, 225],
|
||||
saddlebrown: [139, 69, 19],
|
||||
salmon: [250, 128, 114],
|
||||
sandybrown: [244, 164, 96],
|
||||
seagreen: [46, 139, 87],
|
||||
seashell: [255, 245, 238],
|
||||
sienna: [160, 82, 45],
|
||||
silver: [192, 192, 192],
|
||||
skyblue: [135, 206, 235],
|
||||
slateblue: [106, 90, 205],
|
||||
slategray: [112, 128, 144],
|
||||
slategrey: [112, 128, 144],
|
||||
snow: [255, 250, 250],
|
||||
springgreen: [0, 255, 127],
|
||||
steelblue: [70, 130, 180],
|
||||
tan: [210, 180, 140],
|
||||
teal: [0, 128, 128],
|
||||
thistle: [216, 191, 216],
|
||||
tomato: [255, 99, 71],
|
||||
turquoise: [64, 224, 208],
|
||||
violet: [238, 130, 238],
|
||||
wheat: [245, 222, 179],
|
||||
white: [255, 255, 255],
|
||||
whitesmoke: [245, 245, 245],
|
||||
yellow: [255, 255, 0],
|
||||
yellowgreen: [154, 205, 50]
|
||||
};
|
||||
62
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/projection_definition.test.ts
generated
vendored
Normal file
62
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/projection_definition.test.ts
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import {ProjectionDefinition} from './projection_definition';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('Projection class', () => {
|
||||
test('should parse projection with multiple inputs', () => {
|
||||
const projection = ProjectionDefinition.parse(['mercator', 'vertical-perspective', 0.5]);
|
||||
expect(projection.from).toBe('mercator');
|
||||
expect(projection.to).toBe('vertical-perspective');
|
||||
expect(projection.transition).toBe(0.5);
|
||||
});
|
||||
|
||||
test('should parse projection with single input', () => {
|
||||
const projection = ProjectionDefinition.parse('mercator');
|
||||
expect(projection.from).toBe('mercator');
|
||||
expect(projection.to).toBe('mercator');
|
||||
expect(projection.transition).toBe(1);
|
||||
});
|
||||
|
||||
test('should return undefined when input is not an array or string', () => {
|
||||
const projection = ProjectionDefinition.parse({} as any);
|
||||
expect(projection).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined when input is an array with length not equal to 3', () => {
|
||||
const projection = ProjectionDefinition.parse(['mercator', 'vertical-perspective'] as any);
|
||||
expect(projection).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined when input is an array with non-string and non-number elements', () => {
|
||||
const projection = ProjectionDefinition.parse([1, 2, 3] as any);
|
||||
expect(projection).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should interpolate projections', () => {
|
||||
const projection = ProjectionDefinition.interpolate(
|
||||
'mercator',
|
||||
'vertical-perspective',
|
||||
0.5
|
||||
);
|
||||
expect(projection.from).toBe('mercator');
|
||||
expect(projection.to).toBe('vertical-perspective');
|
||||
expect(projection.transition).toBe(0.5);
|
||||
});
|
||||
|
||||
test('should parse projection object', () => {
|
||||
const projection = ProjectionDefinition.parse({
|
||||
from: 'mercator',
|
||||
to: 'vertical-perspective',
|
||||
transition: 0.5
|
||||
});
|
||||
expect(projection.from).toBe('mercator');
|
||||
expect(projection.to).toBe('vertical-perspective');
|
||||
expect(projection.transition).toBe(0.5);
|
||||
});
|
||||
|
||||
test('should serialize projection', () => {
|
||||
const projection = ProjectionDefinition.parse(['mercator', 'vertical-perspective', 0.5]);
|
||||
expect(JSON.stringify(projection)).toBe(
|
||||
'{\"from\":\"mercator\",\"to\":\"vertical-perspective\",\"transition\":0.5}'
|
||||
);
|
||||
});
|
||||
});
|
||||
44
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/projection_definition.ts
generated
vendored
Normal file
44
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/projection_definition.ts
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
export class ProjectionDefinition {
|
||||
readonly from: string;
|
||||
readonly to: string;
|
||||
readonly transition: number;
|
||||
|
||||
constructor(from: string, to: string, transition: number) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.transition = transition;
|
||||
}
|
||||
|
||||
static interpolate(from: string, to: string, t: number) {
|
||||
return new ProjectionDefinition(from, to, t);
|
||||
}
|
||||
|
||||
static parse(input?: any): ProjectionDefinition {
|
||||
if (input instanceof ProjectionDefinition) {
|
||||
return input;
|
||||
}
|
||||
if (
|
||||
Array.isArray(input) &&
|
||||
input.length === 3 &&
|
||||
typeof input[0] === 'string' &&
|
||||
typeof input[1] === 'string' &&
|
||||
typeof input[2] === 'number'
|
||||
) {
|
||||
return new ProjectionDefinition(input[0], input[1], input[2]);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof input === 'object' &&
|
||||
typeof input.from === 'string' &&
|
||||
typeof input.to === 'string' &&
|
||||
typeof input.transition === 'number'
|
||||
) {
|
||||
return new ProjectionDefinition(input.from, input.to, input.transition);
|
||||
}
|
||||
|
||||
if (typeof input === 'string') {
|
||||
return new ProjectionDefinition(input, input, 1);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
23
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/resolved_image.ts
generated
vendored
Normal file
23
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/resolved_image.ts
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export type ResolvedImageOptions = {
|
||||
name: string;
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
export class ResolvedImage {
|
||||
name: string;
|
||||
available: boolean;
|
||||
|
||||
constructor(options: ResolvedImageOptions) {
|
||||
this.name = options.name;
|
||||
this.available = options.available;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
static fromString(name: string): ResolvedImage | null {
|
||||
if (!name) return null; // treat empty values as no image
|
||||
return new ResolvedImage({name, available: false});
|
||||
}
|
||||
}
|
||||
62
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/variable_anchor_offset_collection.test.ts
generated
vendored
Normal file
62
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/variable_anchor_offset_collection.test.ts
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
import {VariableAnchorOffsetCollection} from './variable_anchor_offset_collection';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('VariableAnchorOffsetCollection', () => {
|
||||
test('VariableAnchorOffsetCollection.parse', () => {
|
||||
expect(VariableAnchorOffsetCollection.parse()).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(null)).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(undefined)).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse('Dennis' as any)).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(3 as any)).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse({} as any)).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse([])).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['Dennis'])).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['top'])).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['top', 'bottom'])).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['top', 3] as any)).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['Dennis', [2, 2]])).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['top', [2, 2]])).toEqual(
|
||||
new VariableAnchorOffsetCollection(['top', [2, 2]])
|
||||
);
|
||||
expect(VariableAnchorOffsetCollection.parse(['top', [2, 2], 'bottom'])).toBeUndefined();
|
||||
expect(VariableAnchorOffsetCollection.parse(['top', [2, 2], 'bottom', [3, 3]])).toEqual(
|
||||
new VariableAnchorOffsetCollection(['top', [2, 2], 'bottom', [3, 3]])
|
||||
);
|
||||
|
||||
const identity = new VariableAnchorOffsetCollection(['top', [2, 2]]);
|
||||
expect(VariableAnchorOffsetCollection.parse(identity)).toBe(identity);
|
||||
});
|
||||
|
||||
test('VariableAnchorOffsetCollection#toString', () => {
|
||||
const coll = new VariableAnchorOffsetCollection(['top', [2, 2]]);
|
||||
expect(coll.toString()).toBe('["top",[2,2]]');
|
||||
});
|
||||
|
||||
describe('interpolate variableAnchorOffsetCollection', () => {
|
||||
const i11nFn = VariableAnchorOffsetCollection.interpolate;
|
||||
const parseFn = VariableAnchorOffsetCollection.parse;
|
||||
|
||||
test('should throw with mismatched endpoints', () => {
|
||||
expect(() =>
|
||||
i11nFn(parseFn(['top', [0, 0]]), parseFn(['bottom', [1, 1]]), 0.5)
|
||||
).toThrowError(
|
||||
'Cannot interpolate values containing mismatched anchors. from[0]: top, to[0]: bottom'
|
||||
);
|
||||
expect(() =>
|
||||
i11nFn(parseFn(['top', [0, 0]]), parseFn(['top', [1, 1], 'bottom', [2, 2]]), 0.5)
|
||||
).toThrowError(
|
||||
'Cannot interpolate values of different length. from: ["top",[0,0]], to: ["top",[1,1],"bottom",[2,2]]'
|
||||
);
|
||||
});
|
||||
|
||||
test('should interpolate offsets', () => {
|
||||
expect(
|
||||
i11nFn(
|
||||
parseFn(['top', [0, 0], 'bottom', [2, 2]]),
|
||||
parseFn(['top', [1, 1], 'bottom', [4, 4]]),
|
||||
0.5
|
||||
).values
|
||||
).toEqual(['top', [0.5, 0.5], 'bottom', [3, 3]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
101
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/variable_anchor_offset_collection.ts
generated
vendored
Normal file
101
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/types/variable_anchor_offset_collection.ts
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
import {RuntimeError} from '../runtime_error';
|
||||
import {interpolateNumber} from '../../util/interpolate-primitives';
|
||||
import type {VariableAnchorOffsetCollectionSpecification} from '../../types.g';
|
||||
|
||||
/** Set of valid anchor positions, as a set for validation */
|
||||
const anchors = new Set([
|
||||
'center',
|
||||
'left',
|
||||
'right',
|
||||
'top',
|
||||
'bottom',
|
||||
'top-left',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-right'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Utility class to assist managing values for text-variable-anchor-offset property. Create instances from
|
||||
* bare arrays using the static method `VariableAnchorOffsetCollection.parse`.
|
||||
* @private
|
||||
*/
|
||||
export class VariableAnchorOffsetCollection {
|
||||
/** Series of paired of anchor (string) and offset (point) values */
|
||||
values: VariableAnchorOffsetCollectionSpecification;
|
||||
|
||||
constructor(values: VariableAnchorOffsetCollectionSpecification) {
|
||||
this.values = values.slice();
|
||||
}
|
||||
|
||||
static parse(
|
||||
input?: VariableAnchorOffsetCollectionSpecification | VariableAnchorOffsetCollection
|
||||
): VariableAnchorOffsetCollection | undefined {
|
||||
if (input instanceof VariableAnchorOffsetCollection) {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (!Array.isArray(input) || input.length < 1 || input.length % 2 !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (let i = 0; i < input.length; i += 2) {
|
||||
// Elements in even positions should be anchor positions; Elements in odd positions should be offset values
|
||||
const anchorValue = input[i];
|
||||
const offsetValue = input[i + 1];
|
||||
|
||||
if (typeof anchorValue !== 'string' || !anchors.has(anchorValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(offsetValue) ||
|
||||
offsetValue.length !== 2 ||
|
||||
typeof offsetValue[0] !== 'number' ||
|
||||
typeof offsetValue[1] !== 'number'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return new VariableAnchorOffsetCollection(input);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify(this.values);
|
||||
}
|
||||
|
||||
static interpolate(
|
||||
from: VariableAnchorOffsetCollection,
|
||||
to: VariableAnchorOffsetCollection,
|
||||
t: number
|
||||
): VariableAnchorOffsetCollection {
|
||||
const fromValues = from.values;
|
||||
const toValues = to.values;
|
||||
|
||||
if (fromValues.length !== toValues.length) {
|
||||
throw new RuntimeError(
|
||||
`Cannot interpolate values of different length. from: ${from.toString()}, to: ${to.toString()}`
|
||||
);
|
||||
}
|
||||
|
||||
const output: VariableAnchorOffsetCollectionSpecification = [];
|
||||
|
||||
for (let i = 0; i < fromValues.length; i += 2) {
|
||||
// Anchor entries must match
|
||||
if (fromValues[i] !== toValues[i]) {
|
||||
throw new RuntimeError(
|
||||
`Cannot interpolate values containing mismatched anchors. from[${i}]: ${fromValues[i]}, to[${i}]: ${toValues[i]}`
|
||||
);
|
||||
}
|
||||
output.push(fromValues[i]);
|
||||
|
||||
// Interpolate the offset values for each anchor
|
||||
const [fx, fy] = fromValues[i + 1] as [number, number];
|
||||
const [tx, ty] = toValues[i + 1] as [number, number];
|
||||
output.push([interpolateNumber(fx, tx, t), interpolateNumber(fy, ty, t)]);
|
||||
}
|
||||
|
||||
return new VariableAnchorOffsetCollection(output);
|
||||
}
|
||||
}
|
||||
35
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/values.test.ts
generated
vendored
Normal file
35
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/values.test.ts
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
CollatorType,
|
||||
ColorArrayType,
|
||||
ColorType,
|
||||
FormattedType,
|
||||
NumberArrayType,
|
||||
PaddingType,
|
||||
ProjectionDefinitionType,
|
||||
VariableAnchorOffsetCollectionType
|
||||
} from './types';
|
||||
import {Collator} from './types/collator';
|
||||
import {Color} from './types/color';
|
||||
import {ColorArray} from './types/color_array';
|
||||
import {Formatted} from './types/formatted';
|
||||
import {NumberArray} from './types/number_array';
|
||||
import {Padding} from './types/padding';
|
||||
import {ProjectionDefinition} from './types/projection_definition';
|
||||
import {VariableAnchorOffsetCollection} from './types/variable_anchor_offset_collection';
|
||||
import {typeOf} from './values';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
|
||||
describe('typeOf', () => {
|
||||
test('typeOf', () => {
|
||||
expect(typeOf(Color.red)).toBe(ColorType);
|
||||
expect(typeOf(ProjectionDefinition.parse('mercator'))).toBe(ProjectionDefinitionType);
|
||||
expect(typeOf(new Collator(false, false, null))).toBe(CollatorType);
|
||||
expect(typeOf(Formatted.factory('a'))).toBe(FormattedType);
|
||||
expect(typeOf(Padding.parse(1))).toBe(PaddingType);
|
||||
expect(typeOf(NumberArray.parse(1))).toBe(NumberArrayType);
|
||||
expect(typeOf(ColorArray.parse('red'))).toBe(ColorArrayType);
|
||||
expect(typeOf(VariableAnchorOffsetCollection.parse(['top', [2, 2]]))).toBe(
|
||||
VariableAnchorOffsetCollectionType
|
||||
);
|
||||
});
|
||||
});
|
||||
180
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/values.ts
generated
vendored
Normal file
180
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/values.ts
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
import {Color} from './types/color';
|
||||
import {Collator} from './types/collator';
|
||||
import {Formatted} from './types/formatted';
|
||||
import {Padding} from './types/padding';
|
||||
import {NumberArray} from './types/number_array';
|
||||
import {ColorArray} from './types/color_array';
|
||||
import {VariableAnchorOffsetCollection} from './types/variable_anchor_offset_collection';
|
||||
import {ResolvedImage} from './types/resolved_image';
|
||||
import {ProjectionDefinition} from './types/projection_definition';
|
||||
import {
|
||||
NullType,
|
||||
NumberType,
|
||||
StringType,
|
||||
BooleanType,
|
||||
ColorType,
|
||||
ObjectType,
|
||||
ValueType,
|
||||
CollatorType,
|
||||
FormattedType,
|
||||
ResolvedImageType,
|
||||
array,
|
||||
PaddingType,
|
||||
NumberArrayType,
|
||||
ColorArrayType,
|
||||
VariableAnchorOffsetCollectionType,
|
||||
ProjectionDefinitionType
|
||||
} from './types';
|
||||
|
||||
import type {Type} from './types';
|
||||
|
||||
export function validateRGBA(r: unknown, g: unknown, b: unknown, a?: unknown): string | null {
|
||||
if (
|
||||
!(
|
||||
typeof r === 'number' &&
|
||||
r >= 0 &&
|
||||
r <= 255 &&
|
||||
typeof g === 'number' &&
|
||||
g >= 0 &&
|
||||
g <= 255 &&
|
||||
typeof b === 'number' &&
|
||||
b >= 0 &&
|
||||
b <= 255
|
||||
)
|
||||
) {
|
||||
const value = typeof a === 'number' ? [r, g, b, a] : [r, g, b];
|
||||
return `Invalid rgba value [${value.join(', ')}]: 'r', 'g', and 'b' must be between 0 and 255.`;
|
||||
}
|
||||
|
||||
if (!(typeof a === 'undefined' || (typeof a === 'number' && a >= 0 && a <= 1))) {
|
||||
return `Invalid rgba value [${[r, g, b, a].join(', ')}]: 'a' must be between 0 and 1.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export type Value =
|
||||
| null
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
| Color
|
||||
| ProjectionDefinition
|
||||
| Collator
|
||||
| Formatted
|
||||
| Padding
|
||||
| NumberArray
|
||||
| ColorArray
|
||||
| ResolvedImage
|
||||
| VariableAnchorOffsetCollection
|
||||
| ReadonlyArray<Value>
|
||||
| {
|
||||
readonly [x: string]: Value;
|
||||
};
|
||||
|
||||
export function isValue(mixed: unknown): boolean {
|
||||
if (
|
||||
mixed === null ||
|
||||
typeof mixed === 'string' ||
|
||||
typeof mixed === 'boolean' ||
|
||||
typeof mixed === 'number' ||
|
||||
mixed instanceof ProjectionDefinition ||
|
||||
mixed instanceof Color ||
|
||||
mixed instanceof Collator ||
|
||||
mixed instanceof Formatted ||
|
||||
mixed instanceof Padding ||
|
||||
mixed instanceof NumberArray ||
|
||||
mixed instanceof ColorArray ||
|
||||
mixed instanceof VariableAnchorOffsetCollection ||
|
||||
mixed instanceof ResolvedImage
|
||||
) {
|
||||
return true;
|
||||
} else if (Array.isArray(mixed)) {
|
||||
for (const item of mixed) {
|
||||
if (!isValue(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (typeof mixed === 'object') {
|
||||
for (const key in mixed) {
|
||||
if (!isValue(mixed[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function typeOf(value: Value): Type {
|
||||
if (value === null) {
|
||||
return NullType;
|
||||
} else if (typeof value === 'string') {
|
||||
return StringType;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return BooleanType;
|
||||
} else if (typeof value === 'number') {
|
||||
return NumberType;
|
||||
} else if (value instanceof Color) {
|
||||
return ColorType;
|
||||
} else if (value instanceof ProjectionDefinition) {
|
||||
return ProjectionDefinitionType;
|
||||
} else if (value instanceof Collator) {
|
||||
return CollatorType;
|
||||
} else if (value instanceof Formatted) {
|
||||
return FormattedType;
|
||||
} else if (value instanceof Padding) {
|
||||
return PaddingType;
|
||||
} else if (value instanceof NumberArray) {
|
||||
return NumberArrayType;
|
||||
} else if (value instanceof ColorArray) {
|
||||
return ColorArrayType;
|
||||
} else if (value instanceof VariableAnchorOffsetCollection) {
|
||||
return VariableAnchorOffsetCollectionType;
|
||||
} else if (value instanceof ResolvedImage) {
|
||||
return ResolvedImageType;
|
||||
} else if (Array.isArray(value)) {
|
||||
const length = value.length;
|
||||
let itemType: Type | typeof undefined;
|
||||
|
||||
for (const item of value) {
|
||||
const t = typeOf(item);
|
||||
if (!itemType) {
|
||||
itemType = t;
|
||||
} else if (itemType === t) {
|
||||
continue;
|
||||
} else {
|
||||
itemType = ValueType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array(itemType || ValueType, length);
|
||||
} else {
|
||||
return ObjectType;
|
||||
}
|
||||
}
|
||||
|
||||
export function valueToString(value: Value) {
|
||||
const type = typeof value;
|
||||
if (value === null) {
|
||||
return '';
|
||||
} else if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return String(value);
|
||||
} else if (
|
||||
value instanceof Color ||
|
||||
value instanceof ProjectionDefinition ||
|
||||
value instanceof Formatted ||
|
||||
value instanceof Padding ||
|
||||
value instanceof NumberArray ||
|
||||
value instanceof ColorArray ||
|
||||
value instanceof VariableAnchorOffsetCollection ||
|
||||
value instanceof ResolvedImage
|
||||
) {
|
||||
return value.toString();
|
||||
} else {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
140
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/visibility.test.ts
generated
vendored
Normal file
140
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/visibility.test.ts
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
import createVisibilityExpression from './visibility';
|
||||
import {describe, test, expect, vi} from 'vitest';
|
||||
|
||||
describe('create visibility expression', () => {
|
||||
test('throws Error for invalid function', () => {
|
||||
expect(() => createVisibilityExpression(['bla'] as any, {})).toThrowError(
|
||||
'Unknown expression "bla". If you wanted a literal array, use ["literal", [...]].'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluate visibility expression', () => {
|
||||
test('literal value none', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const value = createVisibilityExpression('none', {});
|
||||
expect(value.evaluate()).toBe('none');
|
||||
expect(value.getGlobalStateRefs().size).toBe(0);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('literal value visible', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const value = createVisibilityExpression('visible', {});
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(value.getGlobalStateRefs().size).toBe(0);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('global state property set to none', () => {
|
||||
const globalState: Record<string, any> = {};
|
||||
const value = createVisibilityExpression(['global-state', 'x'], globalState);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
globalState.x = 'none';
|
||||
expect(value.evaluate()).toBe('none');
|
||||
expect(value.getGlobalStateRefs().has('x')).toBe(true);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('global state property set to visible', () => {
|
||||
const globalState: Record<string, any> = {};
|
||||
const value = createVisibilityExpression(['global-state', 'x'], globalState);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
globalState.x = 'visible';
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(value.getGlobalStateRefs().has('x')).toBe(true);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('global state flag set to false', () => {
|
||||
const globalState: Record<string, any> = {};
|
||||
const value = createVisibilityExpression(
|
||||
['case', ['global-state', 'x'], 'visible', 'none'],
|
||||
globalState
|
||||
);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
globalState.x = false;
|
||||
expect(value.evaluate()).toBe('none');
|
||||
expect(value.getGlobalStateRefs().has('x')).toBe(true);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('global state flag set to true', () => {
|
||||
const globalState: Record<string, any> = {};
|
||||
const value = createVisibilityExpression(
|
||||
['case', ['global-state', 'x'], 'visible', 'none'],
|
||||
globalState
|
||||
);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
globalState.x = true;
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(value.getGlobalStateRefs().has('x')).toBe(true);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('falls back to default for invalid expression with zoom', () => {
|
||||
const value = createVisibilityExpression(
|
||||
['case', ['==', ['zoom'], 5], 'none', 'visible'],
|
||||
{}
|
||||
);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('warns and falls back to default for invalid expression with feature', () => {
|
||||
const value = createVisibilityExpression(['get', 'x'], {});
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Expected value to be of type string, but found null instead.'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns and falls back to default for invalid expression with feature state', () => {
|
||||
const value = createVisibilityExpression(['feature-state', 'x'], {});
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Expected value to be of type string, but found null instead.'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns and falls back to default for missing global property', () => {
|
||||
const value = createVisibilityExpression(['global-state', 'x'], {});
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Expected value to be of type string, but found null instead.'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns and falls back to default for invalid global property', () => {
|
||||
const value = createVisibilityExpression(['global-state', 'x'], {x: 'invalid'});
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
expect(value.evaluate()).toBe('visible');
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Expected value to be one of "visible", "none", but found "invalid" instead.'
|
||||
);
|
||||
});
|
||||
});
|
||||
86
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/visibility.ts
generated
vendored
Normal file
86
node_modules/@maplibre/maplibre-gl-style-spec/src/expression/visibility.ts
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
import {createExpression, findGlobalStateRefs} from './index';
|
||||
import {StyleExpression, type GlobalProperties, type StylePropertySpecification} from '../index';
|
||||
import {type VisibilitySpecification} from '../types.g';
|
||||
|
||||
const visibilitySpec: StylePropertySpecification = {
|
||||
type: 'enum',
|
||||
'property-type': 'data-constant',
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: ['global-state']
|
||||
},
|
||||
values: {visible: {}, none: {}},
|
||||
transition: false,
|
||||
default: 'visible'
|
||||
};
|
||||
|
||||
export interface VisibilityExpression {
|
||||
/**
|
||||
* Evaluates the visibility expression and returns either 'visible' or 'none'.
|
||||
*/
|
||||
evaluate: () => 'visible' | 'none';
|
||||
/**
|
||||
* Updates the visibility specification.
|
||||
*/
|
||||
setValue: (visibility: VisibilitySpecification) => void;
|
||||
/**
|
||||
* Returns the set of global state properties referenced by the expression.
|
||||
*/
|
||||
getGlobalStateRefs: () => Set<string>;
|
||||
}
|
||||
|
||||
class VisibilityExpressionClass implements VisibilityExpression {
|
||||
private _globalState: Record<string, any>;
|
||||
private _globalStateRefs: Set<string>;
|
||||
private _literalValue: 'visible' | 'none' | undefined;
|
||||
private _compiledValue: StyleExpression;
|
||||
|
||||
constructor(visibility: VisibilitySpecification, globalState: Record<string, any>) {
|
||||
this._globalState = globalState;
|
||||
this.setValue(visibility);
|
||||
}
|
||||
|
||||
evaluate(): 'visible' | 'none' {
|
||||
return this._literalValue ?? this._compiledValue.evaluate({} as GlobalProperties);
|
||||
}
|
||||
|
||||
setValue(visibility: VisibilitySpecification) {
|
||||
if (
|
||||
visibility === null ||
|
||||
visibility === undefined ||
|
||||
visibility === 'visible' ||
|
||||
visibility === 'none'
|
||||
) {
|
||||
this._literalValue = visibility === 'none' ? 'none' : 'visible';
|
||||
this._compiledValue = undefined;
|
||||
this._globalStateRefs = new Set<string>();
|
||||
return;
|
||||
}
|
||||
const compiled = createExpression(visibility, visibilitySpec, this._globalState);
|
||||
if (compiled.result === 'error') {
|
||||
this._literalValue = 'visible';
|
||||
this._compiledValue = undefined;
|
||||
throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(', '));
|
||||
}
|
||||
this._literalValue = undefined;
|
||||
this._compiledValue = compiled.value;
|
||||
this._globalStateRefs = findGlobalStateRefs(compiled.value.expression);
|
||||
}
|
||||
|
||||
getGlobalStateRefs() {
|
||||
return this._globalStateRefs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a visibility expression from a visibility specification.
|
||||
* @param visibility - the visibility specification, literal or expression
|
||||
* @param globalState - the global state object
|
||||
* @returns visibility expression object
|
||||
*/
|
||||
export default function createVisibility(
|
||||
visibility: VisibilitySpecification,
|
||||
globalState: Record<string, any>
|
||||
): VisibilityExpression {
|
||||
return new VisibilityExpressionClass(visibility, globalState);
|
||||
}
|
||||
55
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/README.md
generated
vendored
Normal file
55
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/README.md
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
## Filter
|
||||
|
||||
Filter expressions are used to target specific data in a layer. This library implements the semantics specified by the [MapLibre GL JS spec](https://maplibre.org/maplibre-gl-js-docs/style-spec/).
|
||||
|
||||
### API
|
||||
|
||||
`featureFilter(filter)`
|
||||
|
||||
Given a filter expressed as nested arrays, return a new function
|
||||
that evaluates whether a given feature (with a .properties or .tags property)
|
||||
passes its test.
|
||||
|
||||
#### Parameters
|
||||
|
||||
| parameter | type | description |
|
||||
| --------- | ----- | ---------------- |
|
||||
| `filter` | Array | MapLibre filter |
|
||||
|
||||
**Returns** `Function`, filter-evaluating function
|
||||
|
||||
### Usage
|
||||
|
||||
``` javascript
|
||||
var ff = require('@maplibre/maplibre-gl-style-spec').featureFilter;
|
||||
|
||||
// will match a feature with class of street_limited,
|
||||
// AND an admin_level less than or equal to 3,
|
||||
// that's NOT a polygon.
|
||||
var filter = [
|
||||
"all",
|
||||
["==", "class", "street_limited"],
|
||||
["<=", "admin_level", 3],
|
||||
["!=", "$type", "Polygon"]
|
||||
]
|
||||
|
||||
// will match a feature that has a class of
|
||||
// wetland OR wetland_noveg.
|
||||
// ["in", "class", "wetland", "wetland_noveg"]
|
||||
|
||||
// testFilter will be a function that returns a boolean
|
||||
var testFilter = ff(filter);
|
||||
|
||||
// Layer feature that you're testing. Must have type
|
||||
// and properties keys.
|
||||
var feature = {
|
||||
type: 2,
|
||||
properties: {
|
||||
class: "street_limited",
|
||||
admin_level: 1
|
||||
}
|
||||
};
|
||||
|
||||
// will return a boolean based on whether the feature matched the filter
|
||||
return testFilter({zoom: 0}, feature);
|
||||
```
|
||||
226
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/convert.ts
generated
vendored
Normal file
226
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/convert.ts
generated
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
import {isExpressionFilter} from './index';
|
||||
|
||||
import type {
|
||||
ExpressionFilterSpecification,
|
||||
ExpressionInputType,
|
||||
ExpressionSpecification,
|
||||
FilterSpecification,
|
||||
LegacyFilterSpecification
|
||||
} from '../types.g';
|
||||
|
||||
type ExpectedTypes = {[_: string]: ExpressionInputType};
|
||||
|
||||
/*
|
||||
* Convert the given filter to an expression, storing the expected types for
|
||||
* any feature properties referenced in expectedTypes.
|
||||
*
|
||||
* These expected types are needed in order to construct preflight type checks
|
||||
* needed for handling 'any' filters. A preflight type check is necessary in
|
||||
* order to mimic legacy filters' semantics around expected type mismatches.
|
||||
* For example, consider the legacy filter:
|
||||
*
|
||||
* ["any", ["all", [">", "y", 0], [">", "y", 0]], [">", "x", 0]]
|
||||
*
|
||||
* Naively, we might convert this to the expression:
|
||||
*
|
||||
* ["any", ["all", [">", ["get", "y"], 0], [">", ["get", "z"], 0]], [">", ["get", "x"], 0]]
|
||||
*
|
||||
* But if we tried to evaluate this against, say `{x: 1, y: null, z: 0}`, the
|
||||
* [">", ["get", "y"], 0] would cause an evaluation error, leading to the
|
||||
* entire filter returning false. Legacy filter semantics, though, ask for
|
||||
* [">", "y", 0] to simply return `false` when `y` is of the wrong type,
|
||||
* allowing the subsequent terms of the outer "any" expression to be evaluated
|
||||
* (resulting, in this case, in a `true` value, because x > 0).
|
||||
*
|
||||
* We account for this by inserting a preflight type-checking expression before
|
||||
* each "any" term, allowing us to avoid evaluating the actual converted filter
|
||||
* if any type mismatches would cause it to produce an evaluation error:
|
||||
*
|
||||
* ["any",
|
||||
* ["case",
|
||||
* ["all", ["==", ["typeof", ["get", "y"]], "number"], ["==", ["typeof", ["get", "z"], "number]],
|
||||
* ["all", [">", ["get", "y"], 0], [">", ["get", "z"], 0]],
|
||||
* false
|
||||
* ],
|
||||
* ["case",
|
||||
* ["==", ["typeof", ["get", "x"], "number"]],
|
||||
* [">", ["get", "x"], 0],
|
||||
* false
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* An alternative, possibly more direct approach would be to use type checks
|
||||
* in the conversion of each comparison operator, so that the converted version
|
||||
* of each individual ==, >=, etc. would mimic the legacy filter semantics. The
|
||||
* downside of this approach is that it can lead to many more type checks than
|
||||
* would otherwise be necessary: outside the context of an "any" expression,
|
||||
* bailing out due to a runtime type error (expression semantics) and returning
|
||||
* false (legacy filter semantics) are equivalent: they cause the filter to
|
||||
* produce a `false` result.
|
||||
*/
|
||||
export function convertFilter(
|
||||
filter: FilterSpecification,
|
||||
expectedTypes: ExpectedTypes = {}
|
||||
): ExpressionFilterSpecification {
|
||||
if (isExpressionFilter(filter)) return filter;
|
||||
if (!filter) return true;
|
||||
|
||||
const legacyFilter = filter as LegacyFilterSpecification;
|
||||
const legacyOp = legacyFilter[0];
|
||||
if (filter.length <= 1) return legacyOp !== 'any';
|
||||
|
||||
switch (legacyOp) {
|
||||
case '==':
|
||||
case '!=':
|
||||
case '<':
|
||||
case '>':
|
||||
case '<=':
|
||||
case '>=': {
|
||||
const [, property, value] = filter;
|
||||
return convertComparisonOp(property as string, value, legacyOp, expectedTypes);
|
||||
}
|
||||
case 'any': {
|
||||
const [, ...conditions] = legacyFilter;
|
||||
const children = conditions.map((f: LegacyFilterSpecification) => {
|
||||
const types = {};
|
||||
const child = convertFilter(f, types);
|
||||
const typechecks = runtimeTypeChecks(types);
|
||||
return typechecks === true
|
||||
? child
|
||||
: (['case', typechecks, child, false] as ExpressionSpecification);
|
||||
});
|
||||
return ['any', ...children];
|
||||
}
|
||||
case 'all': {
|
||||
const [, ...conditions] = legacyFilter;
|
||||
const children = conditions.map((f) => convertFilter(f, expectedTypes));
|
||||
return children.length > 1 ? ['all', ...children] : children[0];
|
||||
}
|
||||
case 'none': {
|
||||
const [, ...conditions] = legacyFilter;
|
||||
return ['!', convertFilter(['any', ...conditions], {})];
|
||||
}
|
||||
case 'in': {
|
||||
const [, property, ...values] = legacyFilter;
|
||||
return convertInOp(property, values);
|
||||
}
|
||||
case '!in': {
|
||||
const [, property, ...values] = legacyFilter;
|
||||
return convertInOp(property, values, true);
|
||||
}
|
||||
case 'has':
|
||||
return convertHasOp(legacyFilter[1]);
|
||||
case '!has':
|
||||
return ['!', convertHasOp(legacyFilter[1])];
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Given a set of feature properties and an expected type for each one,
|
||||
// construct an boolean expression that tests whether each property has the
|
||||
// right type.
|
||||
// E.g.: for {name: 'string', population: 'number'}, return
|
||||
// [ 'all',
|
||||
// ['==', ['typeof', ['get', 'name'], 'string']],
|
||||
// ['==', ['typeof', ['get', 'population'], 'number]]
|
||||
// ]
|
||||
function runtimeTypeChecks(expectedTypes: ExpectedTypes): ExpressionFilterSpecification {
|
||||
const conditions = [];
|
||||
for (const property in expectedTypes) {
|
||||
const get = property === '$id' ? ['id'] : ['get', property];
|
||||
conditions.push(['==', ['typeof', get], expectedTypes[property]]);
|
||||
}
|
||||
if (conditions.length === 0) return true;
|
||||
if (conditions.length === 1) return conditions[0];
|
||||
return ['all', ...conditions];
|
||||
}
|
||||
|
||||
function convertComparisonOp(
|
||||
property: string,
|
||||
value: any,
|
||||
op: string,
|
||||
expectedTypes?: ExpectedTypes | null
|
||||
): ExpressionFilterSpecification {
|
||||
let get;
|
||||
if (property === '$type') {
|
||||
return [op, ['geometry-type'], value] as ExpressionFilterSpecification;
|
||||
} else if (property === '$id') {
|
||||
get = ['id'];
|
||||
} else {
|
||||
get = ['get', property];
|
||||
}
|
||||
|
||||
if (expectedTypes && value !== null) {
|
||||
const type = typeof value as any;
|
||||
expectedTypes[property] = type;
|
||||
}
|
||||
|
||||
if (op === '==' && property !== '$id' && value === null) {
|
||||
return [
|
||||
'all',
|
||||
['has', property], // missing property != null for legacy filters
|
||||
['==', get, null]
|
||||
];
|
||||
} else if (op === '!=' && property !== '$id' && value === null) {
|
||||
return [
|
||||
'any',
|
||||
['!', ['has', property]], // missing property != null for legacy filters
|
||||
['!=', get, null]
|
||||
];
|
||||
}
|
||||
|
||||
return [op, get, value] as ExpressionFilterSpecification;
|
||||
}
|
||||
|
||||
function convertInOp(
|
||||
property: string,
|
||||
values: Array<any>,
|
||||
negate = false
|
||||
): ExpressionFilterSpecification {
|
||||
if (values.length === 0) return negate;
|
||||
|
||||
let get: ExpressionSpecification;
|
||||
if (property === '$type') {
|
||||
get = ['geometry-type'];
|
||||
} else if (property === '$id') {
|
||||
get = ['id'];
|
||||
} else {
|
||||
get = ['get', property];
|
||||
}
|
||||
|
||||
// Determine if the list of values to be searched is homogenously typed.
|
||||
// If so (and if the type is string or number), then we can use a
|
||||
// [match, input, [...values], true, false] construction rather than a
|
||||
// bunch of `==` tests.
|
||||
let uniformTypes = true;
|
||||
const type = typeof values[0];
|
||||
for (const value of values) {
|
||||
if (typeof value !== type) {
|
||||
uniformTypes = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniformTypes && (type === 'string' || type === 'number')) {
|
||||
// Match expressions must have unique values.
|
||||
const uniqueValues = values.sort().filter((v, i) => i === 0 || values[i - 1] !== v);
|
||||
return ['match', get, uniqueValues, !negate, negate];
|
||||
}
|
||||
|
||||
if (negate) {
|
||||
return ['all', ...values.map((v) => ['!=', get, v] as ExpressionSpecification)];
|
||||
} else {
|
||||
return ['any', ...values.map((v) => ['==', get, v] as ExpressionSpecification)];
|
||||
}
|
||||
}
|
||||
|
||||
function convertHasOp(property: string): ExpressionFilterSpecification {
|
||||
if (property === '$type') {
|
||||
return true;
|
||||
} else if (property === '$id') {
|
||||
return ['!=', ['id'], null];
|
||||
} else {
|
||||
return ['has', property];
|
||||
}
|
||||
}
|
||||
661
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/feature_filter.test.ts
generated
vendored
Normal file
661
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/feature_filter.test.ts
generated
vendored
Normal file
@@ -0,0 +1,661 @@
|
||||
import {featureFilter, isExpressionFilter} from '.';
|
||||
|
||||
import {convertFilter} from './convert';
|
||||
import {ICanonicalTileID} from '../tiles_and_coordinates';
|
||||
import {FilterSpecification} from '../types.g';
|
||||
import {Feature} from '../expression';
|
||||
import {getGeometry} from '../../test/lib/geometry';
|
||||
import {describe, test, expect, vi, beforeEach} from 'vitest';
|
||||
|
||||
describe('filter', () => {
|
||||
test('expression, zoom', () => {
|
||||
const f = featureFilter(['>=', ['number', ['get', 'x']], ['zoom']]).filter;
|
||||
expect(f({zoom: 1}, {properties: {x: 0}} as any as Feature)).toBe(false);
|
||||
expect(f({zoom: 1}, {properties: {x: 1.5}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 1}, {properties: {x: 2.5}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 2}, {properties: {x: 0}} as any as Feature)).toBe(false);
|
||||
expect(f({zoom: 2}, {properties: {x: 1.5}} as any as Feature)).toBe(false);
|
||||
expect(f({zoom: 2}, {properties: {x: 2.5}} as any as Feature)).toBe(true);
|
||||
});
|
||||
|
||||
test('expression, compare two properties', () => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const f = featureFilter(['==', ['string', ['get', 'x']], ['string', ['get', 'y']]]).filter;
|
||||
expect(f({zoom: 0}, {properties: {x: 1, y: 1}} as any as Feature)).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {x: '1', y: '1'}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {x: 'same', y: 'same'}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {x: null}} as any as Feature)).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {x: undefined}} as any as Feature)).toBe(false);
|
||||
});
|
||||
|
||||
test('expression, collator comparison', () => {
|
||||
const caseSensitive = featureFilter([
|
||||
'==',
|
||||
['string', ['get', 'x']],
|
||||
['string', ['get', 'y']],
|
||||
['collator', {'case-sensitive': true}]
|
||||
]).filter;
|
||||
expect(caseSensitive({zoom: 0}, {properties: {x: 'a', y: 'b'}} as any as Feature)).toBe(
|
||||
false
|
||||
);
|
||||
expect(caseSensitive({zoom: 0}, {properties: {x: 'a', y: 'A'}} as any as Feature)).toBe(
|
||||
false
|
||||
);
|
||||
expect(caseSensitive({zoom: 0}, {properties: {x: 'a', y: 'a'}} as any as Feature)).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
const caseInsensitive = featureFilter([
|
||||
'==',
|
||||
['string', ['get', 'x']],
|
||||
['string', ['get', 'y']],
|
||||
['collator', {'case-sensitive': false}]
|
||||
]).filter;
|
||||
expect(caseInsensitive({zoom: 0}, {properties: {x: 'a', y: 'b'}} as any as Feature)).toBe(
|
||||
false
|
||||
);
|
||||
expect(caseInsensitive({zoom: 0}, {properties: {x: 'a', y: 'A'}} as any as Feature)).toBe(
|
||||
true
|
||||
);
|
||||
expect(caseInsensitive({zoom: 0}, {properties: {x: 'a', y: 'a'}} as any as Feature)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('expression, any/all', () => {
|
||||
expect(featureFilter(['all']).filter(undefined, undefined)).toBe(true);
|
||||
expect(featureFilter(['all', true]).filter(undefined, undefined)).toBe(true);
|
||||
expect(featureFilter(['all', true, false]).filter(undefined, undefined)).toBe(false);
|
||||
expect(featureFilter(['all', true, true]).filter(undefined, undefined)).toBe(true);
|
||||
expect(featureFilter(['any']).filter(undefined, undefined)).toBe(false);
|
||||
expect(featureFilter(['any', true]).filter(undefined, undefined)).toBe(true);
|
||||
expect(featureFilter(['any', true, false]).filter(undefined, undefined)).toBe(true);
|
||||
expect(featureFilter(['any', false, false]).filter(undefined, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
test('expression, literal', () => {
|
||||
expect(featureFilter(['literal', true]).filter(undefined, undefined)).toBe(true);
|
||||
expect(featureFilter(['literal', false]).filter(undefined, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
test('expression, match', () => {
|
||||
const match = featureFilter(['match', ['get', 'x'], ['a', 'b', 'c'], true, false]).filter;
|
||||
expect(match(undefined, {properties: {x: 'a'}} as any as Feature)).toBe(true);
|
||||
expect(match(undefined, {properties: {x: 'c'}} as any as Feature)).toBe(true);
|
||||
expect(match(undefined, {properties: {x: 'd'}} as any as Feature)).toBe(false);
|
||||
});
|
||||
|
||||
test('expression, type error', () => {
|
||||
expect(() => {
|
||||
featureFilter(['==', ['number', ['get', 'x']], ['string', ['get', 'y']]]);
|
||||
}).toThrowError(": Cannot compare types 'number' and 'string'.");
|
||||
|
||||
expect(() => {
|
||||
featureFilter(['number', ['get', 'x']]);
|
||||
}).toThrowError(': Expected boolean but found number instead.');
|
||||
|
||||
expect(() => {
|
||||
featureFilter(['boolean', ['get', 'x']]);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
test('expression, within', () => {
|
||||
const withinFilter = featureFilter([
|
||||
'within',
|
||||
{
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
[
|
||||
[0, 0],
|
||||
[5, 0],
|
||||
[5, 5],
|
||||
[0, 5],
|
||||
[0, 0]
|
||||
]
|
||||
]
|
||||
}
|
||||
]);
|
||||
expect(withinFilter.needGeometry).toBe(true);
|
||||
const canonical = {z: 3, x: 3, y: 3} as ICanonicalTileID;
|
||||
const featureInTile = {} as Feature;
|
||||
getGeometry(featureInTile, {type: 'Point', coordinates: [2, 2]}, canonical);
|
||||
expect(withinFilter.filter({zoom: 3}, featureInTile, canonical)).toBe(true);
|
||||
getGeometry(featureInTile, {type: 'Point', coordinates: [6, 6]}, canonical);
|
||||
expect(withinFilter.filter({zoom: 3}, featureInTile, canonical)).toBe(false);
|
||||
getGeometry(featureInTile, {type: 'Point', coordinates: [5, 5]}, canonical);
|
||||
expect(withinFilter.filter({zoom: 3}, featureInTile, canonical)).toBe(false);
|
||||
getGeometry(
|
||||
featureInTile,
|
||||
{
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[2, 2],
|
||||
[3, 3]
|
||||
]
|
||||
},
|
||||
canonical
|
||||
);
|
||||
expect(withinFilter.filter({zoom: 3}, featureInTile, canonical)).toBe(true);
|
||||
getGeometry(
|
||||
featureInTile,
|
||||
{
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[6, 6],
|
||||
[2, 2]
|
||||
]
|
||||
},
|
||||
canonical
|
||||
);
|
||||
expect(withinFilter.filter({zoom: 3}, featureInTile, canonical)).toBe(false);
|
||||
getGeometry(
|
||||
featureInTile,
|
||||
{
|
||||
type: 'LineString',
|
||||
coordinates: [
|
||||
[5, 5],
|
||||
[2, 2]
|
||||
]
|
||||
},
|
||||
canonical
|
||||
);
|
||||
expect(withinFilter.filter({zoom: 3}, featureInTile, canonical)).toBe(false);
|
||||
});
|
||||
|
||||
test('expression, global-state', () => {
|
||||
const {filter} = featureFilter(['==', ['global-state', 'x'], ['get', 'x']], {x: 1});
|
||||
expect(filter(undefined, {properties: {x: 1}} as any as Feature)).toBe(true);
|
||||
expect(filter(undefined, {properties: {x: 2}} as any as Feature)).toBe(false);
|
||||
});
|
||||
|
||||
legacyFilterTests(featureFilter);
|
||||
});
|
||||
|
||||
describe('getGlobalStateRefs', () => {
|
||||
test('returns global-state keys', () => {
|
||||
const filter = featureFilter(['==', ['global-state', 'x'], ['zoom']]);
|
||||
expect(filter.getGlobalStateRefs()).toEqual(new Set(['x']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy filter detection', () => {
|
||||
test('definitely legacy filters', () => {
|
||||
// Expressions with more than two arguments.
|
||||
expect(isExpressionFilter(['in', 'color', 'red', 'blue'])).toBeFalsy();
|
||||
|
||||
// Expressions where the second argument is not a string or array.
|
||||
expect(isExpressionFilter(['in', 'value', 42])).toBeFalsy();
|
||||
expect(isExpressionFilter(['in', 'value', true])).toBeFalsy();
|
||||
});
|
||||
|
||||
test('ambiguous value', () => {
|
||||
// Should err on the side of reporting as a legacy filter. Style authors can force filters
|
||||
// by using a literal expression as the first argument.
|
||||
expect(isExpressionFilter(['in', 'color', 'red'])).toBeFalsy();
|
||||
});
|
||||
|
||||
test('definitely expressions', () => {
|
||||
expect(isExpressionFilter(['in', ['get', 'color'], 'reddish'])).toBeTruthy();
|
||||
expect(isExpressionFilter(['in', ['get', 'color'], ['red', 'blue']])).toBeTruthy();
|
||||
expect(isExpressionFilter(['in', 42, 42])).toBeTruthy();
|
||||
expect(isExpressionFilter(['in', true, true])).toBeTruthy();
|
||||
expect(isExpressionFilter(['in', 'red', ['get', 'colors']])).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert legacy filters to expressions', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
legacyFilterTests((f) => {
|
||||
const converted = convertFilter(f);
|
||||
return featureFilter(converted);
|
||||
});
|
||||
|
||||
test('mimic legacy type mismatch semantics', () => {
|
||||
const filter = [
|
||||
'any',
|
||||
['all', ['>', 'y', 0], ['>', 'y', 0]],
|
||||
['>', 'x', 0]
|
||||
] as FilterSpecification;
|
||||
|
||||
const converted = convertFilter(filter);
|
||||
const f = featureFilter(converted).filter;
|
||||
|
||||
expect(f({zoom: 0}, {properties: {x: 0, y: 1, z: 1}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {x: 1, y: 0, z: 1}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {x: 0, y: 0, z: 1}} as any as Feature)).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {x: null, y: 1, z: 1}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {x: 1, y: null, z: 1}} as any as Feature)).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {x: null, y: null, z: 1}} as any as Feature)).toBe(false);
|
||||
});
|
||||
|
||||
test('flattens nested, single child all expressions', () => {
|
||||
const filter: FilterSpecification = [
|
||||
'all',
|
||||
['in', '$type', 'Polygon', 'LineString', 'Point'],
|
||||
['all', ['in', 'type', 'island']]
|
||||
];
|
||||
|
||||
const expected: FilterSpecification = [
|
||||
'all',
|
||||
['match', ['geometry-type'], ['LineString', 'Point', 'Polygon'], true, false],
|
||||
['match', ['get', 'type'], ['island'], true, false]
|
||||
];
|
||||
|
||||
const converted = convertFilter(filter);
|
||||
expect(converted).toEqual(expected);
|
||||
});
|
||||
|
||||
test('removes duplicates when outputting match expressions', () => {
|
||||
const filter = ['in', '$id', 1, 2, 3, 2, 1] as FilterSpecification;
|
||||
|
||||
const expected = ['match', ['id'], [1, 2, 3], true, false];
|
||||
|
||||
const converted = convertFilter(filter);
|
||||
expect(converted).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
function legacyFilterTests(createFilterExpr) {
|
||||
test('degenerate', () => {
|
||||
expect(createFilterExpr().filter()).toBe(true);
|
||||
expect(createFilterExpr(undefined).filter()).toBe(true);
|
||||
expect(createFilterExpr(null).filter()).toBe(true);
|
||||
});
|
||||
|
||||
test('==, string', () => {
|
||||
const f = createFilterExpr(['==', 'foo', 'bar']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 'bar'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 'baz'}})).toBe(false);
|
||||
});
|
||||
|
||||
test('==, number', () => {
|
||||
const f = createFilterExpr(['==', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('==, null', () => {
|
||||
const f = createFilterExpr(['==', 'foo', null]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(true);
|
||||
// t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('==, $type', () => {
|
||||
const f = createFilterExpr(['==', '$type', 'LineString']).filter;
|
||||
expect(f({zoom: 0}, {type: 1})).toBe(false);
|
||||
expect(f({zoom: 0}, {type: 2})).toBe(true);
|
||||
});
|
||||
|
||||
test('==, $id', () => {
|
||||
const f = createFilterExpr(['==', '$id', 1234]).filter;
|
||||
|
||||
expect(f({zoom: 0}, {id: 1234})).toBe(true);
|
||||
expect(f({zoom: 0}, {id: '1234'})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {id: 1234}})).toBe(false);
|
||||
});
|
||||
|
||||
test('!=, string', () => {
|
||||
const f = createFilterExpr(['!=', 'foo', 'bar']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 'bar'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 'baz'}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!=, number', () => {
|
||||
const f = createFilterExpr(['!=', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!=, null', () => {
|
||||
const f = createFilterExpr(['!=', 'foo', null]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
// t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!=, $type', () => {
|
||||
const f = createFilterExpr(['!=', '$type', 'LineString']).filter;
|
||||
expect(f({zoom: 0}, {type: 1})).toBe(true);
|
||||
expect(f({zoom: 0}, {type: 2})).toBe(false);
|
||||
});
|
||||
|
||||
test('<, number', () => {
|
||||
const f = createFilterExpr(['<', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('<, string', () => {
|
||||
const f = createFilterExpr(['<', 'foo', '0']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
});
|
||||
|
||||
test('<=, number', () => {
|
||||
const f = createFilterExpr(['<=', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('<=, string', () => {
|
||||
const f = createFilterExpr(['<=', 'foo', '0']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
});
|
||||
|
||||
test('>, number', () => {
|
||||
const f = createFilterExpr(['>', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('>, string', () => {
|
||||
const f = createFilterExpr(['>', 'foo', '0']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
});
|
||||
|
||||
test('>=, number', () => {
|
||||
const f = createFilterExpr(['>=', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('>=, string', () => {
|
||||
const f = createFilterExpr(['>=', 'foo', '0']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: -1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '1'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '-1'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, degenerate', () => {
|
||||
const f = createFilterExpr(['in', 'foo']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, string', () => {
|
||||
const f = createFilterExpr(['in', 'foo', '0']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, number', () => {
|
||||
const f = createFilterExpr(['in', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, null', () => {
|
||||
const f = createFilterExpr(['in', 'foo', null]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(true);
|
||||
// t.equal(f({zoom: 0}, {properties: {foo: undefined}}), false);
|
||||
});
|
||||
|
||||
test('in, multiple', () => {
|
||||
const f = createFilterExpr(['in', 'foo', 0, 1]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 3}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, large_multiple', () => {
|
||||
const values = Array.from({length: 2000}).map(Number.call, Number);
|
||||
values.reverse();
|
||||
const f = createFilterExpr(['in', 'foo'].concat(values)).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1999}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 2000}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, large_multiple, heterogeneous', () => {
|
||||
const values = Array.from({length: 2000}).map(Number.call, Number);
|
||||
values.push('a');
|
||||
values.unshift('b');
|
||||
const f = createFilterExpr(['in', 'foo'].concat(values)).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 'b'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 'a'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1999}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 2000}})).toBe(false);
|
||||
});
|
||||
|
||||
test('in, $type', () => {
|
||||
const f = createFilterExpr(['in', '$type', 'LineString', 'Polygon']).filter;
|
||||
expect(f({zoom: 0}, {type: 1})).toBe(false);
|
||||
expect(f({zoom: 0}, {type: 2})).toBe(true);
|
||||
expect(f({zoom: 0}, {type: 3})).toBe(true);
|
||||
|
||||
const f1 = createFilterExpr(['in', '$type', 'Polygon', 'LineString', 'Point']).filter;
|
||||
expect(f1({zoom: 0}, {type: 1})).toBe(true);
|
||||
expect(f1({zoom: 0}, {type: 2})).toBe(true);
|
||||
expect(f1({zoom: 0}, {type: 3})).toBe(true);
|
||||
});
|
||||
|
||||
test('!in, degenerate', () => {
|
||||
const f = createFilterExpr(['!in', 'foo']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!in, string', () => {
|
||||
const f = createFilterExpr(['!in', 'foo', '0']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!in, number', () => {
|
||||
const f = createFilterExpr(['!in', 'foo', 0]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!in, null', () => {
|
||||
const f = createFilterExpr(['!in', 'foo', null]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
// t.equal(f({zoom: 0}, {properties: {foo: undefined}}), true);
|
||||
});
|
||||
|
||||
test('!in, multiple', () => {
|
||||
const f = createFilterExpr(['!in', 'foo', 0, 1]).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 3}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!in, large_multiple', () => {
|
||||
const f = createFilterExpr(
|
||||
['!in', 'foo'].concat(Array.from({length: 2000}).map(Number.call, Number))
|
||||
).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1999}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 2000}})).toBe(true);
|
||||
});
|
||||
|
||||
test('!in, $type', () => {
|
||||
const f = createFilterExpr(['!in', '$type', 'LineString', 'Polygon']).filter;
|
||||
expect(f({zoom: 0}, {type: 1})).toBe(true);
|
||||
expect(f({zoom: 0}, {type: 2})).toBe(false);
|
||||
expect(f({zoom: 0}, {type: 3})).toBe(false);
|
||||
});
|
||||
|
||||
test('any', () => {
|
||||
const f1 = createFilterExpr(['any']).filter;
|
||||
expect(f1({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
|
||||
const f2 = createFilterExpr(['any', ['==', 'foo', 1]]).filter;
|
||||
expect(f2({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
|
||||
const f3 = createFilterExpr(['any', ['==', 'foo', 0]]).filter;
|
||||
expect(f3({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
|
||||
const f4 = createFilterExpr(['any', ['==', 'foo', 0], ['==', 'foo', 1]]).filter;
|
||||
expect(f4({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
});
|
||||
|
||||
test('all', () => {
|
||||
const f1 = createFilterExpr(['all']).filter;
|
||||
expect(f1({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
|
||||
const f2 = createFilterExpr(['all', ['==', 'foo', 1]]).filter;
|
||||
expect(f2({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
|
||||
const f3 = createFilterExpr(['all', ['==', 'foo', 0]]).filter;
|
||||
expect(f3({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
|
||||
const f4 = createFilterExpr(['all', ['==', 'foo', 0], ['==', 'foo', 1]]).filter;
|
||||
expect(f4({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
});
|
||||
|
||||
test('none', () => {
|
||||
const f1 = createFilterExpr(['none']).filter;
|
||||
expect(f1({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
|
||||
const f2 = createFilterExpr(['none', ['==', 'foo', 1]]).filter;
|
||||
expect(f2({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
|
||||
const f3 = createFilterExpr(['none', ['==', 'foo', 0]]).filter;
|
||||
expect(f3({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
|
||||
const f4 = createFilterExpr(['none', ['==', 'foo', 0], ['==', 'foo', 1]]).filter;
|
||||
expect(f4({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
});
|
||||
|
||||
test('has', () => {
|
||||
const f = createFilterExpr(['has', 'foo']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: true}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(true);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(false);
|
||||
});
|
||||
|
||||
test('!has', () => {
|
||||
const f = createFilterExpr(['!has', 'foo']).filter;
|
||||
expect(f({zoom: 0}, {properties: {foo: 0}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: 1}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: '0'}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: false}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: null}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {foo: undefined}})).toBe(false);
|
||||
expect(f({zoom: 0}, {properties: {}})).toBe(true);
|
||||
});
|
||||
}
|
||||
208
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/index.ts
generated
vendored
Normal file
208
node_modules/@maplibre/maplibre-gl-style-spec/src/feature_filter/index.ts
generated
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
import {createExpression, findGlobalStateRefs} from '../expression';
|
||||
import type {GlobalProperties, Feature} from '../expression';
|
||||
import {ICanonicalTileID} from '../tiles_and_coordinates';
|
||||
import {StylePropertySpecification} from '..';
|
||||
import {ExpressionFilterSpecification, type FilterSpecification} from '../types.g';
|
||||
|
||||
type FilterExpression = (
|
||||
globalProperties: GlobalProperties,
|
||||
feature: Feature,
|
||||
canonical?: ICanonicalTileID
|
||||
) => boolean;
|
||||
|
||||
export type FeatureFilter = {
|
||||
filter: FilterExpression;
|
||||
needGeometry: boolean;
|
||||
getGlobalStateRefs: () => Set<string>;
|
||||
};
|
||||
|
||||
export function isExpressionFilter(filter: any): filter is ExpressionFilterSpecification {
|
||||
if (filter === true || filter === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(filter) || filter.length === 0) {
|
||||
return false;
|
||||
}
|
||||
switch (filter[0]) {
|
||||
case 'has':
|
||||
return filter.length >= 2 && filter[1] !== '$id' && filter[1] !== '$type';
|
||||
|
||||
case 'in':
|
||||
return (
|
||||
filter.length >= 3 && (typeof filter[1] !== 'string' || Array.isArray(filter[2]))
|
||||
);
|
||||
|
||||
case '!in':
|
||||
case '!has':
|
||||
case 'none':
|
||||
return false;
|
||||
|
||||
case '==':
|
||||
case '!=':
|
||||
case '>':
|
||||
case '>=':
|
||||
case '<':
|
||||
case '<=':
|
||||
return filter.length !== 3 || Array.isArray(filter[1]) || Array.isArray(filter[2]);
|
||||
|
||||
case 'any':
|
||||
case 'all':
|
||||
for (const f of filter.slice(1)) {
|
||||
if (!isExpressionFilter(f) && typeof f !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const filterSpec = {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
transition: false,
|
||||
'property-type': 'data-driven',
|
||||
expression: {
|
||||
interpolated: false,
|
||||
parameters: ['zoom', 'feature']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a filter expressed as nested arrays, return a new function
|
||||
* that evaluates whether a given feature (with a .properties or .tags property)
|
||||
* passes its test.
|
||||
*
|
||||
* @private
|
||||
* @param filter MapLibre filter
|
||||
* @param [globalState] Global state object to be used for evaluating 'global-state' expressions
|
||||
* @returns filter-evaluating function
|
||||
*/
|
||||
export function featureFilter(
|
||||
filter: FilterSpecification | void,
|
||||
globalState?: Record<string, any>
|
||||
): FeatureFilter {
|
||||
if (filter === null || filter === undefined) {
|
||||
return {filter: () => true, needGeometry: false, getGlobalStateRefs: () => new Set()};
|
||||
}
|
||||
|
||||
if (!isExpressionFilter(filter)) {
|
||||
filter = convertFilter(filter) as ExpressionFilterSpecification;
|
||||
}
|
||||
|
||||
const compiled = createExpression(
|
||||
filter,
|
||||
filterSpec as StylePropertySpecification,
|
||||
globalState
|
||||
);
|
||||
if (compiled.result === 'error') {
|
||||
throw new Error(compiled.value.map((err) => `${err.key}: ${err.message}`).join(', '));
|
||||
} else {
|
||||
const needGeometry = geometryNeeded(filter);
|
||||
return {
|
||||
filter: (
|
||||
globalProperties: GlobalProperties,
|
||||
feature: Feature,
|
||||
canonical?: ICanonicalTileID
|
||||
) => compiled.value.evaluate(globalProperties, feature, {}, canonical),
|
||||
needGeometry,
|
||||
getGlobalStateRefs: () => findGlobalStateRefs(compiled.value.expression)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Comparison function to sort numbers and strings
|
||||
function compare(a, b) {
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
|
||||
function geometryNeeded(filter) {
|
||||
if (!Array.isArray(filter)) return false;
|
||||
if (filter[0] === 'within' || filter[0] === 'distance') return true;
|
||||
for (let index = 1; index < filter.length; index++) {
|
||||
if (geometryNeeded(filter[index])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function convertFilter(filter?: Array<any> | null | void): unknown {
|
||||
if (!filter) return true;
|
||||
const op = filter[0];
|
||||
if (filter.length <= 1) return op !== 'any';
|
||||
const converted =
|
||||
op === '=='
|
||||
? convertComparisonOp(filter[1], filter[2], '==')
|
||||
: op === '!='
|
||||
? convertNegation(convertComparisonOp(filter[1], filter[2], '=='))
|
||||
: op === '<' || op === '>' || op === '<=' || op === '>='
|
||||
? convertComparisonOp(filter[1], filter[2], op)
|
||||
: op === 'any'
|
||||
? convertDisjunctionOp(filter.slice(1))
|
||||
: op === 'all'
|
||||
? ['all' as unknown].concat(filter.slice(1).map(convertFilter))
|
||||
: op === 'none'
|
||||
? ['all' as unknown].concat(
|
||||
filter.slice(1).map(convertFilter).map(convertNegation)
|
||||
)
|
||||
: op === 'in'
|
||||
? convertInOp(filter[1], filter.slice(2))
|
||||
: op === '!in'
|
||||
? convertNegation(convertInOp(filter[1], filter.slice(2)))
|
||||
: op === 'has'
|
||||
? convertHasOp(filter[1])
|
||||
: op === '!has'
|
||||
? convertNegation(convertHasOp(filter[1]))
|
||||
: true;
|
||||
return converted;
|
||||
}
|
||||
|
||||
function convertComparisonOp(property: string, value: any, op: string) {
|
||||
switch (property) {
|
||||
case '$type':
|
||||
return [`filter-type-${op}`, value];
|
||||
case '$id':
|
||||
return [`filter-id-${op}`, value];
|
||||
default:
|
||||
return [`filter-${op}`, property, value];
|
||||
}
|
||||
}
|
||||
|
||||
function convertDisjunctionOp(filters: Array<Array<any>>) {
|
||||
return ['any' as unknown].concat(filters.map(convertFilter));
|
||||
}
|
||||
|
||||
function convertInOp(property: string, values: Array<any>) {
|
||||
if (values.length === 0) {
|
||||
return false;
|
||||
}
|
||||
switch (property) {
|
||||
case '$type':
|
||||
return ['filter-type-in', ['literal', values]];
|
||||
case '$id':
|
||||
return ['filter-id-in', ['literal', values]];
|
||||
default:
|
||||
if (values.length > 200 && !values.some((v) => typeof v !== typeof values[0])) {
|
||||
return ['filter-in-large', property, ['literal', values.sort(compare)]];
|
||||
} else {
|
||||
return ['filter-in-small', property, ['literal', values]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertHasOp(property: string) {
|
||||
switch (property) {
|
||||
case '$type':
|
||||
return true;
|
||||
case '$id':
|
||||
return ['filter-has-id'];
|
||||
default:
|
||||
return ['filter-has', property];
|
||||
}
|
||||
}
|
||||
|
||||
function convertNegation(filter: unknown) {
|
||||
return ['!', filter];
|
||||
}
|
||||
39
node_modules/@maplibre/maplibre-gl-style-spec/src/format.test.ts
generated
vendored
Normal file
39
node_modules/@maplibre/maplibre-gl-style-spec/src/format.test.ts
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import {format} from './format';
|
||||
import {describe, test, expect} from 'vitest';
|
||||
function roundtrip(style) {
|
||||
return JSON.parse(format(style));
|
||||
}
|
||||
|
||||
describe('format', () => {
|
||||
test('orders top-level keys', () => {
|
||||
expect(
|
||||
Object.keys(
|
||||
roundtrip({
|
||||
layers: [],
|
||||
other: {},
|
||||
sources: {},
|
||||
glyphs: '',
|
||||
sprite: '',
|
||||
version: 6
|
||||
})
|
||||
)
|
||||
).toEqual(['version', 'sources', 'sprite', 'glyphs', 'layers', 'other']);
|
||||
});
|
||||
|
||||
test('orders layer keys', () => {
|
||||
expect(
|
||||
Object.keys(
|
||||
roundtrip({
|
||||
layers: [
|
||||
{
|
||||
paint: {},
|
||||
layout: {},
|
||||
id: 'id',
|
||||
type: 'type'
|
||||
}
|
||||
]
|
||||
}).layers[0]
|
||||
)
|
||||
).toEqual(['id', 'type', 'layout', 'paint']);
|
||||
});
|
||||
});
|
||||
48
node_modules/@maplibre/maplibre-gl-style-spec/src/format.ts
generated
vendored
Normal file
48
node_modules/@maplibre/maplibre-gl-style-spec/src/format.ts
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import {latest} from './reference/latest';
|
||||
import stringifyPretty from 'json-stringify-pretty-compact';
|
||||
|
||||
function sortKeysBy(obj, reference) {
|
||||
const result = {};
|
||||
for (const key in reference) {
|
||||
if (obj[key] !== undefined) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (result[key] === undefined) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a MapLibre Style. Returns a stringified style with its keys
|
||||
* sorted in the same order as the reference style.
|
||||
*
|
||||
* The optional `space` argument is passed to
|
||||
* [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
|
||||
* to generate formatted output.
|
||||
*
|
||||
* If `space` is unspecified, a default of `2` spaces will be used.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} style a MapLibre Style
|
||||
* @param {number} [space] space argument to pass to `JSON.stringify`
|
||||
* @returns {string} stringified formatted JSON
|
||||
* @example
|
||||
* var fs = require('fs');
|
||||
* var format = require('maplibre-gl-style-spec').format;
|
||||
* var style = fs.readFileSync('./source.json', 'utf8');
|
||||
* fs.writeFileSync('./dest.json', format(style));
|
||||
* fs.writeFileSync('./dest.min.json', format(style, 0));
|
||||
*/
|
||||
export function format(style, space = 2) {
|
||||
style = sortKeysBy(style, latest.$root);
|
||||
|
||||
if (style.layers) {
|
||||
style.layers = style.layers.map((layer) => sortKeysBy(layer, latest.layer));
|
||||
}
|
||||
|
||||
return stringifyPretty(style, {indent: space});
|
||||
}
|
||||
278
node_modules/@maplibre/maplibre-gl-style-spec/src/function/convert.ts
generated
vendored
Normal file
278
node_modules/@maplibre/maplibre-gl-style-spec/src/function/convert.ts
generated
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
import type {StylePropertySpecification} from '..';
|
||||
|
||||
function convertLiteral(value) {
|
||||
return typeof value === 'object' ? ['literal', value] : value;
|
||||
}
|
||||
|
||||
export function convertFunction(parameters: any, propertySpec: StylePropertySpecification) {
|
||||
let stops = parameters.stops;
|
||||
if (!stops) {
|
||||
// identity function
|
||||
return convertIdentityFunction(parameters, propertySpec);
|
||||
}
|
||||
|
||||
const zoomAndFeatureDependent = stops && typeof stops[0][0] === 'object';
|
||||
const featureDependent = zoomAndFeatureDependent || parameters.property !== undefined;
|
||||
const zoomDependent = zoomAndFeatureDependent || !featureDependent;
|
||||
|
||||
stops = stops.map((stop) => {
|
||||
if (!featureDependent && (propertySpec as any).tokens && typeof stop[1] === 'string') {
|
||||
return [stop[0], convertTokenString(stop[1])];
|
||||
}
|
||||
return [stop[0], convertLiteral(stop[1])];
|
||||
});
|
||||
|
||||
if (zoomAndFeatureDependent) {
|
||||
return convertZoomAndPropertyFunction(parameters, propertySpec, stops);
|
||||
} else if (zoomDependent) {
|
||||
return convertZoomFunction(parameters, propertySpec, stops);
|
||||
} else {
|
||||
return convertPropertyFunction(parameters, propertySpec, stops);
|
||||
}
|
||||
}
|
||||
|
||||
function convertIdentityFunction(parameters, propertySpec): Array<unknown> {
|
||||
const get = ['get', parameters.property];
|
||||
|
||||
if (parameters.default === undefined) {
|
||||
// By default, expressions for string-valued properties get coerced. To preserve
|
||||
// legacy function semantics, insert an explicit assertion instead.
|
||||
return propertySpec.type === 'string' ? ['string', get] : get;
|
||||
} else if (propertySpec.type === 'enum') {
|
||||
return ['match', get, Object.keys(propertySpec.values), get, parameters.default];
|
||||
} else {
|
||||
const expression = [
|
||||
propertySpec.type === 'color' ? 'to-color' : propertySpec.type,
|
||||
get,
|
||||
convertLiteral(parameters.default)
|
||||
];
|
||||
if (propertySpec.type === 'array') {
|
||||
expression.splice(1, 0, propertySpec.value, propertySpec.length || null);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
function getInterpolateOperator(parameters) {
|
||||
switch (parameters.colorSpace) {
|
||||
case 'hcl':
|
||||
return 'interpolate-hcl';
|
||||
case 'lab':
|
||||
return 'interpolate-lab';
|
||||
default:
|
||||
return 'interpolate';
|
||||
}
|
||||
}
|
||||
|
||||
function convertZoomAndPropertyFunction(parameters, propertySpec, stops) {
|
||||
const featureFunctionParameters = {};
|
||||
const featureFunctionStops = {};
|
||||
const zoomStops = [];
|
||||
for (let s = 0; s < stops.length; s++) {
|
||||
const stop = stops[s];
|
||||
const zoom = stop[0].zoom;
|
||||
if (featureFunctionParameters[zoom] === undefined) {
|
||||
featureFunctionParameters[zoom] = {
|
||||
zoom,
|
||||
type: parameters.type,
|
||||
property: parameters.property,
|
||||
default: parameters.default
|
||||
};
|
||||
featureFunctionStops[zoom] = [];
|
||||
zoomStops.push(zoom);
|
||||
}
|
||||
featureFunctionStops[zoom].push([stop[0].value, stop[1]]);
|
||||
}
|
||||
|
||||
// the interpolation type for the zoom dimension of a zoom-and-property
|
||||
// function is determined directly from the style property specification
|
||||
// for which it's being used: linear for interpolatable properties, step
|
||||
// otherwise.
|
||||
const functionType = getFunctionType({}, propertySpec);
|
||||
if (functionType === 'exponential') {
|
||||
const expression = [getInterpolateOperator(parameters), ['linear'], ['zoom']];
|
||||
|
||||
for (const z of zoomStops) {
|
||||
const output = convertPropertyFunction(
|
||||
featureFunctionParameters[z],
|
||||
propertySpec,
|
||||
featureFunctionStops[z]
|
||||
);
|
||||
appendStopPair(expression, z, output, false);
|
||||
}
|
||||
|
||||
return expression;
|
||||
} else {
|
||||
const expression = ['step', ['zoom']];
|
||||
|
||||
for (const z of zoomStops) {
|
||||
const output = convertPropertyFunction(
|
||||
featureFunctionParameters[z],
|
||||
propertySpec,
|
||||
featureFunctionStops[z]
|
||||
);
|
||||
appendStopPair(expression, z, output, true);
|
||||
}
|
||||
|
||||
fixupDegenerateStepCurve(expression);
|
||||
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
function coalesce(a, b) {
|
||||
if (a !== undefined) return a;
|
||||
if (b !== undefined) return b;
|
||||
}
|
||||
|
||||
function getFallback(parameters, propertySpec) {
|
||||
const defaultValue = convertLiteral(coalesce(parameters.default, propertySpec.default));
|
||||
|
||||
/*
|
||||
* Some fields with type: resolvedImage have an undefined default.
|
||||
* Because undefined is an invalid value for resolvedImage, set fallback to
|
||||
* an empty string instead of undefined to ensure output
|
||||
* passes validation.
|
||||
*/
|
||||
if (defaultValue === undefined && propertySpec.type === 'resolvedImage') {
|
||||
return '';
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function convertPropertyFunction(parameters, propertySpec, stops) {
|
||||
const type = getFunctionType(parameters, propertySpec);
|
||||
const get = ['get', parameters.property];
|
||||
if (type === 'categorical' && typeof stops[0][0] === 'boolean') {
|
||||
const expression: any = ['case'];
|
||||
for (const stop of stops) {
|
||||
expression.push(['==', get, stop[0]], stop[1]);
|
||||
}
|
||||
|
||||
expression.push(getFallback(parameters, propertySpec));
|
||||
return expression;
|
||||
} else if (type === 'categorical') {
|
||||
const expression = ['match', get];
|
||||
for (const stop of stops) {
|
||||
appendStopPair(expression, stop[0], stop[1], false);
|
||||
}
|
||||
expression.push(getFallback(parameters, propertySpec));
|
||||
return expression;
|
||||
} else if (type === 'interval') {
|
||||
const expression = ['step', ['number', get]];
|
||||
for (const stop of stops) {
|
||||
appendStopPair(expression, stop[0], stop[1], true);
|
||||
}
|
||||
fixupDegenerateStepCurve(expression);
|
||||
return parameters.default === undefined
|
||||
? expression
|
||||
: [
|
||||
'case',
|
||||
['==', ['typeof', get], 'number'],
|
||||
expression,
|
||||
convertLiteral(parameters.default)
|
||||
];
|
||||
} else if (type === 'exponential') {
|
||||
const base = parameters.base !== undefined ? parameters.base : 1;
|
||||
const expression = [
|
||||
getInterpolateOperator(parameters),
|
||||
base === 1 ? ['linear'] : ['exponential', base],
|
||||
['number', get]
|
||||
];
|
||||
|
||||
for (const stop of stops) {
|
||||
appendStopPair(expression, stop[0], stop[1], false);
|
||||
}
|
||||
return parameters.default === undefined
|
||||
? expression
|
||||
: [
|
||||
'case',
|
||||
['==', ['typeof', get], 'number'],
|
||||
expression,
|
||||
convertLiteral(parameters.default)
|
||||
];
|
||||
} else {
|
||||
throw new Error(`Unknown property function type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function convertZoomFunction(parameters, propertySpec, stops, input = ['zoom']) {
|
||||
const type = getFunctionType(parameters, propertySpec);
|
||||
let expression;
|
||||
let isStep = false;
|
||||
if (type === 'interval') {
|
||||
expression = ['step', input];
|
||||
isStep = true;
|
||||
} else if (type === 'exponential') {
|
||||
const base = parameters.base !== undefined ? parameters.base : 1;
|
||||
expression = [
|
||||
getInterpolateOperator(parameters),
|
||||
base === 1 ? ['linear'] : ['exponential', base],
|
||||
input
|
||||
];
|
||||
} else {
|
||||
throw new Error(`Unknown zoom function type "${type}"`);
|
||||
}
|
||||
|
||||
for (const stop of stops) {
|
||||
appendStopPair(expression, stop[0], stop[1], isStep);
|
||||
}
|
||||
|
||||
fixupDegenerateStepCurve(expression);
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
function fixupDegenerateStepCurve(expression) {
|
||||
// degenerate step curve (i.e. a constant function): add a noop stop
|
||||
if (expression[0] === 'step' && expression.length === 3) {
|
||||
expression.push(0);
|
||||
expression.push(expression[3]);
|
||||
}
|
||||
}
|
||||
|
||||
function appendStopPair(curve, input, output, isStep) {
|
||||
// Skip duplicate stop values. They were not validated for functions, but they are for expressions.
|
||||
// https://github.com/mapbox/mapbox-gl-js/issues/4107
|
||||
if (curve.length > 3 && input === curve[curve.length - 2]) {
|
||||
return;
|
||||
}
|
||||
// step curves don't get the first input value, as it is redundant.
|
||||
if (!(isStep && curve.length === 2)) {
|
||||
curve.push(input);
|
||||
}
|
||||
curve.push(output);
|
||||
}
|
||||
|
||||
function getFunctionType(parameters, propertySpec) {
|
||||
if (parameters.type) {
|
||||
return parameters.type;
|
||||
} else {
|
||||
return (propertySpec.expression as any).interpolated ? 'exponential' : 'interval';
|
||||
}
|
||||
}
|
||||
|
||||
// "String with {name} token" => ["concat", "String with ", ["get", "name"], " token"]
|
||||
export function convertTokenString(s: string) {
|
||||
const result: any = ['concat'];
|
||||
const re = /{([^{}]+)}/g;
|
||||
let pos = 0;
|
||||
for (let match = re.exec(s); match !== null; match = re.exec(s)) {
|
||||
const literal = s.slice(pos, re.lastIndex - match[0].length);
|
||||
pos = re.lastIndex;
|
||||
if (literal.length > 0) result.push(literal);
|
||||
result.push(['get', match[1]]);
|
||||
}
|
||||
|
||||
if (result.length === 1) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (pos < s.length) {
|
||||
result.push(s.slice(pos));
|
||||
} else if (result.length === 2) {
|
||||
return ['to-string', result[1]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user