forked from MapComplete/MapComplete
		
	Various fixes
This commit is contained in:
		
							parent
							
								
									18044ff22b
								
							
						
					
					
						commit
						07fd8f404a
					
				
					 14 changed files with 154 additions and 60 deletions
				
			
		|  | @ -10,6 +10,10 @@ export class BBox { | |||
|     readonly minLat: number; | ||||
|     readonly minLon: number; | ||||
| 
 | ||||
|     /*** | ||||
|      * Coordinates should be [[lon, lat],[lon, lat]] | ||||
|      * @param coordinates | ||||
|      */ | ||||
|     constructor(coordinates) { | ||||
|         this.maxLat = -90; | ||||
|         this.maxLon = -180; | ||||
|  | @ -45,6 +49,21 @@ export class BBox { | |||
|         return feature.bbox; | ||||
|     } | ||||
|      | ||||
|     static bboxAroundAll(bboxes: BBox[]): BBox{ | ||||
|         let maxLat: number = -90; | ||||
|         let maxLon: number= -180; | ||||
|         let minLat: number= 80; | ||||
|         let minLon: number= 180; | ||||
| 
 | ||||
|         for (const bbox of bboxes) { | ||||
|             maxLat = Math.max(maxLat, bbox.maxLat) | ||||
|             maxLon = Math.max(maxLon, bbox.maxLon) | ||||
|             minLat = Math.min(minLat, bbox.minLat) | ||||
|             minLon = Math.min(minLon, bbox.minLon) | ||||
|         } | ||||
|         return new BBox([[maxLon, maxLat],[minLon,minLat]]) | ||||
|     } | ||||
| 
 | ||||
|     static fromTile(z: number, x: number, y: number): BBox { | ||||
|         return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) | ||||
|     } | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ export interface ExtraFuncParams { | |||
|      */ | ||||
|     getFeaturesWithin: (layerId: string, bbox: BBox) => any[][], | ||||
|     memberships: RelationsTracker | ||||
|     getFeatureById: (id:string) => any | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -79,20 +80,19 @@ class DistanceToFunc implements ExtraFunction { | |||
|             } | ||||
|             if (typeof arg0 === "number") { | ||||
|                 // Feature._lon and ._lat is conveniently place by one of the other metatags
 | ||||
|                 return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]); | ||||
|                 return GeoOperations.distanceBetween([arg0, lat], GeoOperations.centerpointCoordinates(feature)); | ||||
|             } | ||||
|             if (typeof arg0 === "string") { | ||||
|                 // This is an identifier
 | ||||
|                 // TODO FIXME
 | ||||
|                 const feature = undefined // State.state.allElements.ContainingFeatures.get(arg0);
 | ||||
|                 const feature =  featuresPerLayer.getFeatureById(arg0) | ||||
|                 if (feature === undefined) { | ||||
|                     return undefined; | ||||
|                 } | ||||
|                 arg0 = feature; | ||||
|             } | ||||
| 
 | ||||
|             // arg0 is probably a feature
 | ||||
|             return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat]) | ||||
|             // arg0 is probably a geojsonfeature
 | ||||
|             return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), GeoOperations.centerpointCoordinates(feature)) | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -448,11 +448,12 @@ export default class FeaturePipeline { | |||
|         window.setTimeout( | ||||
|             () => { | ||||
|                 const layerDef = src.layer.layerDef; | ||||
|                 const somethingChanged = MetaTagging.addMetatags( | ||||
|                 MetaTagging.addMetatags( | ||||
|                     src.features.data, | ||||
|                     { | ||||
|                         memberships: this.relationTracker, | ||||
|                         getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox) | ||||
|                         getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox), | ||||
|                         getFeatureById: (id:string) => self.state.allElements.ContainingFeatures.get(id) | ||||
|                     }, | ||||
|                     layerDef, | ||||
|                     { | ||||
|  |  | |||
|  | @ -178,8 +178,6 @@ export default class MetaTagging { | |||
| 
 | ||||
|             try { | ||||
|                 const functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) | ||||
| 
 | ||||
| 
 | ||||
|                 ExtraFunctions.FullPatchFeature(params, feature); | ||||
|                 for (const f of functions) { | ||||
|                     f(feature); | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import {UIEventSource} from "../UIEventSource"; | |||
| import MapState from "./MapState"; | ||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | ||||
| import Hash from "../Web/Hash"; | ||||
| import {BBox} from "../BBox"; | ||||
| 
 | ||||
| export default class FeaturePipelineState extends MapState { | ||||
| 
 | ||||
|  | @ -29,6 +30,8 @@ export default class FeaturePipelineState extends MapState { | |||
| 
 | ||||
|                 clusterCounter.addTile(source) | ||||
| 
 | ||||
|                 const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) | ||||
|                  | ||||
|                 // Do show features indicates if the 'showDataLayer' should be shown
 | ||||
|                 const doShowFeatures = source.features.map( | ||||
|                     f => { | ||||
|  | @ -44,7 +47,7 @@ export default class FeaturePipelineState extends MapState { | |||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         if (!source.bbox.overlapsWith(bounds)) { | ||||
|                         if (!sourceBBox.data.overlapsWith(bounds)) { | ||||
|                             // Not within range -> features are hidden
 | ||||
|                             return false | ||||
|                         } | ||||
|  | @ -81,7 +84,7 @@ export default class FeaturePipelineState extends MapState { | |||
| 
 | ||||
| 
 | ||||
|                         return true | ||||
|                     }, [this.currentBounds, source.layer.isDisplayed] | ||||
|                     }, [this.currentBounds, source.layer.isDisplayed, sourceBBox] | ||||
|                 ) | ||||
| 
 | ||||
|                 new ShowDataLayer( | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import {Utils} from "../Utils"; | |||
| 
 | ||||
| export default class Constants { | ||||
| 
 | ||||
|     public static vNumber = "0.13.0-alpha-2"; | ||||
|     public static vNumber = "0.13.0-alpha-3"; | ||||
|     public static ImgurApiKey = '7070e7167f0a25a' | ||||
|     public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" | ||||
| 
 | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ export default class DependencyCalculator { | |||
|             let currentKey = undefined | ||||
|             let currentLine = undefined | ||||
|             const params: ExtraFuncParams = { | ||||
|                 getFeatureById: _ => undefined, | ||||
|                 getFeaturesWithin: (layerId, _) => { | ||||
| 
 | ||||
|                     if(layerId === '*'){ | ||||
|  |  | |||
|  | @ -219,7 +219,7 @@ export interface LayoutConfigJson { | |||
|          * If clustering is defined, defaults to 25 | ||||
|          */ | ||||
|         minNeededElements?: number | ||||
|     }, | ||||
|     } | false, | ||||
| 
 | ||||
|     /** | ||||
|      * The URL of a custom CSS stylesheet to modify the layout | ||||
|  |  | |||
|  | @ -5,9 +5,9 @@ export default class SourceConfig { | |||
| 
 | ||||
|     public readonly osmTags?: TagsFilter; | ||||
|     public readonly overpassScript?: string; | ||||
|     public readonly geojsonSource?: string; | ||||
|     public readonly geojsonZoomLevel?: number; | ||||
|     public readonly isOsmCacheLayer: boolean; | ||||
|     public geojsonSource?: string; | ||||
|     public geojsonZoomLevel?: number; | ||||
|     public isOsmCacheLayer: boolean; | ||||
|     public readonly mercatorCrs: boolean; | ||||
| 
 | ||||
|     constructor(params: { | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -212,7 +212,7 @@ Note that these values can be prepare with javascript in the theme by using a [c | |||
|                 } | ||||
|                  | ||||
|                 if(v.InnerConstructElement !== undefined){ | ||||
|                     console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string", v) | ||||
|                     console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", key,"\nThe value is", v) | ||||
|                     v = ( <HTMLElement> v.InnerConstructElement())?.innerText | ||||
|                 } | ||||
|                  | ||||
|  |  | |||
							
								
								
									
										12
									
								
								assets/themes/postal_codes/license_info.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								assets/themes/postal_codes/license_info.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| [ | ||||
|   { | ||||
|     "path": "townhall.svg", | ||||
|     "license": "CC0", | ||||
|     "authors": [ | ||||
|       "Nebulon42" | ||||
|     ], | ||||
|     "sources": [ | ||||
|       "https://wiki.openstreetmap.org/wiki/File:Town-hall-16.svg" | ||||
|     ] | ||||
|   } | ||||
| ] | ||||
|  | @ -13,7 +13,7 @@ | |||
|     "en" | ||||
|   ], | ||||
|   "maintainer": "", | ||||
|   "icon": "./assets/svg/bug.svg", | ||||
|   "icon": "./assets/themes/postal_codes/townhall.svg", | ||||
|   "version": "0", | ||||
|   "startLat": 0, | ||||
|   "startLon": 0, | ||||
|  | @ -21,13 +21,15 @@ | |||
|   "widenFactor": 0.05, | ||||
|   "socialImage": "", | ||||
|   "hideFromOverview": true, | ||||
|   "clustering": false, | ||||
|   "overpassTimeout": 180, | ||||
|   "layers": [ | ||||
|     { | ||||
|       "id": "postal_codes", | ||||
|       "id": "postal_code_boundary", | ||||
|       "name": { | ||||
|         "en": "postal codes" | ||||
|       }, | ||||
|       "minzoom": 12, | ||||
|       "minzoom": 8, | ||||
|       "title": { | ||||
|         "render": { | ||||
|           "en": "Postal code {postal_code}" | ||||
|  | @ -42,11 +44,7 @@ | |||
|           } | ||||
|         } | ||||
|       ], | ||||
|       "presets": [], | ||||
|       "source": { | ||||
|         "isOsmCache": true, | ||||
|         "geoJson": "http://127.0.0.1:8080/postal_codes_postal_codes_{z}_{x}_{y}.geojson", | ||||
|         "geoJsonZoomLevel": 1, | ||||
|         "osmTags": { | ||||
|           "or": [ | ||||
|             "boundary=postal_code", | ||||
|  | @ -61,12 +59,7 @@ | |||
|       }, | ||||
|       "mapRendering": [ | ||||
|         { | ||||
|           "icon": { | ||||
|             "render": "./assets/svg/bug.svg" | ||||
|           }, | ||||
|           "iconSize": { | ||||
|             "render": "40,40,center" | ||||
|           }, | ||||
|           "label": "<div class='text-xl bg-white rounded-full pl-2 pr-2 break-normal'>{postal_code}</div>", | ||||
|           "location": [ | ||||
|             "point", | ||||
|             "centroid" | ||||
|  | @ -77,8 +70,10 @@ | |||
|             "render": "#00f" | ||||
|           }, | ||||
|           "width": { | ||||
|             "render": "8" | ||||
|           } | ||||
|             "render": "4" | ||||
|           }, | ||||
|           "fill": "no", | ||||
|           "dashArray": "8 8" | ||||
|         } | ||||
|       ], | ||||
|       "isShown": { | ||||
|  | @ -90,34 +85,30 @@ | |||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "id": "town_halls", | ||||
|       "id": "town_hall", | ||||
|       "name": { | ||||
|         "en": "town halls" | ||||
|       }, | ||||
|       "minzoom": 12, | ||||
|       "title": { | ||||
|         "render": { | ||||
|           "en": "Town halls" | ||||
|           "en": "Town hall {name}" | ||||
|         } | ||||
|       }, | ||||
|       "calculatedTags": [ | ||||
|         "_postal_code=feat.overlapWith('postal_codes')[0]?.feat?.properties?.postal_code" | ||||
|       ], | ||||
|         "_postal_code_properties=(() => { const f = feat.overlapWith('postal_code_boundary'); if(f.length===0){return {};}; const p = f[0]?.feat?.properties; return {id:p.id, postal_code: p.postal_code, _closest_town_hall: p._closest_town_hall}; })()", | ||||
|         "_postal_code=feat.get('_postal_code_properties')?.postal_code", | ||||
|         "_postal_code_center_distance=feat.distanceTo(feat.get('_postal_code_properties').id)" | ||||
|        ], | ||||
|       "description": {}, | ||||
|       "tagRenderings": [ | ||||
|       ], | ||||
|       "presets": [], | ||||
|       "source": { | ||||
|         "isOsmCache": true, | ||||
|         "geoJson": "http://127.0.0.1:8080/postal_codes_town_hall_{z}_{x}_{y}.geojson", | ||||
|         "geoJsonZoomLevel": 1, | ||||
|         "osmTags": "amenity=townhall" | ||||
|       }, | ||||
|       "mapRendering": [ | ||||
|         { | ||||
|           "icon": { | ||||
|             "render": "./assets/svg/bug.svg" | ||||
|           }, | ||||
|         { "icon": "./assets/themes/postal_codes/townhall.svg", | ||||
|           "iconSize": { | ||||
|             "render": "40,40,center" | ||||
|           }, | ||||
|  | @ -125,14 +116,6 @@ | |||
|             "point", | ||||
|             "centroid" | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "color": { | ||||
|             "render": "#00f" | ||||
|           }, | ||||
|           "width": { | ||||
|             "render": "8" | ||||
|           } | ||||
|         } | ||||
|       ], | ||||
|       "isShown": { | ||||
							
								
								
									
										25
									
								
								assets/themes/postal_codes/townhall.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								assets/themes/postal_codes/townhall.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    version="1.1" | ||||
|    width="16" | ||||
|    height="16" | ||||
|    viewBox="0 0 16 16" | ||||
|    id="svg2"> | ||||
|   <metadata id="metadata8"> | ||||
|     <rdf:RDF> | ||||
|       <cc:Work rdf:about=""> | ||||
|         <dc:format>image/svg+xml</dc:format> | ||||
|         <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> | ||||
|         <dc:title/> | ||||
|       </cc:Work> | ||||
|     </rdf:RDF> | ||||
|   </metadata> | ||||
|   <defs id="defs6"/> | ||||
|   <rect width="16" height="16" x="0" y="0" id="canvas" style="fill:none;stroke:none;visibility:hidden"/> | ||||
|   <path d="M 7,0 C 6.75,0.0032 6.5,0.1644239 6.5,0.5 L 6.5,4.375 1,7 13,7 7.5,4.375 7.5,0.5 C 7.5,0.1516409 7.25,-0.0031957 7,0 z M 8,0 8,3 12,3 10,1.5 12,0 z m -7,8 0,1 1,0 0,4 -1,0 0,1 12,0 0,-1 -1,0 0,-4 1,0 0,-1 z m 6,1.5 c 1,0 2,0.5 2,1.5 l 0,2 -4,0 0,-2 C 5,10 6,9.5 7,9.5 z" id="town-hall" style="fill:#734a08;fill-opacity:1;stroke:none" transform="translate(1,1)"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.1 KiB | 
|  | @ -24,7 +24,6 @@ import {GeoOperations} from "../Logic/GeoOperations"; | |||
| import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"; | ||||
| import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"; | ||||
| import Loc from "../Models/Loc"; | ||||
| 
 | ||||
| ScriptUtils.fixUtils() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -181,6 +180,23 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr | |||
| function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string, pointsOnlyLayers: string[]) { | ||||
|     const skippedLayers = new Set<string>() | ||||
| 
 | ||||
|     const indexedFeatures : Map<string, any> = new Map<string, any>() | ||||
|     let indexisBuilt = false; | ||||
|     function buildIndex(){ | ||||
|         for (const ff of allFeatures.features.data) { | ||||
|             const f = ff.feature | ||||
|             indexedFeatures.set(f.properties.id, f) | ||||
|         } | ||||
|         indexisBuilt = true; | ||||
|     } | ||||
|      | ||||
|     function getFeatureById(id){ | ||||
|         if(!indexisBuilt){ | ||||
|             buildIndex() | ||||
|         } | ||||
|         return indexedFeatures.get(id) | ||||
|     } | ||||
|      | ||||
|     async function handleLayer(source: FeatureSourceForLayer) { | ||||
|         const layer = source.layer.layerDef; | ||||
|         const targetZoomLevel = layer.source.geojsonZoomLevel ?? 0 | ||||
|  | @ -199,7 +215,8 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations | |||
|                 memberships: relationsTracker, | ||||
|                 getFeaturesWithin: _ => { | ||||
|                     return [allFeatures.features.data.map(f => f.feature)] | ||||
|                 } | ||||
|                 }, | ||||
|                 getFeatureById: getFeatureById | ||||
|             }, | ||||
|             layer, | ||||
|             { | ||||
|  | @ -237,6 +254,7 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations | |||
|                     new UIEventSource<any>(undefined) | ||||
|                     ) | ||||
| 
 | ||||
|                 console.log("Tile "+layer.id+"."+tileIndex+" contains "+filteredTile.features.data.length+" features after filtering ("+tile.features.data.length+") features before") | ||||
|                 if (filteredTile.features.data.length === 0) { | ||||
|                     return | ||||
|                 } | ||||
|  | @ -252,7 +270,7 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations | |||
|                     const calculatedTagKeys = tile.layer.layerDef.calculatedTags.map(ct => ct[0]) | ||||
|                     featureCount++ | ||||
|                     for (const calculatedTagKey of calculatedTagKeys) { | ||||
|                         const strict =  feature.feature.properties[calculatedTagKey] | ||||
|                         const strict = feature.feature.properties[calculatedTagKey] | ||||
|                         feature.feature.properties[calculatedTagKey] =strict | ||||
|                         strictlyCalculated ++; | ||||
|                         if(strictlyCalculated % 100 === 0){ | ||||
|  | @ -292,7 +310,18 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations | |||
| 
 | ||||
|         // And, if needed, to create a points-only layer
 | ||||
|         if (pointsOnlyLayers.indexOf(layer.id) >= 0) { | ||||
|             const features = source.features.data.map(f => f.feature) | ||||
| 
 | ||||
|             const filtered = new FilteringFeatureSource({ | ||||
|                     locationControl:  new UIEventSource<Loc>(undefined), | ||||
|                     allElements: undefined, | ||||
|                     selectedElement: new UIEventSource<any>(undefined) | ||||
|                 }, | ||||
|                 Tiles.tile_index(0,0,0), | ||||
|                 source, | ||||
|                 new UIEventSource<any>(undefined) | ||||
|             ) | ||||
|             const features = filtered.features.data.map(f => f.feature) | ||||
|              | ||||
|             const points = features.map(feature => GeoOperations.centerpoint(feature)) | ||||
|             console.log("Writing points overview for ", layerId) | ||||
|             const targetPath = targetdir + "_" + layerId + "_points.geojson" | ||||
|  | @ -325,7 +354,7 @@ async function main(args: string[]) { | |||
| 
 | ||||
|     console.log("Cache builder started with args ", args.join(", ")) | ||||
|     if (args.length < 6) { | ||||
|         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...]\n" + | ||||
|         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" + | ||||
|             "Note: a new directory named <theme> will be created in targetdirectory") | ||||
|         return; | ||||
|     } | ||||
|  | @ -343,10 +372,7 @@ async function main(args: string[]) { | |||
|     const lat1 = Number(args[5]) | ||||
|     const lon1 = Number(args[6]) | ||||
| 
 | ||||
|     let generatePointLayersFor = [] | ||||
|     if (args[7] == "--generate-point-overview") { | ||||
|         generatePointLayersFor = args[8].split(",") | ||||
|     } | ||||
|     | ||||
| 
 | ||||
| 
 | ||||
|     const tileRange = Tiles.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) | ||||
|  | @ -365,6 +391,32 @@ async function main(args: string[]) { | |||
|         console.error("The theme " + theme + " was not found; try one of ", keys); | ||||
|         return | ||||
|     } | ||||
| 
 | ||||
|     let generatePointLayersFor = [] | ||||
|     if (args[7] == "--generate-point-overview") { | ||||
|         if(args[8] === undefined){ | ||||
|             throw "--generate-point-overview needs a list of layers to generate the overview for (or * for all)" | ||||
|         }else if (args[8] === '*'){ | ||||
|             generatePointLayersFor = theme.layers.map(l => l.id) | ||||
|         }else{ | ||||
|             generatePointLayersFor = args[8].split(",") | ||||
|         } | ||||
|         console.log("Also generating a point overview for layers ", generatePointLayersFor.join(",")) | ||||
|     } | ||||
|     { | ||||
|          | ||||
|     const index = args.indexOf("--force-zoom-level") | ||||
|     if(index >= 0){ | ||||
|         const forcedZoomLevel = Number(args[index + 1]) | ||||
|         for (const layer of theme.layers) { | ||||
|             layer.source.geojsonSource = "https://127.0.0.1/cache_{layer}_{z}_{x}_{y}.geojson" | ||||
|             layer.source.isOsmCacheLayer = true | ||||
|             layer.source.geojsonZoomLevel = forcedZoomLevel | ||||
|         } | ||||
|     } | ||||
|     } | ||||
|      | ||||
|      | ||||
|     const relationTracker = new RelationsTracker() | ||||
| 
 | ||||
|     let failed = 0; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue