diff --git a/app/routes/country.$code.jsx b/app/routes/country.$code.jsx index 16d2524..3d15f50 100644 --- a/app/routes/country.$code.jsx +++ b/app/routes/country.$code.jsx @@ -12,9 +12,22 @@ import { Text, useColorModeValue, } from '@chakra-ui/react'; +import { Prisma } from '@prisma/client'; import { fetch, json, redirect } from '@remix-run/node'; -import { useLoaderData } from '@remix-run/react'; +import { + geoAlbersUk, + geoAlbersUsaTerritories, + geoConicConformalFrance, + geoConicConformalSpain, + geoConicEquidistantJapan, +} from 'd3-composite-projections'; +import { + geoMercator, + geoBounds, + geoPath, +} from 'd3-geo'; import { scaleQuantile } from 'd3-scale'; +import { isArray } from 'lodash'; import { useMemo } from 'react'; import { ComposableMap, Geographies, Geography } from 'react-simple-maps'; @@ -25,6 +38,99 @@ import computeEvent from '../models/event'; import LinearGradient from '../components/LinearGradient'; import EventCard from '../components/EventCard'; +// according to https://en.wikipedia.org/wiki/ISO_3166-2#Subdivisions_included_in_ISO_3166-1 +const countryCodesWithSubdivisions = { + CN: ['TW', 'HK', 'MO'], + FI: ['AX'], + FR: ['BL', 'GF', 'GP', 'MF', 'MQ', 'NC', 'PF', 'PM', 'RE', 'TF', 'WF', 'YT'], + NL: ['AW', 'BQ', 'CW', 'SX'], + NO: ['SJ'], + US: ['AS', 'GU', 'MP', 'PR', 'UM', 'VI'], +}; + +const countryCodesWithRegionsGeojson = { + BE: + // prettier-ignore + ['BRU', 'VAN', 'VOV', 'VBR', 'WHT', 'VLI', 'WLG', 'WLX', 'WNA', 'WBR', 'VWV'].map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/BE/BE-${iso2}.geojson` + ), + CA: + // prettier-ignore + ['AB', 'BC', 'MB', 'NB', 'NL', 'NS', 'NT', 'NU', 'ON', 'PE', 'QC', 'SK', 'YT'].map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/CA/CA-${iso2}.geojson` + ), + CH: + // prettier-ignore + ['AG','AI','AR','BE','BL','BS','FR','GE','GL','GR','JU','LU','NE','NW','OW','SG','SH','SO','SZ','TG','TI','UR','VD','VS','ZG','ZH'].map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/CH/CH-${iso2}.geojson` + ), + CN: + // prettier-ignore + ['AH', 'BJ', 'CQ', 'FJ', 'GS', 'GD', 'GX', 'GZ', 'HI', 'HE', 'HL', 'HA', 'HB', 'HN', 'NM', 'JS', 'JX', 'JL', 'LN', 'NX', 'QH', 'SN', 'SD', 'SH', 'SX', 'SC', 'TJ', 'XZ', 'XJ', 'YN', 'ZJ'] + .map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/CN/CN-${iso2}.geojson` + ), + DE: [ + 'https://raw.githubusercontent.com/codeforgermany/click_that_hood/main/public/data/germany.geojson', + ], + ES: + // prettier-ignore + // ['AN', 'AR', 'AS', 'IB', 'PV', 'CN', 'CB', 'CL', 'CM', 'CT', 'CE', 'MD', 'EX', 'GA', 'ML', 'NC', 'MC', 'RI', 'VC'] + ['C', 'A', 'AB', 'AL', 'O', 'BA', 'PM', 'B', 'BI', 'BU', 'S', 'CS', 'CR', 'M', 'CO', 'CU', 'CC', 'CA', 'SS', 'GI', 'GR', 'GU', 'HU', 'J', 'GC', 'LE', 'L', 'LU', 'MA', 'NA', 'OR', 'P', 'PO', 'MU', 'LO', 'SA', 'TF', 'SG', 'SE', 'SO', 'T', 'TO', 'V', 'VA', 'ZA', 'Z', 'VI', 'AV'] + .map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/ES/ES-${iso2}.geojson` + ), + FR: [ + 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements-avec-outre-mer.geojson', + ], + GB: + // prettier-ignore + // ['ABE', 'ABD', 'ANS', 'AGB', 'BAS', 'BDF', 'BBD', 'BPL', 'BGW', 'XBCP', 'BRC', 'BGE', 'BNH', 'BKM', 'CAY', 'CAM', 'CRF', 'CMN', 'CBF', 'CGN', 'CHE', 'CHW', 'BST', 'EDH', 'LCE', 'LND', 'NGM', 'PTE', 'CLK', 'CWY', 'CON', 'DUR', 'CMA', 'DAL', 'DEN', 'DER', 'DBY', 'DEV', 'DOR', 'DGY', 'DND', 'EAY', 'EDU', 'ELN', 'ERW', 'ERY', 'ESX', 'ESS', 'FAL', 'FIF', 'FLN', 'GLG', 'GLS', 'GWN', 'HAL', 'HAM', 'HPL', 'HEF', 'HRT', 'HLD', 'IVC', 'AGY', 'IOW', 'IOS', 'KEN', 'KHL', 'LAN', 'LEC', 'LIN', 'LUT', 'MDW', 'MTY', 'MDB', 'MLN', 'MIK', 'MON', 'MRY', 'NTL', 'NWP', 'NFK', 'NAY', 'NEL', 'NLK', 'NLN', 'NSM', 'NYK', 'NTH', 'NBL', 'NTT', 'ORK', 'OXF', 'PEM', 'PKN', 'PLY', 'POR', 'POW', 'RDG', 'RCC', 'RFW', 'RCT', 'RUT', 'SCB', 'ZET', 'SHR', 'SLG', 'SOM', 'SAY', 'SGC', 'SLK', 'STH', 'SOS', 'STS', 'STG', 'STT', 'STE', 'SFK', 'SRY', 'SWA', 'SWD', 'TFW', 'THR', 'TOB', 'TOF', 'VGL', 'WRT', 'WAR', 'WBK', 'WDU', 'WLN', 'WSX', 'ELS', 'WIL', 'WNM', 'WOK', 'WOR', 'WRX', 'YOR'] + ['ENG', 'NIR', 'SCT', 'WLS'].map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/GB/GB-${iso2}.geojson` + ), + IE: + // prettier-ignore + ['CE', 'CN', 'CO', 'CW', 'D', 'DL', 'G', 'KE', 'KK', 'KY', 'LD', 'LH', 'LK', 'LM', 'LS', 'MH', 'MN', 'MO', 'OY', 'RN', 'SO', 'TA', 'WD', 'WH', 'WW', 'WX'] + .map(iso2 => `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/IE/IE-${iso2}.geojson`), + IT: [ + 'https://raw.githubusercontent.com/codeforgermany/click_that_hood/main/public/data/italy-provinces.geojson', + ], + JP: new Array(47) + .fill() + .map( + (d, i) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/JP/JP-${( + i + 1 + ) + .toString() + .padStart(2, 0)}.geojson` + ), + TW: [ + 'https://raw.githubusercontent.com/codeforgermany/click_that_hood/main/public/data/taiwan.geojson', + ], + US: + // prettier-ignore + ['AL', 'AK', 'AS', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'GU', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'MP', 'OH', 'OK', 'OR', 'PA', 'PR', 'RI', 'SC', 'SD', 'TN', 'TX', 'VI', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY'].map( + (iso2) => + `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/US/US-${iso2}.geojson` + ), +}; + +const countryCodeProjections = { + ES: geoConicConformalSpain(), + FR: geoConicConformalFrance(), + JP: geoConicEquidistantJapan(), + US: geoAlbersUsaTerritories(), + GB: geoAlbersUk(), +}; + export const loader = async ({ params }) => { if (!params.code) { return redirect('/countries'); @@ -32,16 +138,33 @@ export const loader = async ({ params }) => { const countryCode = params.code.toUpperCase(); + // joining subdivisions that have their own country code + const countryCodes = [ + countryCode, + ...(countryCodesWithSubdivisions[countryCode] || []), + ]; + // using remix-i18n ? // let locale = await i18next.getLocale(request); // fetch country data and use right locale const regions = - await db.$queryRaw`select region as name, count(e.id)::int from indieco.location l left join indieco.entity e on l.id = e.location_id where country_code = ${countryCode} group by region order by count desc`; + await db.$queryRaw`select region as name, count(e.id)::int from indieco.location l left join indieco.entity e on l.id = e.location_id where country_code in (${Prisma.join( + countryCodes + )}) group by region order by count desc`; + + // country does not exist + if (regions.length === 0) { + throw new Response('Not Found', { + status: 404, + }); + } const cities = - await db.$queryRaw`select city as name, region, count(e.id)::int from indieco.location l left join indieco.entity e on l.id = e.location_id where country_code = ${countryCode} group by city, region order by count desc limit 10`; + await db.$queryRaw`select city as name, region, count(e.id)::int from indieco.location l left join indieco.entity e on l.id = e.location_id where country_code in (${Prisma.join( + countryCodes + )}) group by city, region order by count desc limit 10`; // country does not exist if (cities.length === 0) { @@ -83,32 +206,8 @@ export const loader = async ({ params }) => { take: 3, }); - let geoJsonUrls = []; - - if (countryCode === 'JP') { - geoJsonUrls = new Array(47) - .fill() - .map( - (d, i) => - `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/JP/JP-${( - i + 1 - ) - .toString() - .padStart(2, 0)}.geojson` - ); - } else if (countryCode === 'FR') { - geoJsonUrls = [ - 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements-version-simplifiee.geojson', - ]; - } else if (countryCode === 'CH') { - geoJsonUrls = - // prettier-ignore - ['AG','AI','AR','BE','BL','BS','FR','GE','GL','GR','JU','LU','NE','NW','OW','SG','SH','SO','SZ','TG','TI','UR','VD','VS','ZG','ZH'] - .map( - (iso2) => - `https://raw.githubusercontent.com/hyperknot/country-levels-export/master/geojson/medium/iso2/CH/CH-${iso2}.geojson` - ); - } + // getting regions geojson + let geoJsonUrls = countryCodesWithRegionsGeojson[countryCode] || []; const geographies = await Promise.all( geoJsonUrls.map((url) => fetch(url).then((r) => r.json())) @@ -116,21 +215,40 @@ export const loader = async ({ params }) => { let projection; - if (countryCode === 'JP') { - projection = { - scale: 1500, - center: [138.2529, 36.2048], - }; - } else if (countryCode === 'FR') { - projection = { - scale: 2800, - center: [1.7191, 46.7111], - }; - } else if (countryCode === 'CH') { - projection = { - scale: 7500, - center: [8.2275, 46.8182], - }; + const enabledCountries = Object.keys(countryCodesWithRegionsGeojson); + + if ( + enabledCountries.includes(countryCode) && + !countryCodeProjections[countryCode] + ) { + const regionsFeature = + isArray(geographies) && geographies.length === 1 + ? geographies[0] + : { + type: 'FeatureCollection', + features: geographies, + }; + + // const center = geoCentroid(regionsFeature); + const bounds = geoPath(geoMercator().scale(1)).bounds(regionsFeature); + + const mapWidth = 960; + const mapHeight = 500; + + const scale = + 0.95 / + Math.max( + (bounds[1][0] - bounds[0][0]) / mapWidth, + (bounds[1][1] - bounds[0][1]) / mapHeight + ); + + const bbox = geoBounds(regionsFeature); + const center = [ + (bbox[1][0] + bbox[0][0]) / 2, + (bbox[1][1] + bbox[0][1]) / 2, + ]; + + projection = { scale, center }; } const data = { @@ -148,9 +266,18 @@ export const loader = async ({ params }) => { return json(data); }; -export const meta = ({ data: { country } }) => ({ - title: `${country.name}'s Gaming Overview | Indie Collective - Community powered video game data`, -}); +export const meta = ({ data }) => { + if (!data?.country) + return { + title: 'Country Not Found', + }; + + const { country } = data; + + return { + title: `${country.name}'s Gaming Overview | Indie Collective - Community powered video game data`, + }; +}; // light mode const COLOR_RANGE_LIGHT = [ @@ -192,19 +319,22 @@ const CountriesPage = () => { const bg = useColorModeValue('white', 'gray.900'); const geographyHoverFill = useColorModeValue('#a8dd36', '#668327'); - const geographyStyle = useMemo(() => ({ - default: { - outline: 'none', - }, - hover: { - fill: geographyHoverFill, - transition: 'all 250ms', - outline: 'none', - }, - pressed: { - outline: 'none', - }, - }), [geographyHoverFill]); + const geographyStyle = useMemo( + () => ({ + default: { + outline: 'none', + }, + hover: { + fill: geographyHoverFill, + transition: 'all 250ms', + outline: 'none', + }, + pressed: { + outline: 'none', + }, + }), + [geographyHoverFill] + ); const { regions, cities, upcomingEvents } = country; @@ -230,7 +360,7 @@ const CountriesPage = () => { { - + {geographies ? ( <> - {country.code === 'JP' && ( - { - console.log(geos); - return geos; - }} - > - {({ geographies }) => - geographies.map((geo) => { - const region = regions.find( - (item) => - item.name.replace(' Prefecture', '') === - geo.properties.name.replace(' Prefecture', '') - ); - return ( - - ); - }) - } - - )} - {country.code === 'CH' && ( - { - console.log(geos); - return geos; - }} - > - {({ geographies }) => - geographies.map((geo) => { - const region = regions.find( - (item) => item.name === geo.properties.name - ); - return ( - - ); - }) - } - - )} - {country.code === 'FR' && ( - { - console.log(geos); - return geos; - }} - > - {({ geographies }) => - geographies.map((geo) => { - const region = regions.find( - (item) => item.name === geo.properties.nom - ); - return ( - - ); - }) - } - - )} + { + console.log(geos); + return geos; + }} + > + {({ geographies }) => + geographies.map((geo) => { + const region = regions.find( + (item) => { + if (country.code === 'FR') { + return item.name === geo.properties.nom; + } + + if (country.code === 'JP') { + return item.name.replace(' Prefecture', '') === + geo.properties.name.replace(' Prefecture', ''); + } + + return item.name === geo.properties.name + } + ); + return ( + + ); + }) + } + diff --git a/package-lock.json b/package-lock.json index fcac403..3f48fea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@remix-run/serve": "^1.7.2", "@sendgrid/mail": "^7.7.0", "aws-sdk": "^2.780.0", + "d3-composite-projections": "^1.4.0", "d3-scale": "^3.3.0", "date-fns": "^2.16.1", "diff": "^5.1.0", @@ -6595,6 +6596,15 @@ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" }, + "node_modules/d3-composite-projections": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-composite-projections/-/d3-composite-projections-1.4.0.tgz", + "integrity": "sha512-csygyxdRfy7aUYRPea23veM6sjisdHI+DNd0nHcAGMd2LyL2lklr+xLRzHiJ+hy1HGp6YgAtbqdJR8CsLolrNQ==", + "dependencies": { + "d3-geo": "^2.0.1", + "d3-path": "^2.0.0" + } + }, "node_modules/d3-dispatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", @@ -6635,6 +6645,11 @@ "d3-color": "1 - 2" } }, + "node_modules/d3-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", + "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" + }, "node_modules/d3-scale": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz", diff --git a/package.json b/package.json index 261d920..242ae7f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@remix-run/serve": "^1.7.2", "@sendgrid/mail": "^7.7.0", "aws-sdk": "^2.780.0", + "d3-composite-projections": "^1.4.0", "d3-scale": "^3.3.0", "date-fns": "^2.16.1", "diff": "^5.1.0",