From a89d303ecd0937573b56c4dec6b6a3df3caea57b Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sun, 3 Oct 2021 01:38:57 +0200 Subject: [PATCH] Further stabilization of wikipedia box --- InitUiElements.ts | 1 - Logic/ImageProviders/WikidataImageProvider.ts | 7 +- Logic/Osm/Overpass.ts | 3 +- Logic/UIEventSource.ts | 24 +++++- Logic/Web/Wikidata.ts | 51 +++++++++--- Logic/Web/Wikipedia.ts | 1 - UI/BigComponents/AllDownloads.ts | 8 +- UI/BigComponents/FilterView.ts | 1 - UI/ExportPDF.ts | 10 +-- UI/WikipediaBox.ts | 82 +++++++++---------- langs/en.json | 1 + package.json | 2 +- scripts/generateCache.ts | 54 +++++------- test/TestAll.ts | 4 +- test/Wikidata.spec.test.ts | 29 +++++++ 15 files changed, 169 insertions(+), 109 deletions(-) create mode 100644 test/Wikidata.spec.test.ts diff --git a/InitUiElements.ts b/InitUiElements.ts index 94500ec34..8fafd7677 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -150,7 +150,6 @@ export class InitUiElements { if (userDetails === undefined) { return false; } - console.log("Adding home location of ", userDetails) const home = userDetails.home; if (home === undefined) { return userDetails.loggedIn; // If logged in, the home is not set and we unregister. If not logged in, we stay registered if a login still comes diff --git a/Logic/ImageProviders/WikidataImageProvider.ts b/Logic/ImageProviders/WikidataImageProvider.ts index ea333dfbd..f24f58aca 100644 --- a/Logic/ImageProviders/WikidataImageProvider.ts +++ b/Logic/ImageProviders/WikidataImageProvider.ts @@ -23,7 +23,10 @@ export class WikidataImageProvider extends ImageProvider { } public async ExtractUrls(key: string, value: string): Promise[]> { - const entity = await Wikidata.LoadWikidataEntry(value) + const entity = await Wikidata.LoadWikidataEntryAsync(value) + if(entity === undefined){ + return [] + } const allImages : Promise[] = [] // P18 is the claim 'depicted in this image' @@ -32,7 +35,7 @@ export class WikidataImageProvider extends ImageProvider { allImages.push(...promises) } - const commons =entity.wikisites.get("commons") + const commons = entity.commons if (commons !== undefined) { const promises = await WikimediaImageProvider.singleton.ExtractUrls(undefined , commons) allImages.push(...promises) diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index 47b92e278..97911a3b6 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -18,7 +18,8 @@ export class Overpass { private _relationTracker: RelationsTracker; - constructor(filter: TagsFilter, extraScripts: string[], + constructor(filter: TagsFilter, + extraScripts: string[], interpreterUrl: string, timeout: UIEventSource, relationTracker: RelationsTracker, diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index c30363f97..10922ca14 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -1,4 +1,5 @@ import {Utils} from "../Utils"; +import * as Events from "events"; export class UIEventSource { @@ -32,14 +33,14 @@ export class UIEventSource { return []; } - public static flatten(source: UIEventSource>, possibleSources: UIEventSource[]): UIEventSource { + public static flatten(source: UIEventSource>, possibleSources?: UIEventSource[]): UIEventSource { const sink = new UIEventSource(source.data?.data); source.addCallback((latestData) => { sink.setData(latestData?.data); }); - for (const possibleSource of possibleSources) { + for (const possibleSource of possibleSources ?? []) { possibleSource?.addCallback(() => { sink.setData(source.data?.data); }) @@ -186,6 +187,25 @@ export class UIEventSource { } } + /** + * Monadic bind function + */ + public bind(f: ((t: T) => UIEventSource)): UIEventSource{ + const sink = new UIEventSource( undefined ) + const seenEventSources = new Set>(); + this.addCallbackAndRun(data => { + const eventSource = f(data) + if(eventSource === undefined){ + sink.setData(undefined) + }else if(!seenEventSources.has(eventSource)){ + eventSource.addCallbackAndRun(mappedData => sink.setData(mappedData)) + seenEventSources.add(eventSource) + } + }) + + return sink; + } + /** * Monoidal map: * Given a function 'f', will construct a new UIEventSource where the contents will always be "f(this.data)' diff --git a/Logic/Web/Wikidata.ts b/Logic/Web/Wikidata.ts index 3b60a2e6b..ae981ee82 100644 --- a/Logic/Web/Wikidata.ts +++ b/Logic/Web/Wikidata.ts @@ -1,4 +1,5 @@ import {Utils} from "../../Utils"; +import {UIEventSource} from "../UIEventSource"; export interface WikidataResponse { @@ -62,15 +63,23 @@ export default class Wikidata { } } - /** - * Loads a wikidata page - * @returns the entity of the given value - */ - public static async LoadWikidataEntry(value: string | number): Promise { - const wikidataUrl = "https://www.wikidata.org/wiki/" - if (typeof value === "number") { - value = "Q" + value + private static readonly _cache = new Map>() + public static LoadWikidataEntry(value: string | number): UIEventSource<{success: WikidataResponse} | {error: any}> { + const key = this.ExtractKey(value) + const cached = Wikidata._cache.get(key) + if(cached !== undefined){ + return cached } + const src = UIEventSource.FromPromiseWithErr(Wikidata.LoadWikidataEntryAsync(key)) + Wikidata._cache.set(key, src) + return src; + } + + private static ExtractKey(value: string | number) : number{ + if (typeof value === "number") { + return value + } + const wikidataUrl = "https://www.wikidata.org/wiki/" if (value.startsWith(wikidataUrl)) { value = value.substring(wikidataUrl.length) } @@ -78,12 +87,30 @@ export default class Wikidata { // Probably some random link in the image field - we skip it return undefined } - if (!value.startsWith("Q")) { - value = "Q" + value + if (value.startsWith("Q")) { + value = value.substring(1) } - const url = "https://www.wikidata.org/wiki/Special:EntityData/" + value + ".json"; + const n = Number(value) + if(isNaN(n)){ + return undefined + } + return n; + } + + /** + * Loads a wikidata page + * @returns the entity of the given value + */ + public static async LoadWikidataEntryAsync(value: string | number): Promise { + const id = Wikidata.ExtractKey(value) + if(id === undefined){ + console.warn("Could not extract a wikidata entry from", value) + return undefined; + } + console.log("Requesting wikidata with id", id) + const url = "https://www.wikidata.org/wiki/Special:EntityData/Q" + id + ".json"; const response = await Utils.downloadJson(url) - return Wikidata.ParseResponse(response.entities[value]); + return Wikidata.ParseResponse(response.entities["Q" + id]) } } \ No newline at end of file diff --git a/Logic/Web/Wikipedia.ts b/Logic/Web/Wikipedia.ts index 34bde4fc5..dedfa144d 100644 --- a/Logic/Web/Wikipedia.ts +++ b/Logic/Web/Wikipedia.ts @@ -61,7 +61,6 @@ export default class Wikipedia { const links = Array.from(content.getElementsByTagName("a")) - console.log("Links are", links) // Rewrite relative links to absolute links + open them in a new tab links.filter(link => link.getAttribute("href")?.startsWith("/") ?? false). forEach(link => { diff --git a/UI/BigComponents/AllDownloads.ts b/UI/BigComponents/AllDownloads.ts index 2170eb496..3df26f53d 100644 --- a/UI/BigComponents/AllDownloads.ts +++ b/UI/BigComponents/AllDownloads.ts @@ -39,13 +39,13 @@ export default class AllDownloads extends ScrollableFullScreen { const loading = Svg.loading_svg().SetClass("animate-rotate"); + const dloadTrans = Translations.t.general.download const icon = new Toggle(loading, Svg.floppy_ui(), isExporting); const text = new Toggle( - new FixedUiElement("Exporting..."), - + dloadTrans.exporting.Clone(), new Combine([ - Translations.t.general.download.downloadAsPdf.Clone().SetClass("font-bold"), - Translations.t.general.download.downloadAsPdfHelper.Clone()] + dloadTrans.downloadAsPdf.Clone().SetClass("font-bold"), + dloadTrans.downloadAsPdfHelper.Clone()] ).SetClass("flex flex-col") .onClick(() => { generatePdf() diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index ffcdbc3c9..336d77405 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -121,7 +121,6 @@ export default class FilterView extends VariableUiElement { listFilterElements.map((input) => input[1].data) ); - console.log(listTagsFilters, oldValue) flayer.appliedFilters.setData(listTagsFilters); }) ); diff --git a/UI/ExportPDF.ts b/UI/ExportPDF.ts index 54879b9b2..1bb57fb1c 100644 --- a/UI/ExportPDF.ts +++ b/UI/ExportPDF.ts @@ -32,7 +32,7 @@ export default class ExportPDF { private readonly mapH = 210; private readonly scaling = 2 private readonly freeDivId: string; - private readonly _layout: UIEventSource; + private readonly _layout: LayoutConfig; private _screenhotTaken = false; constructor( @@ -41,7 +41,7 @@ export default class ExportPDF { location: UIEventSource, background?: UIEventSource features: FeaturePipeline, - layout: UIEventSource + layout: LayoutConfig } ) { @@ -87,7 +87,6 @@ export default class ExportPDF { minimap.leafletMap .addCallbackAndRunD(leaflet => { const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2)) options.features.GetTilesPerLayerWithin(bounds, tile => { - console.log("REndering", tile.name) new ShowDataLayer( { features: tile, @@ -108,13 +107,13 @@ export default class ExportPDF { } private async CreatePdf(leaflet: L.Map) { + console.log("PDF creation started") const t = Translations.t.general.pdf; - const layout = this._layout.data + const layout = this._layout const screenshotter = new SimpleMapScreenshoter(); //minimap op index.html -> hidden daar alles op doen en dan weg //minimap - leaflet map ophalen - boundaries ophalen - State.state.featurePipeline screenshotter.addTo(leaflet); - console.log("Taking screenshot") let doc = new jsPDF('landscape'); @@ -164,7 +163,6 @@ export default class ExportPDF { const imgSource = layout.icon const imgType = imgSource.substr(imgSource.lastIndexOf(".") + 1); img.src = imgSource - console.log(imgType) if (imgType.toLowerCase() === "svg") { new FixedUiElement("").AttachTo(this.freeDivId) diff --git a/UI/WikipediaBox.ts b/UI/WikipediaBox.ts index f5c582ed2..af3e27c7e 100644 --- a/UI/WikipediaBox.ts +++ b/UI/WikipediaBox.ts @@ -8,78 +8,69 @@ import BaseUIElement from "./BaseUIElement"; import Title from "./Base/Title"; import Translations from "./i18n/Translations"; import Svg from "../Svg"; -import Wikidata from "../Logic/Web/Wikidata"; +import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"; import Locale from "./i18n/Locale"; +import Toggle from "./Input/Toggle"; -export default class WikipediaBox extends Combine { +export default class WikipediaBox extends Toggle { - private static async ExtractWikiPages(wikidata): Promise> { - return (await Wikidata.LoadWikidataEntry(wikidata)).wikisites - } - - - private static _cache = new Map() constructor(wikidataId: string | UIEventSource) { const wp = Translations.t.general.wikipedia; - if(typeof wikidataId === "string"){ + if (typeof wikidataId === "string") { wikidataId = new UIEventSource(wikidataId) } - - const knownPages = new UIEventSource<{success:Map}|{error:any}>(undefined) - - wikidataId.addCallbackAndRunD(wikidataId => { - WikipediaBox.ExtractWikiPages(wikidataId).then(pages => { - knownPages.setData({success:pages}) - }).catch(err=> { - knownPages.setData({error: err}) - }) - }) - - const cachedPages = new Map() - - const contents = new VariableUiElement( - knownPages.map(pages => { - if (pages === undefined) { + + const wikibox = wikidataId + .bind(id => { + console.log("Wikidata is", id) + if(id === undefined){ + return undefined + } + console.log("Initing load WIkidataentry with id", id) + return Wikidata.LoadWikidataEntry(id); + }) + .map(maybewikidata => { + if (maybewikidata === undefined) { return new Loading(wp.loading.Clone()) } - if (pages["error"] !== undefined) { + if (maybewikidata["error"] !== undefined) { return wp.failed.Clone().SetClass("alert p-4") } - const dict: Map = pages["success"] + const wikidata = maybewikidata["success"] + console.log("Got wikidata response", wikidata) + if (wikidata.wikisites.size === 0) { + return wp.noWikipediaPage.Clone() + } - const preferredLanguage = [Locale.language.data, "en", Array.from(dict.keys())[0]] + const preferredLanguage = [Locale.language.data, "en", Array.from(wikidata.wikisites.keys())[0]] let language let pagetitle; let i = 0 do { language = preferredLanguage[i] - pagetitle = dict.get(language) + pagetitle = wikidata.wikisites.get(language) i++; - if(i >= preferredLanguage.length){ - return wp.noWikipediaPage.Clone() - } } while (pagetitle === undefined) - - if(cachedPages.has(language)){ - return cachedPages.get(language) - } - - const page = WikipediaBox.createContents(pagetitle, language); - cachedPages.set(language, page) - return page + return WikipediaBox.createContents(pagetitle, language) }, [Locale.language]) + + + const contents = new VariableUiElement( + wikibox ).SetClass("overflow-auto normal-background rounded-lg") - super([ + const mainContent = new Combine([ new Combine([Svg.wikipedia_ui().SetStyle("width: 1.5rem").SetClass("mr-3"), new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2)]).SetClass("flex"), - contents]) - - this - .SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col") + contents]).SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col") + super( + mainContent, + undefined, + wikidataId.map(id => id !== undefined) + ) } /** @@ -103,6 +94,7 @@ export default class WikipediaBox extends Combine { return new FixedUiElement(htmlContent["success"]).SetClass("wikipedia-article") } if (htmlContent["error"]) { + console.warn("Loading wikipage failed due to", htmlContent["error"]) return wp.failed.Clone().SetClass("alert p-4") } diff --git a/langs/en.json b/langs/en.json index 306ea8e80..650963864 100644 --- a/langs/en.json +++ b/langs/en.json @@ -174,6 +174,7 @@ "downloadAsPdf": "Download a PDF of the current map", "downloadAsPdfHelper": "Ideal to print the current map", "downloadGeojson": "Download visible data as geojson", + "exporting": "Exporting...", "downloadGeoJsonHelper": "Compatible with QGIS, ArcGIS, ESRI, ...", "downloadCSV": "Download visible data as CSV", "downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, …", diff --git a/package.json b/package.json index 2505a2740..d7485c874 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "npm run start:prepare && npm-run-all --parallel start:parallel:*", "strt": "npm run start:prepare && npm run start:parallel:parcel", "start:prepare": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory", - "start:parallel:parcel": "parcel *.html UI/** Logic/** assets/*.json assets/svg/* assets/generated/* assets/layers/*/*.svg assets/tagRenderings/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", + "start:parallel:parcel": "parcel *.html UI/** Logic/** assets/*.json assets/svg/* assets/generated/* assets/layers/*/*.svg assets/layers/*/*.jpg assets/layers/*/*.png assets/tagRenderings/*.json assets/themes/*/*.svg assets/themes/*/*.jpg assets/themes/*/*.png vendor/* vendor/*/*", "start:parallel:tailwindcli": "tailwindcss -i index.css -o css/index-tailwind-output.css --watch", "test": "ts-node test/TestAll.ts", "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index a5a915bde..e207cf67e 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -2,9 +2,6 @@ * Generates a collection of geojson files based on an overpass query for a given theme */ import {Utils} from "../Utils"; - -Utils.runningFromConsole = true - import {Overpass} from "../Logic/Osm/Overpass"; import {existsSync, readFileSync, writeFileSync} from "fs"; import {TagsFilter} from "../Logic/Tags/TagsFilter"; @@ -22,12 +19,13 @@ import FilteredLayer from "../Models/FilteredLayer"; import FeatureSource, {FeatureSourceForLayer} from "../Logic/FeatureSource/FeatureSource"; import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource"; +import Constants from "../Models/Constants"; ScriptUtils.fixUtils() -function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker) { +function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker, backend: string) { let filters: TagsFilter[] = []; let extraScripts: string[] = []; for (const layer of theme.layers) { @@ -58,7 +56,7 @@ function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTra if (filters.length + extraScripts.length === 0) { throw "Nothing to download! The theme doesn't declare anything to download" } - return new Overpass(new Or(filters), extraScripts, new UIEventSource("https://overpass.kumi.systems/api/interpreter"), //https://overpass-api.de/api/interpreter"), + return new Overpass(new Or(filters), extraScripts, backend, new UIEventSource(60), relationTracker); } @@ -71,7 +69,7 @@ function geoJsonName(targetDir: string, x: number, y: number, z: number): string } /// Downloads the given feature and saves them to disk -async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/* : {failed: number, skipped :number} */ { +async function downloadRaw(targetdir: string, r: TileRange, theme: LayoutConfig, relationTracker: RelationsTracker)/* : {failed: number, skipped :number} */ { let downloaded = 0 let failed = 0 let skipped = 0 @@ -93,35 +91,28 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/ east: Math.max(boundsArr[0][1], boundsArr[1][1]), west: Math.min(boundsArr[0][1], boundsArr[1][1]) } + const overpass = createOverpassObject(theme, relationTracker, Constants.defaultOverpassUrls[(downloaded + failed) % Constants.defaultOverpassUrls.length]) const url = overpass.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]") - await ScriptUtils.DownloadJSON(url) - .then(json => { - if (json.elements.length === 0) { - console.log("Got an empty response!") - if ((json.remark ?? "").startsWith("runtime error")) { - console.error("Got a runtime error: ", json.remark) - failed++; - return - } + try { - } - - - console.log("Got the response - writing to ", filename) - writeFileSync(filename, JSON.stringify(json, null, " ")); + const json = await ScriptUtils.DownloadJSON(url) + if (json.elements.length === 0) { + console.log("Got an empty response!") + if ((json.remark ?? "").startsWith("runtime error")) { + console.error("Got a runtime error: ", json.remark) + failed++; } - ) - .catch(err => { - console.log(url) - console.log("Could not download - probably hit the rate limit; waiting a bit. (" + err + ")") - failed++; - return ScriptUtils.sleep(60000).then(() => console.log("Waiting is done")) - }) - if (x < r.xend || y < r.yend) { - console.debug("Cooling down 10s") - await ScriptUtils.sleep(10000) + } else { + console.log("Got the response - writing to ", filename) + writeFileSync(filename, JSON.stringify(json, null, " ")); + } + } catch (err) { + console.log(url) + console.log("Could not download - probably hit the rate limit; waiting a bit. (" + err + ")") + failed++; + await ScriptUtils.sleep(1000) } } } @@ -291,11 +282,10 @@ async function main(args: string[]) { return } const relationTracker = new RelationsTracker() - const overpass = createOverpassObject(theme, relationTracker) let failed = 0; do { - const cachingResult = await downloadRaw(targetdir, tileRange, overpass) + const cachingResult = await downloadRaw(targetdir, tileRange, theme, relationTracker) failed = cachingResult.failed if (failed > 0) { await ScriptUtils.sleep(30000) diff --git a/test/TestAll.ts b/test/TestAll.ts index 6123681fb..bdb92afdb 100644 --- a/test/TestAll.ts +++ b/test/TestAll.ts @@ -10,6 +10,7 @@ import RelationSplitHandlerSpec from "./RelationSplitHandler.spec"; import SplitActionSpec from "./SplitAction.spec"; import {Utils} from "../Utils"; import TileFreshnessCalculatorSpec from "./TileFreshnessCalculator.spec"; +import WikidataSpecTest from "./Wikidata.spec.test"; ScriptUtils.fixUtils() @@ -23,7 +24,8 @@ const allTests = [ new UnitsSpec(), new RelationSplitHandlerSpec(), new SplitActionSpec(), - new TileFreshnessCalculatorSpec() + new TileFreshnessCalculatorSpec(), + new WikidataSpecTest() ] Utils.externalDownloadFunction = async (url) => { diff --git a/test/Wikidata.spec.test.ts b/test/Wikidata.spec.test.ts new file mode 100644 index 000000000..000a77217 --- /dev/null +++ b/test/Wikidata.spec.test.ts @@ -0,0 +1,29 @@ +import Wikidata from "../Logic/Web/Wikidata"; +import * as assert from "assert"; +import {equal} from "assert"; +import T from "./TestHelper"; +import {Utils} from "../Utils"; + +export default class WikidataSpecTest extends T { + constructor() { + super("Wikidata", + [ + ["download wikidata", + async () => { + + Utils.injectJsonDownloadForTests( + "https://www.wikidata.org/wiki/Special:EntityData/Q14517013.json" , + {"entities":{"Q14517013":{"pageid":16187848,"ns":0,"title":"Q14517013","lastrevid":1408823680,"modified":"2021-04-26T07:35:01Z","type":"item","id":"Q14517013","labels":{"nl":{"language":"nl","value":"Vredesmolen"},"en":{"language":"en","value":"Peace Mill"}},"descriptions":{"nl":{"language":"nl","value":"molen in West-Vlaanderen"}},"aliases":{},"claims":{"P625":[{"mainsnak":{"snaktype":"value","property":"P625","hash":"d86538f14e8cca00bbf30fb029829aacbc6903a0","datavalue":{"value":{"latitude":50.99444,"longitude":2.92528,"altitude":null,"precision":0.0001,"globe":"http://www.wikidata.org/entity/Q2"},"type":"globecoordinate"},"datatype":"globe-coordinate"},"type":"statement","id":"Q14517013$DBFBFD69-F54D-4C92-A7F4-A44F876E5776","rank":"normal","references":[{"hash":"732ec1c90a6f0694c7db9a71bf09fe7f2b674172","snaks":{"P143":[{"snaktype":"value","property":"P143","hash":"9123b0de1cc9c3954366ba797d598e4e1ea4146f","datavalue":{"value":{"entity-type":"item","numeric-id":10000,"id":"Q10000"},"type":"wikibase-entityid"},"datatype":"wikibase-item"}]},"snaks-order":["P143"]}]}],"P17":[{"mainsnak":{"snaktype":"value","property":"P17","hash":"c2859f311753176d6bdfa7da54ceeeac7acb52c8","datavalue":{"value":{"entity-type":"item","numeric-id":31,"id":"Q31"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q14517013$C12E4DA5-44E1-41ED-BF3D-C84381246429","rank":"normal"}],"P18":[{"mainsnak":{"snaktype":"value","property":"P18","hash":"af765166ecaa7d01ea800812b5b356886b8849a0","datavalue":{"value":"Klerken Vredesmolen R01.jpg","type":"string"},"datatype":"commonsMedia"},"type":"statement","id":"Q14517013$5291801E-11BE-4CE7-8F42-D0D6A120F390","rank":"normal"}],"P2867":[{"mainsnak":{"snaktype":"value","property":"P2867","hash":"b1c627972ba2cc71e3567d2fb56cb5f90dd64007","datavalue":{"value":"893","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q14517013$2aff9dcd-4d24-cd92-b5af-f6268425695f","rank":"normal"}],"P31":[{"mainsnak":{"snaktype":"value","property":"P31","hash":"9b48263bb51c506553aac2281ae331353b5c9002","datavalue":{"value":{"entity-type":"item","numeric-id":38720,"id":"Q38720"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q14517013$46dd9d89-4999-eee6-20a4-c4f6650b1d9c","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P31","hash":"a1d6f3409c57de0361c68263c9397a99dabe19ea","datavalue":{"value":{"entity-type":"item","numeric-id":3851468,"id":"Q3851468"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q14517013$C83A8B1F-7798-493A-86C9-EC0EFEE356B3","rank":"normal"},{"mainsnak":{"snaktype":"value","property":"P31","hash":"ee5ba9185bdf9f0eb80b52e1cdc70c5883fac95a","datavalue":{"value":{"entity-type":"item","numeric-id":623605,"id":"Q623605"},"type":"wikibase-entityid"},"datatype":"wikibase-item"},"type":"statement","id":"Q14517013$CF74DC2E-6814-4755-9BAD-6EE9FEF637DD","rank":"normal"}],"P2671":[{"mainsnak":{"snaktype":"value","property":"P2671","hash":"83fb38a3c6407f7d0d7bb051d1c31cea8ae26975","datavalue":{"value":"/g/121cb15z","type":"string"},"datatype":"external-id"},"type":"statement","id":"Q14517013$E6FFEF32-0131-42FD-9C66-1A406B68059A","rank":"normal"}]},"sitelinks":{"commonswiki":{"site":"commonswiki","title":"Category:Vredesmolen, Klerken","badges":[],"url":"https://commons.wikimedia.org/wiki/Category:Vredesmolen,_Klerken"},"nlwiki":{"site":"nlwiki","title":"Vredesmolen","badges":[],"url":"https://nl.wikipedia.org/wiki/Vredesmolen"}}}}} + ) + + + const wdata = await Wikidata.LoadWikidataEntryAsync(14517013) + T.isTrue(wdata.wikisites.has("nl"), "dutch for wikisite not found") + equal("Vredesmolen", wdata.wikisites.get("nl")) + } + + ] + ]); + } + +} \ No newline at end of file