forked from MapComplete/MapComplete
		
	Chore: housekeeping, linting
This commit is contained in:
		
							parent
							
								
									f942529755
								
							
						
					
					
						commit
						30d00eb06d
					
				
					 74 changed files with 998 additions and 623 deletions
				
			
		|  | @ -8,7 +8,6 @@ import { Utils } from "../../../Utils" | |||
| import { TagsFilter } from "../../Tags/TagsFilter" | ||||
| import { BBox } from "../../BBox" | ||||
| import { OsmTags } from "../../../Models/OsmFeature" | ||||
| 
 | ||||
| ;("use strict") | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -10,12 +10,12 @@ import { | |||
|     MultiPolygon, | ||||
|     Point, | ||||
|     Polygon, | ||||
|     Position | ||||
|     Position, | ||||
| } from "geojson" | ||||
| import { Tiles } from "../Models/TileRange" | ||||
| import { Utils } from "../Utils" | ||||
| 
 | ||||
| ("use strict") | ||||
| ;("use strict") | ||||
| 
 | ||||
| export class GeoOperations { | ||||
|     private static readonly _earthRadius: number = 6378137 | ||||
|  | @ -107,7 +107,10 @@ export class GeoOperations { | |||
|      * @param lonlat0 | ||||
|      * @param lonlat1 | ||||
|      */ | ||||
|     static distanceBetween(lonlat0: [number, number] | Coord | Position, lonlat1: [number, number] | Position | Coord) { | ||||
|     static distanceBetween( | ||||
|         lonlat0: [number, number] | Coord | Position, | ||||
|         lonlat1: [number, number] | Position | Coord | ||||
|     ) { | ||||
|         return turf.distance(lonlat0, lonlat1, { units: "meters" }) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ export interface ProvidedImage { | |||
|     /** | ||||
|      * An alternative ID, used to deduplicate some images | ||||
|      */ | ||||
|     alt_id?: string, | ||||
|     alt_id?: string | ||||
|     date?: Date | ||||
|     status?: string | "ready" | ||||
|     /** | ||||
|  |  | |||
|  | @ -98,7 +98,11 @@ export class ImageUploadManager { | |||
| 
 | ||||
|         const tags = await ExifReader.load(file) | ||||
|         if (tags.ProjectionType.value === "cylindrical") { | ||||
|             return { error: new Translation({ en: "Cylindrical images (typically created by a Panorama-app) are not supported" }) } | ||||
|             return { | ||||
|                 error: new Translation({ | ||||
|                     en: "Cylindrical images (typically created by a Panorama-app) are not supported", | ||||
|                 }), | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return true | ||||
|  | @ -125,7 +129,6 @@ export class ImageUploadManager { | |||
|             ignoreGPS: boolean | false | ||||
|         } | ||||
|     ): void { | ||||
| 
 | ||||
|         const tags: OsmTags = tagsStore.data | ||||
|         const featureId = <OsmId | NoteId>tags.id | ||||
| 
 | ||||
|  | @ -290,7 +293,7 @@ export class ImageUploadManager { | |||
|         let absoluteUrl: string | ||||
| 
 | ||||
|         try { | ||||
|             ({ key, value, absoluteUrl } = await this._uploader.uploadImage( | ||||
|             ;({ key, value, absoluteUrl } = await this._uploader.uploadImage( | ||||
|                 blob, | ||||
|                 location, | ||||
|                 author, | ||||
|  |  | |||
|  | @ -196,7 +196,10 @@ export class Mapillary extends ImageProvider { | |||
|         try { | ||||
|             license.date = new Date(date) | ||||
|         } catch (e) { | ||||
|             console.warn("Could not parse captured_at date from mapillary image. The date is:", date) | ||||
|             console.warn( | ||||
|                 "Could not parse captured_at date from mapillary image. The date is:", | ||||
|                 date | ||||
|             ) | ||||
|         } | ||||
|         return license | ||||
|     } | ||||
|  |  | |||
|  | @ -28,7 +28,8 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|      * const match = url.match(PanoramaxImageProvider.isDirectLink) | ||||
|      * match[1] // => "e931ce57-4591-4dd5-aa4c-595e89c37e84"
 | ||||
|      */ | ||||
|     public static readonly isDirectLink = /https:\/\/panoramax.mapcomplete.org\/api\/pictures\/([0-9a-f-]+)\/(hd)|(sd)|(thumb).jpg/ | ||||
|     public static readonly isDirectLink = | ||||
|         /https:\/\/panoramax.mapcomplete.org\/api\/pictures\/([0-9a-f-]+)\/(hd)|(sd)|(thumb).jpg/ | ||||
| 
 | ||||
|     public defaultKeyPrefixes: string[] = ["panoramax", "image"] | ||||
|     public readonly name: string = "panoramax" | ||||
|  | @ -51,7 +52,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|             new SvelteUIElement(Panoramax_bw), | ||||
|             p.createViewLink({ | ||||
|                 imageId: img?.id, | ||||
|                 location | ||||
|                 location, | ||||
|             }), | ||||
|             true | ||||
|         ) | ||||
|  | @ -65,14 +66,14 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         const p = new Panoramax(host) | ||||
|         return p.createViewLink({ | ||||
|             imageId: img?.id, | ||||
|             location | ||||
|             location, | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public addKnownMeta(meta: ImageData, url?: string) { | ||||
|         PanoramaxImageProvider.knownMeta[meta.id] = { | ||||
|             data: Promise.resolve({ data: meta, url }), | ||||
|             time: new Date() | ||||
|             time: new Date(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -125,7 +126,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|             status: meta.properties["geovisio:status"], | ||||
|             rotation: Number(meta.properties["view:azimuth"]), | ||||
|             isSpherical: meta.properties.exif["Xmp.GPano.ProjectionType"] === "equirectangular", | ||||
|             date: new Date(meta.properties.datetime) | ||||
|             date: new Date(meta.properties.datetime), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -156,7 +157,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         const promise: Promise<{ data: ImageData; url: string }> = this.getInfoForUncached(id) | ||||
|         PanoramaxImageProvider.knownMeta[id] = { | ||||
|             time: new Date(), | ||||
|             data: promise | ||||
|             data: promise, | ||||
|         } | ||||
|         return await promise | ||||
|     } | ||||
|  | @ -215,7 +216,7 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|         return { | ||||
|             artist: meta.data.providers.at(-1).name, // We take the last provider, as that one probably contain the username of the uploader
 | ||||
|             date: new Date(meta.data.properties["datetime"]), | ||||
|             licenseShortName: meta.data.properties["geovisio:license"] | ||||
|             licenseShortName: meta.data.properties["geovisio:license"], | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -247,8 +248,8 @@ export default class PanoramaxImageProvider extends ImageProvider { | |||
|             properties: { | ||||
|                 url, | ||||
|                 northOffset, | ||||
|                 pitchOffset | ||||
|             } | ||||
|                 pitchOffset, | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -263,7 +264,6 @@ export class PanoramaxUploader implements ImageUploader { | |||
|         this.panoramax = new AuthorizedPanoramax(url, token) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     async uploadImage( | ||||
|         blob: File, | ||||
|         currentGps: [number, number], | ||||
|  | @ -287,13 +287,12 @@ export class PanoramaxUploader implements ImageUploader { | |||
|                 throw "Unsupported image format: cylindrical images (panorama images) are currently not supported" | ||||
|             } | ||||
|             if (tags?.GPSLatitude?.value && tags?.GPSLongitude?.value) { | ||||
| 
 | ||||
|                 const [[latD], [latM], [latS, latSDenom]] = < | ||||
|                     [[number, number], [number, number], [number, number]] | ||||
|                     >tags?.GPSLatitude?.value | ||||
|                 >tags?.GPSLatitude?.value | ||||
|                 const [[lonD], [lonM], [lonS, lonSDenom]] = < | ||||
|                     [[number, number], [number, number], [number, number]] | ||||
|                     >tags?.GPSLongitude?.value | ||||
|                 >tags?.GPSLongitude?.value | ||||
| 
 | ||||
|                 const exifLat = latD + latM / 60 + latS / (3600 * latSDenom) | ||||
|                 const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom) | ||||
|  | @ -335,9 +334,7 @@ export class PanoramaxUploader implements ImageUploader { | |||
|                 } else { | ||||
|                     datetime = exifDatetime.toISOString() | ||||
|                 } | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|         } catch (e) { | ||||
|             console.warn("Could not read EXIF-tags due to", e) | ||||
|         } | ||||
|  | @ -356,7 +353,7 @@ export class PanoramaxUploader implements ImageUploader { | |||
|             indexInSequence: sequence["stats:items"].count + 1, // stats:items is '1'-indexed, so .count is also the last index
 | ||||
|             exifOverride: { | ||||
|                 Artist: author, | ||||
|             } | ||||
|             }, | ||||
|         } | ||||
|         if (progress) { | ||||
|             options.onProgress = (e: ProgressEvent) => { | ||||
|  | @ -373,7 +370,7 @@ export class PanoramaxUploader implements ImageUploader { | |||
|         return { | ||||
|             key: "panoramax", | ||||
|             value: img.id, | ||||
|             absoluteUrl: img.assets.hd.href | ||||
|             absoluteUrl: img.assets.hd.href, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ export class Changes { | |||
|                 featureSwitchIsTesting?: Store<boolean> | ||||
|             } | ||||
|             osmConnection: OsmConnection | ||||
|             reportError?: ((message: string | Error | XMLHttpRequest, extramessage?: string) => void), | ||||
|             reportError?: (message: string | Error | XMLHttpRequest, extramessage?: string) => void | ||||
|             featureProperties?: FeaturePropertiesStore | ||||
|             historicalUserLocations?: FeatureSource<Feature<Point, GeoLocationPointProperties>> | ||||
|             allElements?: IndexedFeatureSource | ||||
|  | @ -694,7 +694,7 @@ export class Changes { | |||
|                     "Refusing change about " + | ||||
|                         c.type + | ||||
|                         "/" + | ||||
|                     id + | ||||
|                         id + | ||||
|                         " as not in the objects. No internet?" | ||||
|                 ) | ||||
|                 refused.push(c) | ||||
|  |  | |||
|  | @ -154,8 +154,8 @@ export class OsmConnection { | |||
|     constructor(options?: { | ||||
|         dryRun?: Store<boolean> | ||||
|         fakeUser?: false | boolean | ||||
|         oauth_token?: UIEventSource<string>, | ||||
|         shared_cookie?: string, | ||||
|         oauth_token?: UIEventSource<string> | ||||
|         shared_cookie?: string | ||||
|         // Used to keep multiple changesets open and to write to the correct changeset
 | ||||
|         singlePage?: boolean | ||||
|         attemptLogin?: boolean | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import { BBox } from "../BBox" | |||
| import osmtogeojson from "osmtogeojson" | ||||
| import { FeatureCollection, Geometry } from "geojson" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| ;("use strict") | ||||
| /** | ||||
|  * Interfaces overpass to get all the latest data | ||||
|  |  | |||
|  | @ -1,14 +1,42 @@ | |||
| import { Utils } from "../../Utils" | ||||
| /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ | ||||
| export class ThemeMetaTagging { | ||||
|    public static readonly themeName = "usersettings" | ||||
|     public static readonly themeName = "usersettings" | ||||
| 
 | ||||
|    public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { | ||||
|       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )  | ||||
|       Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )  | ||||
|       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href   }) (feat)  )  | ||||
|       Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat)  )  | ||||
|       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )  | ||||
|       feat.properties['__current_backgroun'] = 'initial_value' | ||||
|    } | ||||
| } | ||||
|     public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { | ||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => | ||||
|             feat.properties._description | ||||
|                 .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) | ||||
|                 ?.at(1) | ||||
|         ) | ||||
|         Utils.AddLazyProperty( | ||||
|             feat.properties, | ||||
|             "_d", | ||||
|             () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" | ||||
|         ) | ||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => | ||||
|             ((feat) => { | ||||
|                 const e = document.createElement("div") | ||||
|                 e.innerHTML = feat.properties._d | ||||
|                 return Array.from(e.getElementsByTagName("a")).filter( | ||||
|                     (a) => a.href.match(/mastodon|en.osm.town/) !== null | ||||
|                 )[0]?.href | ||||
|             })(feat) | ||||
|         ) | ||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => | ||||
|             ((feat) => { | ||||
|                 const e = document.createElement("div") | ||||
|                 e.innerHTML = feat.properties._d | ||||
|                 return Array.from(e.getElementsByTagName("a")).filter( | ||||
|                     (a) => a.getAttribute("rel")?.indexOf("me") >= 0 | ||||
|                 )[0]?.href | ||||
|             })(feat) | ||||
|         ) | ||||
|         Utils.AddLazyProperty( | ||||
|             feat.properties, | ||||
|             "_mastodon_candidate", | ||||
|             () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a | ||||
|         ) | ||||
|         feat.properties["__current_backgroun"] = "initial_value" | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -361,7 +361,13 @@ export default class NameSuggestionIndex { | |||
|         return nsi.generateMappings(key, tags, country, center, options) | ||||
|     } | ||||
| 
 | ||||
|     private static readonly brandPrefix = ["name", "alt_name", "operator", "brand", "official_name"] as const | ||||
|     private static readonly brandPrefix = [ | ||||
|         "name", | ||||
|         "alt_name", | ||||
|         "operator", | ||||
|         "brand", | ||||
|         "official_name", | ||||
|     ] as const | ||||
| 
 | ||||
|     /** | ||||
|      * An NSI-item might have tags such as `name=X`, `alt_name=brand X`,  `brand=X`, `brand:wikidata`, `shop=Y`, `service:abc=yes` | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ class P4CImageFetcher implements ImageFetcher { | |||
|                 searchRadius, | ||||
|                 { | ||||
|                     mindate: new Date().getTime() - maxAgeSeconds, | ||||
|                     towardscenter: false | ||||
|                     towardscenter: false, | ||||
|                 } | ||||
|             ) | ||||
|         } catch (e) { | ||||
|  | @ -152,9 +152,9 @@ class ImagesInLoadedDataFetcher implements ImageFetcher { | |||
|                     coordinates: { lng: centerpoint[0], lat: centerpoint[1] }, | ||||
|                     provider: "OpenStreetMap", | ||||
|                     details: { | ||||
|                         isSpherical: false | ||||
|                         isSpherical: false, | ||||
|                     }, | ||||
|                     osmTags: { image } | ||||
|                     osmTags: { image }, | ||||
|                 }) | ||||
|             } | ||||
|         }) | ||||
|  | @ -170,7 +170,7 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher { | |||
|     public static readonly apiUrls: ReadonlyArray<string> = [ | ||||
|         "https://panoramax.openstreetmap.fr", | ||||
|         "https://api.panoramax.xyz", | ||||
|         "https://panoramax.mapcomplete.org" | ||||
|         "https://panoramax.mapcomplete.org", | ||||
|     ] | ||||
| 
 | ||||
|     constructor(url?: string, radius: number = 50) { | ||||
|  | @ -191,7 +191,7 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher { | |||
|             provider: "panoramax", | ||||
|             direction: imageData.properties["view:azimuth"], | ||||
|             osmTags: { | ||||
|                 panoramax: imageData.id | ||||
|                 panoramax: imageData.id, | ||||
|             }, | ||||
|             thumbUrl: imageData.assets.thumb.href, | ||||
|             date: new Date(imageData.properties.datetime).getTime(), | ||||
|  | @ -200,8 +200,8 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher { | |||
|             detailsUrl: imageData.id, | ||||
|             details: { | ||||
|                 isSpherical: | ||||
|                     imageData.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular" | ||||
|             } | ||||
|                     imageData.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular", | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -209,23 +209,26 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher { | |||
|         const radiusSettings = [ | ||||
|             { | ||||
|                 place_fov_tolerance: 180, | ||||
|                 radius: 15 | ||||
|                 radius: 15, | ||||
|             }, | ||||
|             { | ||||
|                 place_fov_tolerance: 180, | ||||
|                 radius: 25 | ||||
|                 radius: 25, | ||||
|             }, | ||||
|             { | ||||
|                 place_fov_tolerance: 90, | ||||
|                 radius: 50 | ||||
|             } | ||||
|                 radius: 50, | ||||
|             }, | ||||
|         ] | ||||
|         const promises: Promise<ImageData[]>[] = [] | ||||
|         const maxRadius = this._radius | ||||
|         let prevRadius = 0 | ||||
| 
 | ||||
|         const nearby = this._panoramax.search({ | ||||
|             bbox: new BBox([[lon - 0.0001, lat - 0.0001], [lon + 0.0001, lat + 0.0001]]).toLngLatFlat() | ||||
|             bbox: new BBox([ | ||||
|                 [lon - 0.0001, lat - 0.0001], | ||||
|                 [lon + 0.0001, lat + 0.0001], | ||||
|             ]).toLngLatFlat(), | ||||
|         }) | ||||
|         promises.push(nearby) // We do a nearby search with bbox, see https://source.mapcomplete.org/MapComplete/MapComplete/issues/2384
 | ||||
|         for (const radiusSetting of radiusSettings) { | ||||
|  | @ -233,7 +236,7 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher { | |||
|                 place: [lon, lat], | ||||
|                 place_distance: [prevRadius, Math.min(maxRadius, radiusSetting.radius)], | ||||
|                 place_fov_tolerance: radiusSetting.place_fov_tolerance, | ||||
|                 limit: 50 | ||||
|                 limit: 50, | ||||
|             }) | ||||
|             promises.push(promise) | ||||
|             prevRadius = radiusSetting.radius | ||||
|  | @ -277,7 +280,7 @@ class MapillaryFetcher implements ImageFetcher { | |||
|                 boundingBox.getWest(), | ||||
|                 boundingBox.getSouth(), | ||||
|                 boundingBox.getEast(), | ||||
|                 boundingBox.getNorth() | ||||
|                 boundingBox.getNorth(), | ||||
|             ].join(",") + | ||||
|             "&access_token=" + | ||||
|             encodeURIComponent(Constants.mapillary_client_token_v4) + | ||||
|  | @ -323,17 +326,17 @@ class MapillaryFetcher implements ImageFetcher { | |||
|                 coordinates: { lng: c[0], lat: c[1] }, | ||||
|                 thumbUrl: img.thumb_256_url, | ||||
|                 osmTags: { | ||||
|                     mapillary: img.id | ||||
|                     mapillary: img.id, | ||||
|                 }, | ||||
|                 details: { | ||||
|                     isSpherical: this._panoramas === "only" | ||||
|                     isSpherical: this._panoramas === "only", | ||||
|                 }, | ||||
| 
 | ||||
|                 detailsUrl: Mapillary.singleton.visitUrl(img, { lon, lat }), | ||||
|                 date: img.captured_at, | ||||
|                 license: "CC-BY-SA", | ||||
|                 author: img.creator.username, | ||||
|                 direction: img.compass_angle | ||||
|                 direction: img.compass_angle, | ||||
|             }) | ||||
|         } | ||||
|         return pics | ||||
|  | @ -349,7 +352,7 @@ export class CombinedFetcher { | |||
|         Imgur.apiUrl, | ||||
|         ...Imgur.supportingUrls, | ||||
|         ...MapillaryFetcher.apiUrls, | ||||
|         ...ImagesFromPanoramaxFetcher.apiUrls | ||||
|         ...ImagesFromPanoramaxFetcher.apiUrls, | ||||
|     ] | ||||
| 
 | ||||
|     constructor(radius: number, maxage: Date, indexedFeatures: IndexedFeatureSource) { | ||||
|  | @ -361,15 +364,15 @@ export class CombinedFetcher { | |||
|             new MapillaryFetcher({ | ||||
|                 max_images: 25, | ||||
|                 start_captured_at: maxage, | ||||
|                 panoramas: "only" | ||||
|                 panoramas: "only", | ||||
|             }), | ||||
|             new MapillaryFetcher({ | ||||
|                 max_images: 25, | ||||
|                 start_captured_at: maxage, | ||||
|                 panoramas: "no" | ||||
|                 panoramas: "no", | ||||
|             }), | ||||
|             new P4CImageFetcher("mapillary"), | ||||
|             new P4CImageFetcher("wikicommons") | ||||
|             new P4CImageFetcher("wikicommons"), | ||||
|         ].map((f) => new CachedFetcher(f)) | ||||
|     } | ||||
| 
 | ||||
|  | @ -387,7 +390,7 @@ export class CombinedFetcher { | |||
| 
 | ||||
|             const newList = [] | ||||
|             const seenIds = new Set<string>() | ||||
|             for (const p4CPicture of [...sink.data ?? [], ...pics]) { | ||||
|             for (const p4CPicture of [...(sink.data ?? []), ...pics]) { | ||||
|                 const id = p4CPicture.pictureUrl | ||||
|                 if (seenIds.has(id)) { | ||||
|                     continue | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue