diff --git a/LayerDefinition.ts b/LayerDefinition.ts
index f9429d4d2..ebdbb73eb 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 9f5dcd995..664da0070 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 514290741..438f46071 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 08b33c164..d0622d1ab 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 000000000..5785a114b
--- /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 1a4d103c4..163e53b3d 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 000000000..f86d7c76b
--- /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 6500b4411..e36a69be8 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 09701d888..3d22e6e3a 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 000000000..c1a0333ae
--- /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 a4d5b8521..919f080b5 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 000000000..4a601321e
--- /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 "";
+ }
+
+ 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 000000000..b47dceff7
--- /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 4c8ee8b3f..de28e0c99 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 8e650c669..5d470423f 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 8767197e5..f11109241 100644
--- a/index.html
+++ b/index.html
@@ -45,6 +45,8 @@
+
+