forked from MapComplete/MapComplete
		
	Visualize attribution in the attribution panel
This commit is contained in:
		
							parent
							
								
									a16745d0d1
								
							
						
					
					
						commit
						12d99e0323
					
				
					 10 changed files with 122 additions and 28 deletions
				
			
		|  | @ -452,5 +452,23 @@ export default class LayerConfig { | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public ExtractImages(): Set<string> { | ||||||
|  |         const parts: Set<string>[] = [] | ||||||
|  |         parts.push(...this.tagRenderings?.map(tr => tr.ExtractImages(false))) | ||||||
|  |         parts.push(...this.titleIcons?.map(tr => tr.ExtractImages(true))) | ||||||
|  |         parts.push(this.icon?.ExtractImages(true)) | ||||||
|  |         parts.push(...this.iconOverlays?.map(overlay => overlay.then.ExtractImages(true))) | ||||||
|  |         for (const preset of this.presets) { | ||||||
|  |             parts.push(new Set<string>(preset.description?.ExtractImages(false))) | ||||||
|  |         } | ||||||
|  |         | ||||||
|  |         const allIcons = new Set<string>(); | ||||||
|  |         for (const part of parts) { | ||||||
|  |             part?.forEach(allIcons.add, allIcons) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return allIcons; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -182,4 +182,14 @@ export default class LayoutConfig { | ||||||
|         custom.splice(0, 0, msg); |         custom.splice(0, 0, msg); | ||||||
|         return custom; |         return custom; | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     public ExtractImages() : Set<string>{ | ||||||
|  |         const icons = new Set<string>() | ||||||
|  |         for (const layer of this.layers) { | ||||||
|  |             layer.ExtractImages().forEach(icons.add, icons) | ||||||
|  |         } | ||||||
|  |         icons.add(this.icon) | ||||||
|  |         icons.add(this.socialImage) | ||||||
|  |         return icons | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -70,7 +70,7 @@ export default class TagRenderingConfig { | ||||||
|                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => |                 addExtraTags: json.freeform.addExtraTags?.map((tg, i) => | ||||||
|                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [] |                     FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [] | ||||||
|             } |             } | ||||||
|             if(json.freeform["extraTags"] !== undefined){ |             if (json.freeform["extraTags"] !== undefined) { | ||||||
|                 throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` |                 throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` | ||||||
|             } |             } | ||||||
|             if (this.freeform.key === undefined || this.freeform.key === "") { |             if (this.freeform.key === undefined || this.freeform.key === "") { | ||||||
|  | @ -254,5 +254,16 @@ export default class TagRenderingConfig { | ||||||
|         return undefined; |         return undefined; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public ExtractImages(isIcon: boolean): Set<string> { | ||||||
|  |          | ||||||
|  |         const usedIcons = new Set<string>() | ||||||
|  |         this.render?.ExtractImages(isIcon)?.forEach(usedIcons.add, usedIcons) | ||||||
|  | 
 | ||||||
|  |         for (const mapping of this.mappings ?? []) { | ||||||
|  |             mapping.then.ExtractImages(isIcon).forEach(usedIcons.add, usedIcons) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         return usedIcons; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -201,8 +201,6 @@ Data from OpenStreetMap | ||||||
| 
 | 
 | ||||||
| Background layer selection: curated by https://github.com/osmlab/editor-layer-index | Background layer selection: curated by https://github.com/osmlab/editor-layer-index | ||||||
| 
 | 
 | ||||||
| Images from Wikipedia/Wikimedia |  | ||||||
| 
 |  | ||||||
| https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg | https://commons.wikimedia.org/wiki/File:Camera_font_awesome.svg | ||||||
| Camera Icon, Dave Gandy, CC-BY-SA 3.0 | Camera Icon, Dave Gandy, CC-BY-SA 3.0 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,12 +6,18 @@ import State from "../../State"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
|  | import * as licenses from "../../assets/generated/license_info.json" | ||||||
|  | import SmallLicense from "../../Models/smallLicense"; | ||||||
|  | import {Icon} from "leaflet"; | ||||||
|  | import Img from "../Base/Img"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The attribution panel shown on mobile |  * The attribution panel shown on mobile | ||||||
|  */ |  */ | ||||||
| export default class AttributionPanel extends Combine { | export default class AttributionPanel extends Combine { | ||||||
| 
 | 
 | ||||||
|  |     private static LicenseObject = AttributionPanel.GenerateLicenses(); | ||||||
|  | 
 | ||||||
|     constructor(layoutToUse: UIEventSource<LayoutConfig>) { |     constructor(layoutToUse: UIEventSource<LayoutConfig>) { | ||||||
|         super([ |         super([ | ||||||
|             Translations.t.general.attribution.attributionContent, |             Translations.t.general.attribution.attributionContent, | ||||||
|  | @ -19,8 +25,42 @@ export default class AttributionPanel extends Combine { | ||||||
|             "<br/>", |             "<br/>", | ||||||
|             new Attribution(undefined, undefined, State.state.layoutToUse, undefined), |             new Attribution(undefined, undefined, State.state.layoutToUse, undefined), | ||||||
|             "<br/>", |             "<br/>", | ||||||
|             "<h4>", Translations.t.general.attribution.iconAttribution.title, "</h4>" |             Translations.t.general.attribution.iconAttribution.title.Clone().SetClass("font-bold pt-12 pb-3"), | ||||||
| 
 |             ...Array.from(layoutToUse.data.ExtractImages()).map(AttributionPanel.IconAttribution) | ||||||
|         ]); |         ]); | ||||||
|  |         this.SetClass("flex flex-col") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static IconAttribution(iconPath: string) { | ||||||
|  |         console.log("Attribution panel for ", iconPath) | ||||||
|  |         if (iconPath.startsWith("http")) { | ||||||
|  |             iconPath = "." + new URL(iconPath).pathname; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const license: SmallLicense = AttributionPanel.LicenseObject[iconPath] | ||||||
|  |         if (license == undefined) { | ||||||
|  |             return undefined; | ||||||
|  |         } | ||||||
|  |         if(license.license.indexOf("trivial")>=0){ | ||||||
|  |             return undefined; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return new Combine([ | ||||||
|  |             `<img src='${iconPath}' style="width: 50px; height: 50px; margin-right: 0.5em;">`, | ||||||
|  |             new Combine([ | ||||||
|  |                 new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"), | ||||||
|  |                 new Combine([license.license,  license.sources.length > 0 ? " - " : "",  | ||||||
|  |                     ...license.sources.map(link => `<a href='${link}'>${new URL(link).hostname}</a> `)]).SetClass("block") | ||||||
|  |             ]).SetClass("flex flex-col") | ||||||
|  |         ]).SetClass("flex") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static GenerateLicenses() { | ||||||
|  |         const allLicenses = {} | ||||||
|  |         for (const key in licenses) { | ||||||
|  |             const license: SmallLicense = licenses[key]; | ||||||
|  |             allLicenses[license.path] = license | ||||||
|  |         } | ||||||
|  |         return allLicenses; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -8,6 +8,8 @@ export class Translation extends UIElement { | ||||||
|     public static forcedLanguage = undefined; |     public static forcedLanguage = undefined; | ||||||
| 
 | 
 | ||||||
|     public readonly translations: object |     public readonly translations: object | ||||||
|  |     return | ||||||
|  |     allIcons; | ||||||
| 
 | 
 | ||||||
|     constructor(translations: object, context?: string) { |     constructor(translations: object, context?: string) { | ||||||
|         super(Locale.language) |         super(Locale.language) | ||||||
|  | @ -46,7 +48,7 @@ export class Translation extends UIElement { | ||||||
|     public SupportedLanguages(): string[] { |     public SupportedLanguages(): string[] { | ||||||
|         const langs = [] |         const langs = [] | ||||||
|         for (const translationsKey in this.translations) { |         for (const translationsKey in this.translations) { | ||||||
|             if(translationsKey === "#"){ |             if (translationsKey === "#") { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|             langs.push(translationsKey) |             langs.push(translationsKey) | ||||||
|  | @ -102,7 +104,6 @@ export class Translation extends UIElement { | ||||||
|         return new Translation(this.translations) |         return new Translation(this.translations) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     FirstSentence() { |     FirstSentence() { | ||||||
| 
 | 
 | ||||||
|         const tr = {}; |         const tr = {}; | ||||||
|  | @ -115,4 +116,23 @@ export class Translation extends UIElement { | ||||||
| 
 | 
 | ||||||
|         return new Translation(tr); |         return new Translation(tr); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public ExtractImages(isIcon = false): string[] { | ||||||
|  |         const allIcons: string[] = [] | ||||||
|  |         for (const key in this.translations) { | ||||||
|  |             const render = this.translations[key] | ||||||
|  | 
 | ||||||
|  |             if (isIcon) { | ||||||
|  |                 const icons = render.split(";").filter(part => part.match(/(\.svg|\.png|\.jpg)$/) != null) | ||||||
|  |                 allIcons.push(...icons) | ||||||
|  |             } else if(!Utils.runningFromConsole){ | ||||||
|  |                 // This might be a tagrendering containing some img as html
 | ||||||
|  |                 const htmlElement = document.createElement("div") | ||||||
|  |                 htmlElement.innerHTML = render | ||||||
|  |                 const images = Array.from(htmlElement.getElementsByTagName("img")).map(img => img.src) | ||||||
|  |                 allIcons.push(...images) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return allIcons.filter(icon => icon != undefined) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -27,7 +27,7 @@ | ||||||
|     <link rel="icon" href="./assets/svg/add.svg" sizes="any" type="image/svg+xml"> |     <link rel="icon" href="./assets/svg/add.svg" sizes="any" type="image/svg+xml"> | ||||||
|     <meta property="og:image" content="./assets/SocialImage.png"> |     <meta property="og:image" content="./assets/SocialImage.png"> | ||||||
|     <meta property="og:title" content="MapComplete - editable, thematic maps with OpenStreetMap"> |     <meta property="og:title" content="MapComplete - editable, thematic maps with OpenStreetMap"> | ||||||
|     <meta property="og:description" content="MapComplete is a platform to visualize OpenStreetMap on a specific topic and to easily contribute data back to it.">` |     <meta property="og:description" content="MapComplete is a platform to visualize OpenStreetMap on a specific topic and to easily contribute data back to it."> | ||||||
|      |      | ||||||
|     <link rel="apple-touch-icon" sizes="512x512" href="./assets/generated/svg_mapcomplete_logo512.png"> |     <link rel="apple-touch-icon" sizes="512x512" href="./assets/generated/svg_mapcomplete_logo512.png"> | ||||||
|     <link rel="apple-touch-icon" sizes="384x384" href="./assets/generated/svg_mapcomplete_logo384.png"> |     <link rel="apple-touch-icon" sizes="384x384" href="./assets/generated/svg_mapcomplete_logo384.png"> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", |     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", | ||||||
|     "start": "npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", |     "start": "npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", | ||||||
|     "test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts", |     "test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts && ts-node test/ImageAttribution.spec.ts", | ||||||
|     "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", |     "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", | ||||||
|     "generate:images": "ts-node scripts/generateIncludedImages.ts", |     "generate:images": "ts-node scripts/generateIncludedImages.ts", | ||||||
|     "generate:translations": "ts-node scripts/generateTranslations.ts", |     "generate:translations": "ts-node scripts/generateTranslations.ts", | ||||||
|  |  | ||||||
|  | @ -1,18 +1,9 @@ | ||||||
| import {Utils} from "../Utils"; | import {Utils} from "../Utils"; | ||||||
|  | import {lstatSync, readdirSync, readFileSync, writeFileSync} from "fs"; | ||||||
|  | import SmallLicense from "../Models/smallLicense"; | ||||||
| 
 | 
 | ||||||
| Utils.runningFromConsole = true; | Utils.runningFromConsole = true; | ||||||
| 
 | 
 | ||||||
| import {existsSync, mkdirSync, readdirSync, readFileSync, writeFile, writeFileSync, lstatSync} from "fs"; |  | ||||||
| import {LicenseInfo} from "../Logic/Web/Wikimedia"; |  | ||||||
| import {icon, Icon} from "leaflet"; |  | ||||||
| 
 |  | ||||||
| interface SmallLicense { |  | ||||||
|     path: string, |  | ||||||
|     authors: string[], |  | ||||||
|     license: string, |  | ||||||
|     sources: string[] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Sweeps the entire 'assets/' (except assets/generated) directory for image files and any 'license_info.json'-file. |  * Sweeps the entire 'assets/' (except assets/generated) directory for image files and any 'license_info.json'-file. | ||||||
|  * Checks that the license info is included for each of them and generates a compiles license_info.json for those |  * Checks that the license info is included for each of them and generates a compiles license_info.json for those | ||||||
|  | @ -179,6 +170,11 @@ console.log(`There are ${missingLicenses.length} licenses missing.`) | ||||||
| 
 | 
 | ||||||
| // shuffle(missingLicenses)
 | // shuffle(missingLicenses)
 | ||||||
| 
 | 
 | ||||||
|  | process.on('SIGINT', function() { | ||||||
|  |     console.log("Aborting... Bye!"); | ||||||
|  |     process.exit(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| let i = 1; | let i = 1; | ||||||
| for (const missingLicens of missingLicenses) { | for (const missingLicens of missingLicenses) { | ||||||
|     console.log(i + " / " + missingLicenses.length) |     console.log(i + " / " + missingLicenses.length) | ||||||
|  |  | ||||||
|  | @ -16,18 +16,19 @@ import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; | ||||||
| import {Tag} from "../Logic/Tags/Tag"; | import {Tag} from "../Logic/Tags/Tag"; | ||||||
| import {And} from "../Logic/Tags/And"; | import {And} from "../Logic/Tags/And"; | ||||||
| import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; | import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; | ||||||
|  | import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||||
|  | import AllKnownLayers from "../Customizations/AllKnownLayers"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| new T("ImageSearcher", [ | new T("ImageAttribution Tests", [ | ||||||
|     [ |     [ | ||||||
|         "Should find images", |         "Should find all the images", | ||||||
|         () => { |         () => { | ||||||
|             const tags = new UIEventSource({ |             const pumps = AllKnownLayers.sharedLayers["bike_repair_station"] | ||||||
|                 "mapillary": "https://www.mapillary.com/app/?pKey=bYH6FFl8LXAPapz4PNSh3Q" |             const expected = "./assets/layers/bike_repair_station/pump_example_manual.jpg" | ||||||
|             }); |             const images = pumps.ExtractImages(); | ||||||
|             const searcher = ImageSearcher.construct(tags) | 
 | ||||||
|             const result = searcher.data[0]; |             equal(images.length, 5, "The pump example was not found") | ||||||
|             equal(result.url, "https://www.mapillary.com/map/im/bYH6FFl8LXAPapz4PNSh3Q"); |  | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue