Add better relation support
This commit is contained in:
parent
7b47af8978
commit
12afdcab75
18 changed files with 2637 additions and 2386 deletions
|
@ -130,8 +130,11 @@ export interface LayerConfigJson {
|
|||
*/
|
||||
rotation?: string | TagRenderingConfigJson;
|
||||
/**
|
||||
* A HTML-fragment that is shown at the center of the icon, for example:
|
||||
* A HTML-fragment that is shown below the icon, for example:
|
||||
* <div style="background: white; display: block">{name}</div>
|
||||
*
|
||||
* If the icon is undefined, then the label is shown in the center of the feature.
|
||||
* Note that, if the wayhandling hides the icon then no label is shown as well.
|
||||
*/
|
||||
label?: string | TagRenderingConfigJson ;
|
||||
|
||||
|
|
|
@ -77,7 +77,8 @@ export default class TagRenderingConfig {
|
|||
throw `Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${context}`
|
||||
}
|
||||
if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) {
|
||||
throw `Freeform.key ${this.freeform.key} is an invalid type`
|
||||
const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", ");
|
||||
throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}`
|
||||
}
|
||||
if (this.freeform.addExtraTags) {
|
||||
const usedKeys = new And(this.freeform.addExtraTags).usedKeys();
|
||||
|
@ -204,7 +205,7 @@ export default class TagRenderingConfig {
|
|||
return true;
|
||||
}
|
||||
if (this.multiAnswer) {
|
||||
for (const m of this.mappings) {
|
||||
for (const m of this.mappings ?? []) {
|
||||
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ The above code will be executed for every feature in the layer. The feature is a
|
|||
* distanceTo
|
||||
* overlapWith
|
||||
* closest
|
||||
* memberships
|
||||
|
||||
### distanceTo
|
||||
|
||||
|
@ -71,4 +72,8 @@ Gives a list of features from the specified layer which this feature overlaps wi
|
|||
|
||||
Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.
|
||||
|
||||
* list of features
|
||||
* list of features
|
||||
|
||||
### memberships
|
||||
|
||||
Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. For example: \`\_part\_of\_walking\_routes=feat.memberships().map(r => r.relation.tags.name).join(';')\`
|
|
@ -1,6 +1,7 @@
|
|||
import {GeoOperations} from "./GeoOperations";
|
||||
import {UIElement} from "../UI/UIElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import State from "../State";
|
||||
|
||||
export class ExtraFunction {
|
||||
|
||||
|
@ -35,7 +36,7 @@ The above code will be executed for every feature in the layer. The feature is a
|
|||
Some advanced functions are available on <b>feat</b> as well:
|
||||
|
||||
`
|
||||
private static OverlapFunc = new ExtraFunction(
|
||||
private static readonly OverlapFunc = new ExtraFunction(
|
||||
"overlapWith",
|
||||
"Gives a list of features from the specified layer which this feature overlaps with, the amount of overlap in m². The returned value is <b>{ feat: GeoJSONFeature, overlap: number}</b>",
|
||||
["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"],
|
||||
|
@ -56,26 +57,26 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
}
|
||||
}
|
||||
)
|
||||
private static DistanceToFunc = new ExtraFunction(
|
||||
private static readonly DistanceToFunc = new ExtraFunction(
|
||||
"distanceTo",
|
||||
"Calculates the distance between the feature and a specified point",
|
||||
["longitude", "latitude"],
|
||||
(featuresPerLayer, feature) => {
|
||||
return (arg0, lat) => {
|
||||
if(typeof arg0 === "number"){
|
||||
if (typeof arg0 === "number") {
|
||||
const lon = arg0
|
||||
// Feature._lon and ._lat is conveniently place by one of the other metatags
|
||||
return GeoOperations.distanceBetween([lon, lat], [feature._lon, feature._lat]);
|
||||
}else{
|
||||
} else {
|
||||
// arg0 is probably a feature
|
||||
return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0),[feature._lon, feature._lat])
|
||||
return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private static ClosestObjectFunc = new ExtraFunction(
|
||||
private static readonly ClosestObjectFunc = new ExtraFunction(
|
||||
"closest",
|
||||
"Given either a list of geojson features or a single layer name, gives the single object which is nearest to the feature. In the case of ways/polygons, only the centerpoint is considered.",
|
||||
["list of features"],
|
||||
|
@ -87,7 +88,7 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
let closestFeature = undefined;
|
||||
let closestDistance = undefined;
|
||||
for (const otherFeature of features) {
|
||||
if(otherFeature == feature){
|
||||
if (otherFeature == feature) {
|
||||
continue; // We ignore self
|
||||
}
|
||||
let distance = undefined;
|
||||
|
@ -99,10 +100,10 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
[feature._lon, feature._lat]
|
||||
)
|
||||
}
|
||||
if(distance === undefined){
|
||||
if (distance === undefined) {
|
||||
throw "Undefined distance!"
|
||||
}
|
||||
if(closestFeature === undefined || distance < closestDistance){
|
||||
if (closestFeature === undefined || distance < closestDistance) {
|
||||
closestFeature = otherFeature
|
||||
closestDistance = distance;
|
||||
}
|
||||
|
@ -113,7 +114,19 @@ Some advanced functions are available on <b>feat</b> as well:
|
|||
)
|
||||
|
||||
|
||||
private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc];
|
||||
private static readonly Memberships = new ExtraFunction(
|
||||
"memberships",
|
||||
"Gives a list of {role: string, relation: Relation}-objects, containing all the relations that this feature is part of. \n\nFor example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
|
||||
[],
|
||||
(featuresPerLayer, feature) => {
|
||||
return () => {
|
||||
return State.state.knownRelations.data?.get(feature.id) ?? [];
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
private static readonly allFuncs: ExtraFunction[] = [ExtraFunction.DistanceToFunc, ExtraFunction.OverlapFunc, ExtraFunction.ClosestObjectFunc, ExtraFunction.Memberships];
|
||||
private readonly _name: string;
|
||||
private readonly _args: string[];
|
||||
private readonly _doc: string;
|
||||
|
|
|
@ -12,6 +12,7 @@ import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
|||
import Loc from "../../Models/Loc";
|
||||
import GeoJsonSource from "./GeoJsonSource";
|
||||
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
|
||||
import RegisteringFeatureSource from "./RegisteringFeatureSource";
|
||||
|
||||
export default class FeaturePipeline implements FeatureSource {
|
||||
|
||||
|
@ -24,33 +25,38 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
locationControl: UIEventSource<Loc>) {
|
||||
|
||||
const amendedOverpassSource =
|
||||
new RememberingSource(new FeatureDuplicatorPerLayer(flayers,
|
||||
new LocalStorageSaver(updater, layout))
|
||||
);
|
||||
new RememberingSource(
|
||||
new LocalStorageSaver(
|
||||
new MetaTaggingFeatureSource( // first we metatag, then we save to get the metatags into storage too
|
||||
new RegisteringFeatureSource(
|
||||
new FeatureDuplicatorPerLayer(flayers,
|
||||
updater)
|
||||
)), layout));
|
||||
|
||||
const geojsonSources: GeoJsonSource [] = []
|
||||
for (const flayer of flayers.data) {
|
||||
const sourceUrl = flayer.layerDef.source.geojsonSource
|
||||
if (sourceUrl !== undefined) {
|
||||
geojsonSources.push(
|
||||
new GeoJsonSource(flayer.layerDef.id, sourceUrl))
|
||||
geojsonSources.push(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers,
|
||||
new GeoJsonSource(flayer.layerDef.id, sourceUrl))))
|
||||
}
|
||||
}
|
||||
|
||||
const amendedLocalStorageSource =
|
||||
new RememberingSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
|
||||
);
|
||||
new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
|
||||
));
|
||||
|
||||
newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints);
|
||||
newPoints = new MetaTaggingFeatureSource(new FeatureDuplicatorPerLayer(flayers,
|
||||
new RegisteringFeatureSource(newPoints)));
|
||||
|
||||
const merged =
|
||||
new MetaTaggingFeatureSource(
|
||||
new FeatureSourceMerger([
|
||||
amendedOverpassSource,
|
||||
amendedLocalStorageSource,
|
||||
newPoints,
|
||||
...geojsonSources
|
||||
]));
|
||||
|
||||
new FeatureSourceMerger([
|
||||
amendedOverpassSource,
|
||||
amendedLocalStorageSource,
|
||||
newPoints,
|
||||
...geojsonSources
|
||||
]);
|
||||
|
||||
const source =
|
||||
new WayHandlingApplyingFeatureSource(flayers,
|
||||
|
|
|
@ -16,10 +16,6 @@ export default class MetaTaggingFeatureSource implements FeatureSource {
|
|||
featuresFreshness.forEach(featureFresh => {
|
||||
const feature = featureFresh.feature;
|
||||
|
||||
if(!State.state.allElements.has(feature.properties.id)){
|
||||
State.state.allElements.addOrGetElement(feature)
|
||||
}
|
||||
|
||||
if (Hash.hash.data === feature.properties.id) {
|
||||
State.state.selectedElement.setData(feature);
|
||||
}
|
||||
|
|
19
Logic/FeatureSource/RegisteringFeatureSource.ts
Normal file
19
Logic/FeatureSource/RegisteringFeatureSource.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import FeatureSource from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import State from "../../State";
|
||||
|
||||
export default class RegisteringFeatureSource implements FeatureSource {
|
||||
features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
constructor(source: FeatureSource) {
|
||||
this.features = source.features;
|
||||
this.features.addCallbackAndRun(features => {
|
||||
for (const feature of features ?? []) {
|
||||
if (!State.state.allElements.has(feature.feature.properties.id)) {
|
||||
State.state.allElements.addOrGetElement(feature.feature)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import * as turf from 'turf'
|
||||
import * as turf from '@turf/turf'
|
||||
|
||||
export class GeoOperations {
|
||||
|
||||
|
@ -118,6 +118,9 @@ export class GeoOperations {
|
|||
return inside;
|
||||
};
|
||||
|
||||
static lengthInMeters(feature: any) {
|
||||
return turf.length(feature) * 1000
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
58
Logic/Osm/ExtractRelations.ts
Normal file
58
Logic/Osm/ExtractRelations.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import State from "../../State";
|
||||
|
||||
export interface Relation {
|
||||
id: number,
|
||||
type: "relation"
|
||||
members: {
|
||||
type: ("way" | "node" | "relation"),
|
||||
ref: number,
|
||||
role: string
|
||||
}[],
|
||||
tags: any,
|
||||
// Alias for tags; tags == properties
|
||||
properties: any
|
||||
}
|
||||
|
||||
export default class ExtractRelations {
|
||||
|
||||
public static RegisterRelations(overpassJson: any) : void{
|
||||
const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
|
||||
console.log("Assigned memberships: ", memberships)
|
||||
State.state.knownRelations.setData(memberships)
|
||||
}
|
||||
|
||||
private static GetRelationElements(overpassJson: any): Relation[] {
|
||||
const relations = overpassJson.elements.filter(element => element.type === "relation")
|
||||
for (const relation of relations) {
|
||||
relation.properties = relation.tags
|
||||
}
|
||||
return relations
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a mapping of {memberId --> {role in relation, id of relation} }
|
||||
* @param relations
|
||||
* @constructor
|
||||
*/
|
||||
private static BuildMembershipTable(relations: Relation[]): Map<string, { role: string, relation: Relation, }[]> {
|
||||
const memberships = new Map<string, { role: string, relation: Relation }[]>()
|
||||
|
||||
for (const relation of relations) {
|
||||
for (const member of relation.members) {
|
||||
|
||||
const role = {
|
||||
role: member.role,
|
||||
relation: relation
|
||||
}
|
||||
const key = member.type + "/" + member.ref
|
||||
if (!memberships.has(key)) {
|
||||
memberships.set(key, [])
|
||||
}
|
||||
memberships.get(key).push(role)
|
||||
}
|
||||
}
|
||||
|
||||
return memberships
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import * as $ from "jquery"
|
||||
import * as OsmToGeoJson from "osmtogeojson";
|
||||
import Bounds from "../../Models/Bounds";
|
||||
import {TagsFilter} from "../TagsFilter";
|
||||
import {TagsFilter} from "../Tags/TagsFilter";
|
||||
import ExtractRelations from "./ExtractRelations";
|
||||
|
||||
/**
|
||||
* Interfaces overpass to get all the latest data
|
||||
|
@ -38,9 +39,9 @@ export class Overpass {
|
|||
return;
|
||||
}
|
||||
|
||||
ExtractRelations.RegisterRelations(json)
|
||||
// @ts-ignore
|
||||
const geojson = OsmToGeoJson.default(json);
|
||||
console.log("Received geojson", geojson)
|
||||
const osmTime = new Date(json.osm3s.timestamp_osm_base);
|
||||
continuation(geojson, osmTime);
|
||||
|
||||
|
|
|
@ -52,6 +52,18 @@ export default class SimpleMetaTagger {
|
|||
feature.area = sqMeters;
|
||||
})
|
||||
);
|
||||
|
||||
private static lngth = new SimpleMetaTagger(
|
||||
["_length", "_length:km"], "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter",
|
||||
(feature => {
|
||||
const l = GeoOperations.lengthInMeters(feature)
|
||||
feature.properties["_length"] = "" + l
|
||||
const km = Math.floor(l / 1000)
|
||||
const kmRest = Math.round((l - km * 1000) / 100)
|
||||
feature.properties["_length:km"] = "" + km+ "." + kmRest
|
||||
})
|
||||
)
|
||||
|
||||
private static country = new SimpleMetaTagger(
|
||||
["_country"], "The country code of the property (with latlon2country)",
|
||||
feature => {
|
||||
|
@ -294,6 +306,7 @@ export default class SimpleMetaTagger {
|
|||
public static metatags = [
|
||||
SimpleMetaTagger.latlon,
|
||||
SimpleMetaTagger.surfaceArea,
|
||||
SimpleMetaTagger.lngth,
|
||||
SimpleMetaTagger.country,
|
||||
SimpleMetaTagger.isOpen,
|
||||
SimpleMetaTagger.carriageWayWidth,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.6.8c";
|
||||
public static vNumber = "0.6.9";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
6
State.ts
6
State.ts
|
@ -17,6 +17,7 @@ import UpdateFromOverpass from "./Logic/Actors/UpdateFromOverpass";
|
|||
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||
import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
|
@ -76,6 +77,11 @@ export default class State {
|
|||
*/
|
||||
public readonly selectedElement = new UIEventSource<any>(undefined, "Selected element")
|
||||
|
||||
/**
|
||||
* Keeps track of relations: which way is part of which other way?
|
||||
* Set by the overpass-updater; used in the metatagging
|
||||
*/
|
||||
public readonly knownRelations = new UIEventSource<Map<string, {role: string, relation: Relation}[]>>(undefined, "Relation memberships")
|
||||
|
||||
public readonly featureSwitchUserbadge: UIEventSource<boolean>;
|
||||
public readonly featureSwitchSearch: UIEventSource<boolean>;
|
||||
|
|
|
@ -46,7 +46,7 @@ export default class TagRenderingAnswer extends UIElement {
|
|||
if (this._configuration.multiAnswer) {
|
||||
|
||||
let freeformKeyUsed = this._configuration.freeform?.key === undefined; // If it is undefined, it is "used" already, or at least we don't have to check for it anymore
|
||||
const applicableThens: Translation[] = Utils.NoNull(this._configuration.mappings.map(mapping => {
|
||||
const applicableThens: Translation[] = Utils.NoNull(this._configuration.mappings?.map(mapping => {
|
||||
if (mapping.if === undefined) {
|
||||
return mapping.then;
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export default class TagRenderingAnswer extends UIElement {
|
|||
return mapping.then;
|
||||
}
|
||||
return undefined;
|
||||
}))
|
||||
}) ?? [])
|
||||
|
||||
if (!freeformKeyUsed
|
||||
&& tags[this._configuration.freeform.key] !== undefined) {
|
||||
|
|
|
@ -27,10 +27,126 @@
|
|||
"play_forest",
|
||||
"playground",
|
||||
"sport_pitch",
|
||||
"slow_roads",
|
||||
{ "builtin": "slow_roads",
|
||||
"override": {
|
||||
"calculatedTags": [
|
||||
"_part_of_walking_routes=feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\").join(', ')"
|
||||
]
|
||||
}
|
||||
},
|
||||
"grass_in_parks",
|
||||
"village_green"
|
||||
"village_green",
|
||||
{
|
||||
"id": "walking_routes",
|
||||
"name": {
|
||||
"nl": "Wandelroutes van provincie Antwerpen"
|
||||
},
|
||||
"description": "Walking routes by 'provincie Antwerpen'",
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
"type=route",
|
||||
"route=foot",
|
||||
"operator=provincie Antwerpen"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"render": "Wandeling <i>{name}</i>",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "name~.*wandeling.*",
|
||||
"then": "{name}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tagRenderings": [
|
||||
{
|
||||
"render": {
|
||||
"nl": "Deze wandeling is <b>{_length:km}km</b> lang"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"if": "route=iwn",
|
||||
"then": {
|
||||
"nl": "Dit is een internationale wandelroute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "route=nwn",
|
||||
"then": {
|
||||
"nl": "Dit is een nationale wandelroute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "route=rwn",
|
||||
"then": {
|
||||
"nl": "Dit is een regionale wandelroute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "route=lwn",
|
||||
"then": {
|
||||
"nl": "Dit is een lokale wandelroute"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"render": {
|
||||
"nl": "<h3>Korte beschrijving:</h3>{description}"
|
||||
},
|
||||
"question": "Geef een korte beschrijving van de wandeling (max 255 tekens)",
|
||||
"freeform": {
|
||||
"key": "description",
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"nl": "Wie beheert deze wandeling en plaatst dus de signalisatiebordjes?"
|
||||
},
|
||||
"render": "Signalisatie geplaatst door {operator}",
|
||||
"freeform":{
|
||||
"key": "operator"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"nl": "Naar wie kan men emailen bij problemen rond signalisatie?"
|
||||
},
|
||||
"render": {
|
||||
"nl": "Bij problemen met signalisatie kan men emailen naar <a href='mailto:{operator:email}'>{operator:email}</a>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "operator:email",
|
||||
"type": "email"
|
||||
}
|
||||
},
|
||||
"questions",
|
||||
"reviews"
|
||||
],
|
||||
"color": {
|
||||
"render": "#6d6",
|
||||
"mappings":[
|
||||
{
|
||||
"if": "color~*",
|
||||
"then": "{color}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"width": {
|
||||
"render": "3"
|
||||
}
|
||||
}
|
||||
|
||||
],
|
||||
"roamingRenderings": []
|
||||
"roamingRenderings": [
|
||||
{
|
||||
"render": "Maakt deel uit van {_part_of_walking_routes}",
|
||||
"condition": "_part_of_walking_routes~*"
|
||||
}
|
||||
]
|
||||
}
|
4691
package-lock.json
generated
4691
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -38,7 +38,11 @@
|
|||
"dependencies": {
|
||||
"@babel/preset-env": "7.13.8",
|
||||
"@tailwindcss/postcss7-compat": "^2.0.2",
|
||||
"@turf/buffer": "^6.3.0",
|
||||
"@turf/collect": "^6.3.0",
|
||||
"@turf/distance": "^6.3.0",
|
||||
"@turf/length": "^6.3.0",
|
||||
"@turf/turf": "^6.3.0",
|
||||
"@types/jquery": "^3.5.5",
|
||||
"@types/leaflet-markercluster": "^1.0.3",
|
||||
"@types/leaflet-providers": "^1.2.0",
|
||||
|
|
|
@ -42,7 +42,7 @@ writeFileSync("./assets/generated/known_layers_and_themes.json", JSON.stringify(
|
|||
}))
|
||||
|
||||
|
||||
console.log("Discovered ", layerFiles.length, "layers and ", themeFiles.length, "themes\n")
|
||||
console.log("Discovered", layerFiles.length, "layers and", themeFiles.length, "themes\n")
|
||||
console.log(" ---------- VALIDATING ---------")
|
||||
// ------------- VALIDATION --------------
|
||||
const licensePaths = []
|
||||
|
|
Loading…
Reference in a new issue