From 2acd53d1502b1928d2fd719c2831fc3198b03a05 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 25 Jun 2020 03:39:31 +0200 Subject: [PATCH] Add image upload functionality with imgur --- LayerDefinition.ts | 9 ++- Layers/CommonTagMappings.ts | 4 +- Layers/Playground.ts | 2 +- Logic/ImageSearcher.ts | 25 ++++++-- Logic/Imgur.ts | 66 ++++++++++++++++++++ Logic/OsmConnection.ts | 1 + Logic/OsmImageUploadHandler.ts | 70 ++++++++++++++++++++++ Logic/Wikimedia.ts | 2 + README.md | 11 ++++ UI/ImageUploadFlow.ts | 92 ++++++++++++++++++++++++++++ UI/UIElement.ts | 1 + UI/UIRadioButton.ts | 106 +++++++++++++++++++++++++++++++++ UI/VariableUIElement.ts | 17 ++++++ UI/VerticalCombine.ts | 3 + index.css | 4 ++ index.html | 2 + index.ts | 15 +++-- package-lock.json | 10 ++-- package.json | 1 + test.html | 1 + test.ts | 85 +++++++++++--------------- 21 files changed, 458 insertions(+), 69 deletions(-) create mode 100644 Logic/Imgur.ts create mode 100644 Logic/OsmImageUploadHandler.ts create mode 100644 UI/ImageUploadFlow.ts create mode 100644 UI/UIRadioButton.ts create mode 100644 UI/VariableUIElement.ts diff --git a/LayerDefinition.ts b/LayerDefinition.ts index f9429d4..ebdbb73 100644 --- a/LayerDefinition.ts +++ b/LayerDefinition.ts @@ -11,6 +11,8 @@ import {Tag, TagsFilter} from "./Logic/TagsFilter"; import {FilteredLayer} from "./Logic/FilteredLayer"; import {ImageCarousel} from "./UI/Image/ImageCarousel"; import {FixedUiElement} from "./UI/FixedUiElement"; +import {OsmImageUploadHandler} from "./Logic/OsmImageUploadHandler"; +import {UserDetails} from "./Logic/OsmConnection"; export class LayerDefinition { @@ -31,7 +33,7 @@ export class LayerDefinition { removeTouchingElements: boolean = false; - asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes): + asLayer(basemap: Basemap, allElements: ElementStorage, changes: Changes, userDetails: UserDetails): FilteredLayer { const self = this; @@ -53,9 +55,12 @@ export class LayerDefinition { } infoboxes.push(new ImageCarousel(tagsES)); - + infoboxes.push(new FixedUiElement("
")); + infoboxes.push(new OsmImageUploadHandler( + tagsES, userDetails, changes + ).getUI()); const qbox = new QuestionPicker(changes.asQuestions(self.questions), tagsES); infoboxes.push(qbox); diff --git a/Layers/CommonTagMappings.ts b/Layers/CommonTagMappings.ts index 9f5dcd9..664da00 100644 --- a/Layers/CommonTagMappings.ts +++ b/Layers/CommonTagMappings.ts @@ -26,8 +26,8 @@ export class CommonTagMappings { public static osmLink = new TagMappingOptions({ key: "id", mapping: { - "node/-1": "Over enkele momenten sturen we je punt naar OpenStreetMap" + "node/-1": "Over enkele momenten sturen we je punt naar OpenStreetMap" }, - template: " Op OSM" + template: " Op OSM" }) } \ No newline at end of file diff --git a/Layers/Playground.ts b/Layers/Playground.ts index 5142907..438f460 100644 --- a/Layers/Playground.ts +++ b/Layers/Playground.ts @@ -16,7 +16,7 @@ export class Playground extends LayerDefinition { this.removeContainedElements = true; this.minzoom = 13; - this.questions = [Quests.nameOf(this.name), Quests.accessNatureReserve]; + this.questions = [Quests.nameOf(this.name)]; this.style = this.generateStyleFunction(); this.elementsToShow = [ new TagMappingOptions({ diff --git a/Logic/ImageSearcher.ts b/Logic/ImageSearcher.ts index 08b33c1..d0622d1 100644 --- a/Logic/ImageSearcher.ts +++ b/Logic/ImageSearcher.ts @@ -19,7 +19,6 @@ export class ImageSearcher extends UIEventSource { constructor(tags: UIEventSource) { super([]); - // this.ListenTo(this._embeddedImages); this._tags = tags; @@ -27,7 +26,8 @@ export class ImageSearcher extends UIEventSource { this._wdItem.addCallback(() => { // Load the wikidata item, then detect usage on 'commons' let wikidataId = self._wdItem.data; - if (wikidataId.startsWith("Q")) { + // @ts-ignore + if (wikidataId.startsWith("Q")) { wikidataId = wikidataId.substr(1); } Wikimedia.GetWikiData(parseInt(wikidataId), (wd: Wikidata) => { @@ -44,14 +44,17 @@ export class ImageSearcher extends UIEventSource { this._commons.addCallback(() => { const commons: string = self._commons.data; + // @ts-ignore if (commons.startsWith("Category:")) { Wikimedia.GetCategoryFiles(commons, (images: ImagesInCategory) => { for (const image of images.images) { self.AddImage(image.filename); } }) - } else if (commons.startsWith("File:")) { - self.AddImage(commons); + } else { // @ts-ignore + if (commons.startsWith("File:")) { + self.AddImage(commons); + } } }); @@ -79,7 +82,7 @@ export class ImageSearcher extends UIEventSource { } private LoadImages(): void { - if(!this._activated){ + if (!this._activated) { return; } const imageTag = this._tags.data.image; @@ -90,6 +93,18 @@ export class ImageSearcher extends UIEventSource { } } + const image0 = this._tags.data["image:0"]; + if (image0 !== undefined) { + this.AddImage(image0); + } + let imageIndex = 1; + let imagei = this._tags.data["image:" + imageIndex]; + while (imagei !== undefined) { + this.AddImage(imagei); + imageIndex++; + imagei = this._tags.data["image:" + imageIndex]; + } + const wdItem = this._tags.data.wikidata; if (wdItem !== undefined) { this._wdItem.setData(wdItem); diff --git a/Logic/Imgur.ts b/Logic/Imgur.ts new file mode 100644 index 0000000..5785a11 --- /dev/null +++ b/Logic/Imgur.ts @@ -0,0 +1,66 @@ +import $ from "jquery" + +export class Imgur { + + + static uploadMultiple( + title: string, description: string, blobs: FileList, + handleSuccessfullUpload: ((imageURL: string) => void), + allDone: (() => void), + offset:number = 0) { + + if (blobs.length == offset) { + allDone(); + return; + } + const blob = blobs.item(offset); + const self = this; + this.uploadImage(title, description, blob, + (imageUrl) => { + handleSuccessfullUpload(imageUrl); + self.uploadMultiple( + title, description, blobs, + handleSuccessfullUpload, + allDone, + offset + 1); + } + ); + + + } + + static uploadImage(title: string, description: string, blob, + handleSuccessfullUpload: ((imageURL: string) => void)) { + + const apiUrl = 'https://api.imgur.com/3/image'; + const apiKey = '7070e7167f0a25a'; + + var settings = { + async: true, + crossDomain: true, + processData: false, + contentType: false, + type: 'POST', + url: apiUrl, + headers: { + Authorization: 'Client-ID ' + apiKey, + Accept: 'application/json', + }, + mimeType: 'multipart/form-data', + }; + var formData = new FormData(); + formData.append('image', blob); + formData.append("title", title); + formData.append("description", description) + // @ts-ignore + settings.data = formData; + + // Response contains stringified JSON + // Image URL available at response.data.link + $.ajax(settings).done(function (response) { + response = JSON.parse(response); + handleSuccessfullUpload(response.data.link); + }); + } + +} \ No newline at end of file diff --git a/Logic/OsmConnection.ts b/Logic/OsmConnection.ts index 1a4d103..163e53b 100644 --- a/Logic/OsmConnection.ts +++ b/Logic/OsmConnection.ts @@ -69,6 +69,7 @@ export class OsmConnection { let data = self.userDetails.data; data.loggedIn = true; + console.log(userInfo); data.name = userInfo.getAttribute('display_name'); data.csCount = userInfo.getElementsByTagName("changesets")[0].getAttribute("count"); data.img = userInfo.getElementsByTagName("img")[0].getAttribute("href"); diff --git a/Logic/OsmImageUploadHandler.ts b/Logic/OsmImageUploadHandler.ts new file mode 100644 index 0000000..f86d7c7 --- /dev/null +++ b/Logic/OsmImageUploadHandler.ts @@ -0,0 +1,70 @@ +/** + * Helps in uplaoding, by generating the rigth title, decription and by adding the tag to the changeset + */ +import {UIEventSource} from "../UI/UIEventSource"; +import {ImageUploadFlow} from "../UI/ImageUploadFlow"; +import {Changes} from "./Changes"; +import {UserDetails} from "./OsmConnection"; + +export class OsmImageUploadHandler { + private _tags: UIEventSource; + private _changeHandler: Changes; + private _userdetails: UserDetails; + + constructor(tags: UIEventSource, + userdetails: UserDetails, + changeHandler: Changes + ) { + if (tags === undefined || userdetails === undefined || changeHandler === undefined) { + throw "Something is undefined" + } + console.log(tags, changeHandler, userdetails) + this._tags = tags; + this._changeHandler = changeHandler; + this._userdetails = userdetails; + } + + private generateOptions(license: string) { + console.log(this) + console.log(this._tags, this._changeHandler, this._userdetails) + const tags = this._tags.data; + + const title = tags.name ?? "Unknown area"; + const description = [ + "author:" + this._userdetails.name, + "license:" + license, + "wikidata:" + tags.wikidata, + "osmid:" + tags.id, + "name:" + tags.name + ].join("\n"); + + const changes = this._changeHandler; + return { + title: title, + description: description, + handleURL: function (url) { + let freeIndex = 0; + while (tags["image:" + freeIndex] !== undefined) { + freeIndex++; + } + console.log("Adding image:" + freeIndex, url); + changes.addChange(tags.id, "image:" + freeIndex, url); + }, + allDone: function () { + changes.uploadAll(function () { + console.log("Writing changes...") + }); + } + } + } + + getUI(): ImageUploadFlow { + const self = this; + return new ImageUploadFlow(function (license) { + return self.generateOptions(license) + } + ); + } + + +} \ No newline at end of file diff --git a/Logic/Wikimedia.ts b/Logic/Wikimedia.ts index 6500b44..e36a69b 100644 --- a/Logic/Wikimedia.ts +++ b/Logic/Wikimedia.ts @@ -100,6 +100,8 @@ export class Wikimedia { handleWikidata(wd); }); } + + } diff --git a/README.md b/README.md index 09701d8..3d22e6e 100644 --- a/README.md +++ b/README.md @@ -44,5 +44,16 @@ When a map feature is clicked, a popup shows the information, images and questio The answers given by the user are sent (after a few seconds) to OpenStreetMap directly - if the user is logged in. If not logged in, the user is prompted to do so. +### Searching images +Images are fetched from: +- The OSM `image`, `image:0`, `image:1`, ... tags +- The OSM `wikimedia_commons` tags +- If wikidata is present, the wikidata `P18` (image) claim and, if a commons link is present, the commons images + +### Uploading images + +Images are uplaoded to imgur, as their API was way easier to handle. The URL is written into the changes + +The idea is that one in a while, the images are transfered to wikipedia \ No newline at end of file diff --git a/UI/ImageUploadFlow.ts b/UI/ImageUploadFlow.ts new file mode 100644 index 0000000..c1a0333 --- /dev/null +++ b/UI/ImageUploadFlow.ts @@ -0,0 +1,92 @@ +import {UIElement} from "./UIElement"; +import {UIEventSource} from "./UIEventSource"; +import {UIRadioButton} from "./UIRadioButton"; +import {VariableUiElement} from "./VariableUIElement"; +import $ from "jquery" +import {Imgur} from "../Logic/Imgur"; + +export class ImageUploadFlow extends UIElement { + private _licensePicker: UIRadioButton; + private _licenseExplanation: UIElement; + private _isUploading: UIEventSource = new UIEventSource(0) + private _uploadOptions: (license: string) => { title: string; description: string; handleURL: (url: string) => void; allDone: (() => void) }; + + constructor(uploadOptions: ((license: string) => + { + title: string, + description: string, + handleURL: ((url: string) => void), + allDone: (() => void) + }) + ) { + super(undefined); + this._uploadOptions = uploadOptions; + this.ListenTo(this._isUploading); + this._licensePicker = UIRadioButton.FromStrings( + [ + "CC-BY-SA", + "CC-BY", + "CC0" + ] + ); + const licenseExplanations = { + "CC-BY-SA": + "Creative Commonse met naamsvermelding en gelijk delen
" + + "Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden én ze afgeleide werken met deze licentie en attributie delen.", + "CC-BY": + "Creative Commonse met naamsvermelding
" + + "Je foto mag door iedereen gratis gebruikt worden, als ze je naam vermelden", + "CC0": + "Geen copyright
Je foto mag door iedereen voor alles gebruikt worden" + } + this._licenseExplanation = new VariableUiElement( + this._licensePicker.SelectedElementIndex.map((license) => { + return licenseExplanations[license?.value] + }) + ); + } + + + protected InnerRender(): string { + + if (this._isUploading.data > 0) { + return "Bezig met uploaden, nog " + this._isUploading.data + " foto's te gaan..." + } + + return "Foto's toevoegen
" + + 'Kies een licentie:
' + + this._licensePicker.Render() + + this._licenseExplanation.Render() + "
" + + '
' + ; + } + + InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + this._licensePicker.Update(); + const selector = document.getElementById('fileselector-' + this.id); + const self = this; + if (selector != null) { + selector.onchange = function (event) { + const files = $(this).get(0).files; + self._isUploading.setData(files.length); + + const opts = self._uploadOptions(self._licensePicker.SelectedElementIndex.data.value); + + Imgur.uploadMultiple(opts.title, opts.description, files, + function (url) { + console.log("File saved at", url); + self._isUploading.setData(self._isUploading.data - 1); + opts.handleURL(url); + }, + function () { + console.log("All uploads completed") + opts.allDone(); + } + ) + } + } + } + + +} \ No newline at end of file diff --git a/UI/UIElement.ts b/UI/UIElement.ts index a4d5b85..919f080 100644 --- a/UI/UIElement.ts +++ b/UI/UIElement.ts @@ -47,6 +47,7 @@ export abstract class UIElement { let element = document.getElementById(divId); element.innerHTML = this.Render(); this.Update(); + return this; } protected abstract InnerRender(): string; diff --git a/UI/UIRadioButton.ts b/UI/UIRadioButton.ts new file mode 100644 index 0000000..4a60132 --- /dev/null +++ b/UI/UIRadioButton.ts @@ -0,0 +1,106 @@ +import {UIElement} from "./UIElement"; +import {UIEventSource} from "./UIEventSource"; +import {FixedUiElement} from "./FixedUiElement"; +import $ from "jquery" + +export class UIRadioButton extends UIElement { + + public readonly SelectedElementIndex: UIEventSource<{ index: number, value: string }> + = new UIEventSource<{ index: number, value: string }>(null); + + private readonly _elements: UIEventSource<{ element: UIElement, value: string }[]> + + constructor(elements: UIEventSource<{ element: UIElement, value: string }[]>) { + super(elements); + this._elements = elements; + } + + static FromStrings(choices: string[]): UIRadioButton { + const wrapped = []; + for (const choice of choices) { + wrapped.push({ + element: new FixedUiElement(choice), + value: choice + }); + } + return new UIRadioButton(new UIEventSource<{ element: UIElement, value: string }[]>(wrapped)) + } + + private IdFor(i) { + return 'radio-' + this.id + '-' + i; + } + + protected InnerRender(): string { + + let body = ""; + let i = 0; + for (const el of this._elements.data) { + const uielement = el.element; + const value = el.value; + + const htmlElement = + '' + + '' + + '
'; + body += htmlElement; + + i++; + } + + return "
" + body + "
"; + } + + InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + const self = this; + + function checkButtons() { + for (let i = 0; i < self._elements.data.length; i++) { + const el = document.getElementById(self.IdFor(i)); + // @ts-ignore + if (el.checked) { + var v = {index: i, value: self._elements.data[i].value} + self.SelectedElementIndex.setData(v); + } + } + } + + + const el = document.getElementById(this.id); + el.addEventListener("change", + function () { + checkButtons(); + } + ); + + if (this.SelectedElementIndex.data == null) { + const el = document.getElementById(this.IdFor(0)); + el.checked = true; + checkButtons(); + } else { + + // We check that what is selected matches the previous rendering + var checked = -1; + var expected = -1 + for (let i = 0; i < self._elements.data.length; i++) { + const el = document.getElementById(self.IdFor(i)); + // @ts-ignore + if (el.checked) { + checked = i; + } + if (el.value === this.SelectedElementIndex.data.value) { + expected = i; + } + } + if (expected != checked) { + const el = document.getElementById(this.IdFor(expected)); + // @ts-ignore + el.checked = true; + } + } + + + } + + +} \ No newline at end of file diff --git a/UI/VariableUIElement.ts b/UI/VariableUIElement.ts new file mode 100644 index 0000000..b47dcef --- /dev/null +++ b/UI/VariableUIElement.ts @@ -0,0 +1,17 @@ +import {UIElement} from "./UIElement"; +import {UIEventSource} from "./UIEventSource"; + +export class VariableUiElement extends UIElement { + private _html: UIEventSource; + + constructor(html: UIEventSource) { + super(html); + this._html = html; + } + + protected InnerRender(): string { + return this._html.data; + } + + +} \ No newline at end of file diff --git a/UI/VerticalCombine.ts b/UI/VerticalCombine.ts index 4c8ee8b..de28e0c 100644 --- a/UI/VerticalCombine.ts +++ b/UI/VerticalCombine.ts @@ -18,6 +18,9 @@ export class VerticalCombine extends UIElement { return html; } InnerUpdate(htmlElement: HTMLElement) { + for (const element of this._elements){ + element.Update(); + } } Activate() { diff --git a/index.css b/index.css index 8e650c6..5d47042 100644 --- a/index.css +++ b/index.css @@ -235,4 +235,8 @@ body { .hidden { display: none; +} + +.osmlink{ + font-size: xx-small; } \ No newline at end of file diff --git a/index.html b/index.html index 8767197..f111092 100644 --- a/index.html +++ b/index.html @@ -45,6 +45,8 @@ + + diff --git a/test.ts b/test.ts index 5abad71..4d3a78b 100644 --- a/test.ts +++ b/test.ts @@ -1,63 +1,50 @@ -import {UIEventSource} from "./UI/UIEventSource"; -import {WikimediaImage} from "./UI/Image/WikimediaImage"; -import {ImagesInCategory, Wikidata, Wikimedia} from "./Logic/Wikimedia"; -import {UIElement} from "./UI/UIElement"; -import {SlideShow} from "./UI/SlideShow"; -import {ImageSearcher} from "./Logic/ImageSearcher"; -import {KnownSet} from "./Layers/KnownSet"; -import {Park} from "./Layers/Park"; import {FixedUiElement} from "./UI/FixedUiElement"; +import $ from "jquery" +import {Imgur} from "./Logic/Imgur"; +import {ImageUploadFlow} from "./UI/ImageUploadFlow"; +import {UserDetails} from "./Logic/OsmConnection"; +import {UIEventSource} from "./UI/UIEventSource"; +import {UIRadioButton} from "./UI/UIRadioButton"; +import {UIElement} from "./UI/UIElement"; +var tags = { + "name": "Astridpark Brugge", + "wikidata":"Q1234", + "leisure":"park" +} -let properties = { - image: "https://www.designindaba.com/sites/default/files/node/news/21663/buteparkcardiff.jpg", - wikimedia_commons: "File:Boekenkast Sint-Lodewijks.jpg", - wikidata: "Q2763812"}; -let tagsES = new UIEventSource(properties); +var userdetails = new UserDetails() +userdetails.loggedIn = true; +userdetails.name = "Pietervdvn"; -let searcher = new ImageSearcher(tagsES); +new ImageUploadFlow( +).AttachTo("maindiv") //*/ -const uiElements = searcher.map((imageURLS : string[]) => { - const uiElements : UIElement[] = []; - for (const url of imageURLS) { - uiElements.push(ImageSearcher.CreateImageElement(url)); - } - return uiElements; -}); -new SlideShow( - new FixedUiElement("Afbeeldingen"), - uiElements, - new FixedUiElement("Geen afbeeldingen gevonden...") - -).AttachTo("maindiv"); -searcher.Activate(); /* -const imageSource = new UIEventSource("https://commons.wikimedia.org/wiki/Special:FilePath/File:Pastoor van Haeckeplantsoen, Brugge (1).JPG?width=1000"); +$('document').ready(function () { + $('input[type=file]').on('change', function () { + var $files = $(this).get(0).files; -// new SimpleImageElement(imageSource).AttachTo("maindiv"); -const wikimediaImageSource = new UIEventSource("File:Deelboekenkast_rouppeplein.jpg"); -// new WikimediaImage(wikimediaImageSource).AttachTo("maindiv"); + if ($files.length) { + // Reject big files + if ($files[0].size > $(this).data('max-size') * 1024) { + console.log('Please select a smaller file'); + return false; + } -const wdItem = 2763812; -Wikimedia.GetWikiData(wdItem, (wd : Wikidata) => { + // Begin file upload + console.log('Uploading file to Imgur..'); - const category = wd.commonsWiki; - Wikimedia.GetCategoryFiles(category, (images: ImagesInCategory) => { - - const imageElements: UIElement[] = []; - for (const image of images.images) { - const wikimediaImageSource = new UIEventSource(image.filename); - var uielem = new WikimediaImage(wikimediaImageSource); - imageElements.push(uielem); + const imgur = new Imgur(); + imgur.uploadImage("KorenBloem", "Een korenbloem, ergens", $files[0], + (url) => { + console.log("URL: ", url); + }) } - var slides = new UIEventSource(imageElements); - new SlideShow(slides).AttachTo("maindiv"); - }) -}) -*/ - - + }); +}); +*/ \ No newline at end of file