Skip to content

Commit 282e244

Browse files
committed
add Suriname (WIP)
1 parent ce93a45 commit 282e244

14 files changed

Lines changed: 10522 additions & 13709 deletions

data/cinema-context.tsv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
venue_name venue_type_name venue_type_url venue_description location_start_date location_end_date address_locality address_region postal_code street_address latitude longitude citation_name citation_url same_as_url
2+
Kriterion Art house cinema https://homernetwork.github.io/vocabulary/venues/Art_house_cinema Filmtheater Kriterion, officieel Vereniging Onderlinge Studentensteun Kriterion (V.O.S.S. Kriterion), is een door studenten geëxploiteerde bioscoop en studentenvereniging aan de Roetersstraat 170 in Amsterdam. Het is voortgekomen uit het studentenverzet tijdens de Tweede Wereldoorlog. 1945-11-06 Amsterdam NH 1018 WE Roetersstraat 170 52.3625 4.910556 Cinema Context http://www.cinemacontext.nl/ http://www.cinemacontext.nl/id/B000016

data/template.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
venue_name venue_type_name venue_type_url venue_description location_start_date location_end_date address_locality address_region postal_code street_address latitude longitude citation_name citation_url same_as_url

package-lock.json

Lines changed: 317 additions & 210 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,33 @@
1717
"@eslint/compat": "^1.2.9",
1818
"@eslint/js": "^9.26.0",
1919
"@sveltejs/adapter-static": "^3.0.8",
20-
"@sveltejs/kit": "^2.20.8",
20+
"@sveltejs/kit": "^2.21.0",
2121
"@sveltejs/vite-plugin-svelte": "^5.0.3",
2222
"@tailwindcss/typography": "^0.5.16",
23-
"@tailwindcss/vite": "^4.1.5",
24-
"@types/node": "^22.15.12",
23+
"@tailwindcss/vite": "^4.1.6",
24+
"@types/node": "^22.15.17",
2525
"csv-parser": "^3.2.0",
2626
"eslint": "^9.26.0",
27-
"eslint-config-prettier": "^10.1.2",
28-
"eslint-plugin-svelte": "^3.5.1",
29-
"globals": "^16.0.0",
27+
"eslint-config-prettier": "^10.1.5",
28+
"eslint-plugin-svelte": "^3.6.0",
29+
"globals": "^16.1.0",
3030
"prettier": "^3.5.3",
3131
"prettier-plugin-svelte": "^3.3.3",
3232
"prettier-plugin-tailwindcss": "^0.6.11",
33-
"svelte": "^5.28.2",
33+
"svelte": "^5.28.6",
3434
"svelte-check": "^4.1.7",
35-
"tailwindcss": "^4.1.5",
35+
"tailwindcss": "^4.1.6",
3636
"typescript": "^5.8.3",
37-
"typescript-eslint": "^8.32.0",
37+
"typescript-eslint": "^8.32.1",
3838
"vite": "^6.3.5"
3939
},
4040
"dependencies": {
41-
"@lucide/svelte": "^0.507.0",
41+
"@lucide/svelte": "^0.510.0",
42+
"bibtex-js-parser": "^1.1.6",
4243
"daisyui": "^5.0.35",
4344
"geojson": "^0.5.0",
44-
"linkify-html": "^4.2.0",
45-
"linkifyjs": "^4.2.0",
45+
"linkify-html": "^4.3.1",
46+
"linkifyjs": "^4.3.1",
4647
"maplibre-gl": "^5.5.0",
4748
"maplibre-gl-temporal-control": "^1.2.0",
4849
"svelte-multiselect": "^11.0.0-rc.1",

scripts/tsv_to_jsonld.ts

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import csvParser from 'csv-parser';
22
import * as fs from 'fs';
33
import path from 'path';
44

5-
import type { DataDistribution, Dataset, Venue } from '../src/lib/types';
5+
import type {
6+
DataDistribution,
7+
Dataset,
8+
Place,
9+
Venue,
10+
LocationRole,
11+
Address
12+
} from '../src/lib/types';
613

714
const INDEXFILE = '../static/datasets/index.jsonld';
815

@@ -73,12 +80,11 @@ const generateJsonLD = (dataset: Dataset, tsvFilepath: string, outputFilePath: s
7380
? row.additionalTypeName.split(';').map((name: string) => name.trim())
7481
: [];
7582

76-
// If no additional types, provide a standard 'Venue' type'
7783
const additionalTypes =
7884
additionalTypeIds.length > 0
7985
? additionalTypeIds.map((id: string, index: number) => ({
8086
'@id': id,
81-
name: additionalTypeNames[index] || ''
87+
name: additionalTypeNames[index] || undefined
8288
}))
8389
: [{ '@id': 'https://homernetwork.github.io/vocabulary/venues/Venue', name: 'Venue' }];
8490

@@ -94,42 +100,55 @@ const generateJsonLD = (dataset: Dataset, tsvFilepath: string, outputFilePath: s
94100
// Multiple SameAs
95101
const sameAsUrls = row.sameAs ? row.sameAs.split(';').map((url: string) => url.trim()) : [];
96102

103+
// Location
104+
const address: Address = { '@type': 'PostalAddress' };
105+
if (row.addressLocality) address.addressLocality = row.addressLocality;
106+
if (row.addressRegion) address.addressRegion = row.addressRegion;
107+
if (row.postalCode) address.postalCode = row.postalCode;
108+
if (row.streetAddress) address.streetAddress = row.streetAddress;
109+
110+
const locationDetails: Place = { '@type': 'Place' };
111+
if (Object.keys(address).length > 1) {
112+
// more than just @type
113+
locationDetails.address = address;
114+
}
115+
116+
// Geo
117+
if (row.latitude && row.longitude) {
118+
const latitude = parseFloat(row.latitude);
119+
const longitude = parseFloat(row.longitude);
120+
121+
if (isNaN(latitude) || isNaN(longitude)) {
122+
console.error(`Invalid latitude or longitude for ID: ${row.id}`);
123+
return;
124+
}
125+
126+
const geo = {
127+
'@type': 'GeoCoordinates' as const,
128+
latitude: latitude,
129+
longitude: longitude
130+
};
131+
132+
locationDetails.geo = geo;
133+
}
134+
135+
const venueLocation: LocationRole = { '@type': 'Role', location: locationDetails };
136+
if (row.startDate) venueLocation.startDate = row.startDate;
137+
if (row.endDate) venueLocation.endDate = row.endDate;
138+
97139
const venue: Venue = {
140+
'@id': row.id,
98141
'@context': 'https://schema.org/',
99142
'@type': 'Place',
100-
'@id': row.id,
101-
additionalType: additionalTypes.length > 0 ? additionalTypes : undefined,
102143
name: row.name,
103-
description: row.description,
104-
location: {
105-
'@type': 'Role',
106-
startDate: row.startDate,
107-
endDate: row.endDate,
108-
location: {
109-
'@type': 'Place',
110-
address: {
111-
'@type': 'PostalAddress',
112-
addressLocality: row.addressLocality,
113-
addressRegion: row.addressRegion,
114-
postalCode: row.postalCode,
115-
streetAddress: row.streetAddress
116-
},
117-
geo: {
118-
'@type': 'GeoCoordinates',
119-
latitude: parseFloat(row.latitude),
120-
longitude: parseFloat(row.longitude)
121-
}
122-
}
123-
},
124-
citation: citations,
125-
sameAs: sameAsUrls
144+
additionalType: additionalTypes,
145+
location: venueLocation
126146
};
127147

128-
Object.keys(venue).forEach((key) => {
129-
if (venue[key] === undefined) {
130-
delete venue[key];
131-
}
132-
});
148+
if (row.description) venue.description = row.description;
149+
150+
if (citations.length > 0) venue.citation = citations;
151+
if (sameAsUrls.length > 0) venue.sameAs = sameAsUrls;
133152

134153
venues.push(venue);
135154
})

src/lib/components/Sidebar.svelte

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,7 @@
1515
{#if $selectedFeature}
1616
<div class="h-full overflow-auto">
1717
<div class="card-body flex h-full flex-col p-4 pb-0">
18-
<div class="flex flex-none items-center justify-between px-4">
19-
<div>
20-
<h2 class="card-title text-2xl font-semibold">Details</h2>
21-
<p>Showing venue details</p>
22-
</div>
23-
<button
24-
class="btn btn-ghost"
25-
onclick={($selectedFeature = null)}
26-
aria-label="Close venue details"
27-
>
28-
<X />
29-
</button>
30-
</div>
31-
<div class="divider px-4"></div>
32-
{#if $selectedFeature}
33-
<Venue venue={$selectedFeature} />
34-
{/if}
18+
<Venue bind:venue={$selectedFeature} />
3519
</div>
3620
</div>
3721
{:else}

src/lib/components/Venue.svelte

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
import type { Venue } from '$lib/types';
33
import VenueMapPreview from './VenueMapPreview.svelte';
44
import linkifyHtml from 'linkify-html';
5-
import { MapPin, FileText, Link2, CalendarRange, Fullscreen, Copy, Check } from '@lucide/svelte';
5+
import {
6+
MapPin,
7+
FileText,
8+
Link2,
9+
CalendarRange,
10+
Fullscreen,
11+
Copy,
12+
Check,
13+
X
14+
} from '@lucide/svelte';
615
716
import { map } from '$lib/stores';
817
@@ -36,7 +45,12 @@
3645
</script>
3746

3847
<div class="bg-base-00 w-full rounded-lg px-4">
39-
<h2 class="text-2xl font-bold">{venue.name}</h2>
48+
<div class="flex flex-none items-center justify-between">
49+
<h2 class="text-2xl font-bold">{venue.name}</h2>
50+
<button class="btn btn-ghost" onclick={(venue = null)} aria-label="Close venue details">
51+
<X />
52+
</button>
53+
</div>
4054

4155
{#if venue.location.startDate}
4256
<div class="mb-3 flex items-center gap-1 text-sm opacity-80">
@@ -71,7 +85,7 @@
7185
</h3>
7286

7387
<div class="mb-3 h-48 w-full overflow-hidden">
74-
<VenueMapPreview geo={venue.location.location.geo} />
88+
<VenueMapPreview geo={venue.location.location.geo!} />
7589
</div>
7690

7791
<div class="mb-6 grid grid-cols-1 gap-2">
@@ -93,12 +107,12 @@
93107
class="badge hover:bg-base-300 flex cursor-pointer items-center gap-2 font-mono transition-colors"
94108
onclick={() =>
95109
copyToClipboard(
96-
`${venue.location.location.geo.latitude}, ${venue.location.location.geo.longitude}`,
110+
`${venue.location.location.geo!.latitude}, ${venue.location.location.geo!.longitude}`,
97111
'coordinates'
98112
)}
99113
aria-label="Copy coordinates"
100114
>
101-
{venue.location.location.geo.latitude}, {venue.location.location.geo.longitude}
115+
{venue.location.location.geo!.latitude}, {venue.location.location.geo!.longitude}
102116
{#if copiedStates['coordinates']}
103117
<Check size={14} class="text-success" />
104118
{:else}

src/lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type GeoCoordinates = {
2020
export type Place = {
2121
'@type': 'Place';
2222
address?: Address;
23-
geo: GeoCoordinates;
23+
geo?: GeoCoordinates;
2424
};
2525

2626
export type LocationRole = {

src/lib/utils.ts

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
import { countFeatures, venueTypes } from './stores';
22
import type { Dataset, Venue } from '$lib/types';
3-
import { type Option } from 'svelte-multiselect';
3+
import type { ObjectOption } from 'svelte-multiselect';
44

55
import { base } from '$app/paths';
66

7-
export const loadData = async () => {
8-
const res = await fetch(base + 'datasets/index.json');
7+
type Fetch = typeof fetch;
8+
9+
export const loadData = async (fetch: Fetch) => {
10+
const res = await fetch(base + '/datasets/index.jsonld');
911
const index = await res.json();
1012

1113
const venuesData: Venue[] = [];
1214

13-
index.dataset.forEach(async (dataset: Dataset) => {
14-
console.log(dataset['@id']);
15-
const data = await fetch(dataset['@id']).then((res) => res.json());
16-
venuesData.push(...data);
17-
});
15+
if (index.dataset && Array.isArray(index.dataset)) {
16+
await Promise.all(
17+
index.dataset.map(async (dataset: Dataset) => {
18+
const contentUrl = dataset.distribution.find(
19+
(distribution) => distribution.encodingFormat === 'application/ld+json'
20+
)?.contentUrl;
21+
22+
const res = await fetch('datasets/' + contentUrl);
23+
const data = await res.json();
24+
venuesData.push(...data.hasPart);
25+
})
26+
);
27+
} else {
28+
console.error('No dataset array found in the index:', index);
29+
}
1830

1931
const geoJsonFeatures = convertToGeoJson(venuesData);
2032
countFeatures.set(geoJsonFeatures.features.length);
@@ -26,48 +38,63 @@ export const loadData = async () => {
2638
};
2739

2840
const convertToGeoJson = (data: Venue[]) => {
41+
const features = data.reduce((features: GeoJSON.Feature[], venue, index) => {
42+
if (!venue.location?.location?.geo?.longitude || !venue.location?.location?.geo?.latitude) {
43+
return features;
44+
}
45+
46+
const type = Array.isArray(venue.additionalType)
47+
? venue.additionalType.map((type) => type['@id'])
48+
: [];
49+
50+
const startYear = venue.location?.startDate
51+
? new Date(venue.location.startDate).getFullYear()
52+
: null;
53+
const endYear = venue.location?.endDate
54+
? new Date(venue.location.endDate).getFullYear()
55+
: new Date().getFullYear();
56+
57+
const feature: GeoJSON.Feature = {
58+
id: index,
59+
type: 'Feature',
60+
geometry: {
61+
type: 'Point',
62+
coordinates: [venue.location.location.geo.longitude, venue.location.location.geo.latitude]
63+
},
64+
properties: {
65+
name: venue.name,
66+
nameLower: venue.name.toLowerCase(),
67+
type: type,
68+
startYear: startYear,
69+
endYear: endYear,
70+
venue: venue // Full venue data
71+
}
72+
};
73+
74+
features.push(feature);
75+
return features;
76+
}, []);
77+
2978
const geojson = {
3079
type: 'FeatureCollection',
31-
features: data.map((venue) => {
32-
const i = data.indexOf(venue);
33-
const type = venue.additionalType.map((type) => type['@id']);
34-
const startYear = venue.location.startDate
35-
? new Date(venue.location.startDate).getFullYear()
36-
: null;
37-
const endYear = venue.location.endDate
38-
? new Date(venue.location.endDate).getFullYear()
39-
: new Date().getFullYear();
40-
return {
41-
id: i,
42-
type: 'Feature',
43-
geometry: {
44-
type: 'Point',
45-
coordinates: [venue.location.location.geo.longitude, venue.location.location.geo.latitude]
46-
},
47-
properties: {
48-
name: venue.name,
49-
type: type,
50-
startYear: startYear,
51-
endYear: endYear,
52-
venue: venue // Full venue data (as string)
53-
}
54-
};
55-
})
80+
features: features
5681
};
5782

5883
return geojson;
5984
};
6085

6186
const getVenueTypes = (data: Venue[]) => {
62-
const venueTypes = data.map((venue) => venue.additionalType);
63-
const uniqueVenueTypes = [...new Set(venueTypes.flat())];
87+
const allVenueTypes = data.flatMap((venue) => venue.additionalType);
6488

65-
const options: Option[] = uniqueVenueTypes.map((venueType) => {
66-
return {
67-
label: venueType.name,
68-
value: venueType['@id']
69-
};
89+
const uniqueTypesMap = new Map();
90+
allVenueTypes.forEach((venueType) => {
91+
uniqueTypesMap.set(venueType['@id'], venueType);
7092
});
7193

72-
return options.sort();
94+
const options: ObjectOption[] = Array.from(uniqueTypesMap.values()).map((venueType) => ({
95+
label: venueType.name,
96+
value: venueType['@id']
97+
}));
98+
99+
return options.sort((a, b) => String(a.label).localeCompare(String(b.label)));
73100
};

0 commit comments

Comments
 (0)